@dzhechkov/harness-core 0.8.6 → 0.8.10
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 +257 -53
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +9 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +55 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +178 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +9 -8
- package/sbom.json +562 -52
- package/src/amendment-trace.ts +12 -1
- package/src/compounding.ts +300 -1
- package/src/eta.ts +590 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +70 -4
- package/src/index.ts +63 -2
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/operations.ts +199 -2
- package/src/patterns.ts +252 -43
- package/src/setup.ts +20 -17
- package/src/target-integrations.ts +225 -0
- package/src/vector-tier.ts +44 -14
package/src/guard.ts
CHANGED
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
// PURE: `evaluateGuard` operates over INJECTED FACTS (package.json deps, a drift result, lesson text, README
|
|
12
12
|
// counts, store size) that the CLI gathers. No filesystem here → deterministic + unit-testable without a repo.
|
|
13
13
|
|
|
14
|
-
import { type RuleTemplate, type TemplateParams, type ChangeSet, templateFires, validTemplateParams } from './guard-promotion.js';
|
|
14
|
+
import { lessonRuleContentAnchor, type RuleTemplate, type TemplateParams, type ChangeSet, templateFires, validTemplateParams } from './guard-promotion.js';
|
|
15
15
|
import { STUB_MARKERS, STUB_PHRASES, checkNoStubs, type StubWaiver } from './no-stubs.js';
|
|
16
|
+
import {
|
|
17
|
+
VOLUME_SHADOW_RULE_IDS,
|
|
18
|
+
evaluateVolumeShadow,
|
|
19
|
+
unknownVolumeShadow,
|
|
20
|
+
type GuardObservation,
|
|
21
|
+
type VolumeShadowInput,
|
|
22
|
+
type VolumeShadowResult,
|
|
23
|
+
} from './guard-volume.js';
|
|
16
24
|
|
|
17
25
|
export type GuardSeverity = 'hard' | 'soft';
|
|
18
26
|
export type GuardOp = 'publish' | 'teach' | 'consolidate' | 'reindex';
|
|
@@ -39,6 +47,8 @@ export interface Violation {
|
|
|
39
47
|
readonly rule: string;
|
|
40
48
|
readonly severity: GuardSeverity;
|
|
41
49
|
readonly detail: string;
|
|
50
|
+
/** Present for promoted-template firings; ids alone do not prove equal effective rule content. */
|
|
51
|
+
readonly contentAnchor?: string;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export interface GuardResult {
|
|
@@ -54,11 +64,15 @@ export interface GuardResult {
|
|
|
54
64
|
* argument; a note is the cheap fix. Present only when non-empty.
|
|
55
65
|
*/
|
|
56
66
|
readonly notes?: readonly string[];
|
|
67
|
+
/** Versioned shadow measurements. They are evidence only and never enter the verdict reducer. */
|
|
68
|
+
readonly observations?: readonly GuardObservation[];
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** Facts the CLI injects; each rule reads only the fields it needs. Missing evidence ⇒ that rule is skipped. */
|
|
60
72
|
export interface GuardFacts {
|
|
61
73
|
readonly op: GuardOp;
|
|
74
|
+
/** Publish-only raw volume facts. Absence preserves the legacy result shape. */
|
|
75
|
+
readonly volume?: VolumeShadowInput;
|
|
62
76
|
/** for no-workspace-star: each publishable package's deps map. */
|
|
63
77
|
readonly packages?: readonly { readonly name: string; readonly deps: Readonly<Record<string, string>> }[];
|
|
64
78
|
/** for no-skill-drift: the names that byte-drift between copies (from sweepSkillDrift). */
|
|
@@ -287,6 +301,10 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
287
301
|
{ id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
|
|
288
302
|
{ id: 'routing-store-stale', severity: 'soft', ops: ['publish'], description: 'harvested routing telemetry has been applied to the auto-cost outcome store' },
|
|
289
303
|
{ id: 'marketplace-parity', severity: 'soft', ops: ['publish'], description: 'the published .claude-plugin/ showcase composition matches a fresh regeneration from the live registry (version excluded — an operator field)' },
|
|
304
|
+
{ id: 'template-context-token-weight', severity: 'soft', ops: ['publish'], description: 'observe the UTF-8-byte estimated token weight of each selected template context corpus; measured starting points are advisory only' },
|
|
305
|
+
{ id: 'template-context-largest-file-share', severity: 'soft', ops: ['publish'], description: 'observe the largest template file share on the same estimated-token basis as its corpus total; advisory only' },
|
|
306
|
+
{ id: 'feature-artifact-diff-ratio', severity: 'soft', ops: ['publish'], description: 'observe feature artifact bytes against attributable unified-diff bytes, explicitly a proxy; advisory only' },
|
|
307
|
+
{ id: 'feature-tier-artifact-set', severity: 'soft', ops: ['publish'], description: 'observe artifacts due for the recorded feature tier, active steps, consumers, and lifecycle; advisory only' },
|
|
290
308
|
{ id: 'agents-md-policy-sync', severity: 'soft', ops: ['publish'], description: 'proves the AGENTS.md copy is in SYNC with its source — not that the runtime read or obeyed it; heal drift with dz agents-sync' },
|
|
291
309
|
{ id: 'lockfile-in-sync', severity: 'soft', ops: ['publish'], description: 'every workspace @dzhechkov/* dependency spec matches the specifier pnpm-lock.yaml records for that importer (a dep bump without a lockfile refresh breaks CI with ERR_PNPM_OUTDATED_LOCKFILE). SOFT-ONLY — a config cannot promote it to HARD' },
|
|
292
310
|
{ id: 'store-bloat-cap', severity: 'soft', ops: ['teach', 'consolidate'], description: 'the learned store is within its size cap' },
|
|
@@ -586,7 +604,13 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
586
604
|
* may not understand a file, and "I might be wrong" plus "block the publish" is the wrong pair. Disabling
|
|
587
605
|
* such a rule stays allowed — only the promotion is refused.
|
|
588
606
|
*/
|
|
589
|
-
export const SOFT_ONLY_RULES: readonly string[] = [
|
|
607
|
+
export const SOFT_ONLY_RULES: readonly string[] = [
|
|
608
|
+
'lockfile-in-sync',
|
|
609
|
+
'agents-md-policy-sync',
|
|
610
|
+
'routing-store-stale',
|
|
611
|
+
'marketplace-parity',
|
|
612
|
+
...VOLUME_SHADOW_RULE_IDS,
|
|
613
|
+
];
|
|
590
614
|
|
|
591
615
|
/**
|
|
592
616
|
* A well-formed PROMOTED rule: an id the engine does not know, made enforceable by a template +
|
|
@@ -612,7 +636,12 @@ function templateChecker(rule: GuardRule & { template: RuleTemplate; params: Tem
|
|
|
612
636
|
const r = templateFires(rule.template, rule.params, change);
|
|
613
637
|
if (Object.hasOwn(r, 'undecidable') || !(r as { fired?: boolean }).fired) return [];
|
|
614
638
|
// A promoted rule is ALWAYS soft, whatever severity reaches this point (belt to resolveRules' braces).
|
|
615
|
-
return [{
|
|
639
|
+
return [{
|
|
640
|
+
rule: rule.id,
|
|
641
|
+
severity: 'soft',
|
|
642
|
+
detail: `${(r as { detail?: string }).detail ?? 'template rule fired'} (promoted rule — advisory)`,
|
|
643
|
+
contentAnchor: lessonRuleContentAnchor(rule.template, rule.params),
|
|
644
|
+
}];
|
|
616
645
|
};
|
|
617
646
|
}
|
|
618
647
|
|
|
@@ -671,8 +700,32 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
671
700
|
);
|
|
672
701
|
const violations: Violation[] = [];
|
|
673
702
|
const checked: string[] = [];
|
|
703
|
+
const observations: GuardObservation[] = [];
|
|
704
|
+
let volumeResult: VolumeShadowResult | undefined;
|
|
705
|
+
let volumeEvaluated = false;
|
|
706
|
+
const volume = (): VolumeShadowResult => {
|
|
707
|
+
if (volumeEvaluated) return volumeResult ?? { observations: [], signals: [], notes: [] };
|
|
708
|
+
volumeEvaluated = true;
|
|
709
|
+
try {
|
|
710
|
+
volumeResult = evaluateVolumeShadow(facts.volume);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
volumeResult = unknownVolumeShadow(
|
|
713
|
+
'volume-evaluator-failure',
|
|
714
|
+
error instanceof Error ? error.message : String(error),
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return volumeResult;
|
|
718
|
+
};
|
|
674
719
|
for (const r of active) {
|
|
675
720
|
checked.push(r.id);
|
|
721
|
+
if ((VOLUME_SHADOW_RULE_IDS as readonly string[]).includes(r.id)) {
|
|
722
|
+
const emission = volume();
|
|
723
|
+
observations.push(...emission.observations.filter((item) => item.rule === r.id));
|
|
724
|
+
violations.push(...emission.signals
|
|
725
|
+
.filter((signal) => signal.rule === r.id)
|
|
726
|
+
.map((signal) => ({ rule: signal.rule, severity: 'soft' as const, detail: signal.detail })));
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
676
729
|
// A promoted (template) rule has no built-in checker by design — it is enforceable through the
|
|
677
730
|
// shared `templateFires` predicate instead. Without this branch a promoted rule written into
|
|
678
731
|
// `.dz/guard.json` would be INERT: present in the config, listed as checked, enforcing nothing —
|
|
@@ -720,7 +773,17 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
720
773
|
notes.push('agents-md-policy-sync: this repository carries a dz:policies fence but its canonical policy sources are unreadable — the advisory rule could not compare, and that gap is on the record, not a silent pass');
|
|
721
774
|
}
|
|
722
775
|
}
|
|
723
|
-
|
|
776
|
+
for (const item of observations) {
|
|
777
|
+
if (item.status === 'unknown') notes.push(`${item.rule} ${item.scope}: ${item.detail}`);
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
op,
|
|
781
|
+
verdict,
|
|
782
|
+
violations,
|
|
783
|
+
checked,
|
|
784
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
785
|
+
...(observations.length > 0 ? { observations } : {}),
|
|
786
|
+
};
|
|
724
787
|
}
|
|
725
788
|
|
|
726
789
|
/** An append-only audit record. `ts` is injected by the caller (no wall-clock here → deterministic tests). */
|
|
@@ -731,6 +794,8 @@ export interface GuardAuditRecord {
|
|
|
731
794
|
readonly violations: readonly Violation[];
|
|
732
795
|
/** informational notes (FN-7 — e.g. the no-stubs skipped-files note); on the record, never a verdict input. */
|
|
733
796
|
readonly notes?: readonly string[];
|
|
797
|
+
/** Shadow measurements copied without recomputation from the evaluated result. */
|
|
798
|
+
readonly observations?: readonly GuardObservation[];
|
|
734
799
|
/** set when the operator overrode a block with `--force <reason>` — the override is logged, never silent. */
|
|
735
800
|
readonly override?: { readonly forced: true; readonly reason: string };
|
|
736
801
|
}
|
|
@@ -743,6 +808,7 @@ export function auditRecord(result: GuardResult, ts: string, override?: { reason
|
|
|
743
808
|
verdict: result.verdict,
|
|
744
809
|
violations: result.violations,
|
|
745
810
|
...(Array.isArray(result.notes) && result.notes.length > 0 ? { notes: result.notes } : {}),
|
|
811
|
+
...(Array.isArray(result.observations) && result.observations.length > 0 ? { observations: result.observations } : {}),
|
|
746
812
|
...(override && typeof override.reason === 'string' ? { override: { forced: true, reason: override.reason } } : {}),
|
|
747
813
|
};
|
|
748
814
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,11 @@ export * from './apply.js';
|
|
|
15
15
|
export { bundleSkills } from './bundle.js';
|
|
16
16
|
export type { BundleOptions, BundleResult, BundledSkill } from './bundle.js';
|
|
17
17
|
export * from './targets.js';
|
|
18
|
+
export * from './target-integrations.js';
|
|
19
|
+
export * from './integration-evidence.js';
|
|
20
|
+
export * from './integration-apply.js';
|
|
21
|
+
export * from './integrations-verify.js';
|
|
22
|
+
export { redactProbeText, PROBE_REDACTION_MARKER } from './integration-probe-worker.js';
|
|
18
23
|
// Target-parity model (feature target-parity-matrix, ADR-001) — the computed feature×target map.
|
|
19
24
|
export {
|
|
20
25
|
RUNTIME_CAPABILITIES,
|
|
@@ -94,7 +99,8 @@ export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, dis
|
|
|
94
99
|
export * from './package-skill-layouts.js';
|
|
95
100
|
export { recommend } from './recommend.js';
|
|
96
101
|
export { pretrain } from './pretrain.js';
|
|
97
|
-
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, loadStoreRecords, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
102
|
+
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, recordLessonForms, loadStorePatternsSync, loadStoreRecords, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
103
|
+
export { normalizeLessonForms, validateClassTemplate, lessonPairIdOf, mergeLessonFormHits, mergeLessonMatchedForms } from './lesson-generalization.js';
|
|
98
104
|
export { withStoreLock, withStoreLockSync, storeLockPath, StoreLockTimeoutError, StoreLockCompromisedError, STALE_LOCK_MS, LOCK_TIMEOUT_MS } from './store-lock.js';
|
|
99
105
|
export type { StoreLockOptions } from './store-lock.js';
|
|
100
106
|
export { withNamedLockSync, namedLockPath, isSafeLockName, NamedLockNameError, NamedLockTimeoutError, NamedLockCompromisedError } from './named-lock.js';
|
|
@@ -121,7 +127,8 @@ export {
|
|
|
121
127
|
renderBridgeReport,
|
|
122
128
|
} from './qe-bridge.js';
|
|
123
129
|
export type { BridgeFamily, BridgeFailureReason, BridgeFinding, BridgeSignoff, BridgeParse, BridgeParseOk, BridgeParseFail, BridgeChannels, BridgeAudit, ClaudeResultExtraction, NamedExtract, BridgePromptInput } from './qe-bridge.js';
|
|
124
|
-
export type { PatternRecord, SessionRecord, LearningConfig, MemoryLearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult, ReinforcementState, ReinforcePatternResult, StoreStats, LessonDeltaReport, LessonDeltaRow, QuarantineState, PromoteResult, QuarantineExpiryCandidate } from './patterns.js';
|
|
130
|
+
export type { PatternRecord, SessionRecord, LearningConfig, MemoryLearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, RecallPatternsOptions, RecordLessonFormsResult, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult, ReinforcementState, ReinforcePatternResult, StoreStats, LessonDeltaReport, LessonDeltaRow, QuarantineState, PromoteResult, QuarantineExpiryCandidate } from './patterns.js';
|
|
131
|
+
export type { LessonForm, LessonMatchedForm, LessonFormsInput, LessonFormRankedHit, MergedLessonFormHit } from './lesson-generalization.js';
|
|
125
132
|
export { DEFAULT_REINFORCE_THRESHOLD, NoopLearningBackend, NativeReinforcementBackend, resolveLearningBackend, isLearningSignalBackend, applyLearningSignals, applyLearningSignalsWithDelta, applyLearningSignalsWithTerms } from './learning-backend.js';
|
|
126
133
|
export type { LearningSignalBackend, LearningSignalStats, LearningSample, SignalCandidate, EnhanceContext, TrainingResult, LearningBackendMode, RerankTerm } from './learning-backend.js';
|
|
127
134
|
// lesson-bandit-rerank (I-8): the ACL's public surface only. The vendored engine class is
|
|
@@ -186,6 +193,15 @@ export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf,
|
|
|
186
193
|
agentdbStorePath, agentdbMcpStorePath, agentdbStoreSeparationProblem } from './setup.js';
|
|
187
194
|
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
|
|
188
195
|
export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
|
|
196
|
+
export {
|
|
197
|
+
ETA_MAX_STAGE_MS,
|
|
198
|
+
estimateEta,
|
|
199
|
+
extractStageSamples,
|
|
200
|
+
formatEta,
|
|
201
|
+
parseCheckpointLines,
|
|
202
|
+
segmentRun,
|
|
203
|
+
} from './eta.js';
|
|
204
|
+
export type { CheckpointObservation, EtaEstimate, EtaInput, IncompleteCoverageSample, RunSegment, StageDurationSample, StageSample } from './eta.js';
|
|
189
205
|
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES } from './agentdb-index.js';
|
|
190
206
|
export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
|
|
191
207
|
export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
|
|
@@ -250,6 +266,50 @@ export { hookDecision, isFenced, isNewLine, ESCAPE_TEACHING } from './claim-chec
|
|
|
250
266
|
export type { HookDecision, HookDecisionOpts } from './claim-check-hook-policy.js';
|
|
251
267
|
export { step8ClaimGate } from './feature-adr-claim-gate.js';
|
|
252
268
|
export type { Step8ClaimCounts, Step8ClaimGate } from './feature-adr-claim-gate.js';
|
|
269
|
+
export {
|
|
270
|
+
DEFAULT_CODE_LANDING_CEILING_MS,
|
|
271
|
+
CODE_LANDING_CEILING_ENV,
|
|
272
|
+
decideCodeLandingLiveness,
|
|
273
|
+
extractCodexCompanionJobId,
|
|
274
|
+
parseCodeLandingLivenessSignal,
|
|
275
|
+
} from './feature-adr-landing.js';
|
|
276
|
+
export type {
|
|
277
|
+
CodeLandingLivenessVerdict,
|
|
278
|
+
CodeLandingLivenessReason,
|
|
279
|
+
CodeLandingLivenessInput,
|
|
280
|
+
CodeLandingLivenessDecision,
|
|
281
|
+
CodeLandingLivenessProbe,
|
|
282
|
+
} from './feature-adr-landing.js';
|
|
283
|
+
export {
|
|
284
|
+
buildDecisionContext,
|
|
285
|
+
normalizeDecisionRecall,
|
|
286
|
+
parseDecisionRecallFrame,
|
|
287
|
+
parseDecisionRecallApplicationProbe,
|
|
288
|
+
mergeDecisionRecallEvents,
|
|
289
|
+
reduceDecisionRecallMetrics,
|
|
290
|
+
summarizeDecisionRecallReceipts,
|
|
291
|
+
decisionRecallEnterCmd,
|
|
292
|
+
decisionRecallRunCmd,
|
|
293
|
+
decisionRecallAppendCmd,
|
|
294
|
+
decisionRecallApplicationProbeCmd,
|
|
295
|
+
} from './feature-adr-decision-recall.js';
|
|
296
|
+
export type {
|
|
297
|
+
DecisionRecallKind,
|
|
298
|
+
DecisionRecallStage,
|
|
299
|
+
DecisionRecallOutcomeName,
|
|
300
|
+
DecisionRecallContext,
|
|
301
|
+
DecisionRecallFrame,
|
|
302
|
+
DecisionRecallSelection,
|
|
303
|
+
DecisionRecallOutcome,
|
|
304
|
+
DecisionRecallEvent,
|
|
305
|
+
DecisionRecallDisposition,
|
|
306
|
+
DecisionRecallApplicationProbe,
|
|
307
|
+
MergedDecisionRecallAttempt,
|
|
308
|
+
MergedDecisionRecallDecision,
|
|
309
|
+
MergedDecisionRecallEvents,
|
|
310
|
+
DecisionRecallRatio,
|
|
311
|
+
DecisionRecallMetrics,
|
|
312
|
+
} from './feature-adr-decision-recall.js';
|
|
253
313
|
export {
|
|
254
314
|
CHECKPOINT_STAGES,
|
|
255
315
|
STAGE_ARTIFACTS,
|
|
@@ -826,6 +886,7 @@ export * from './model-recommender.js';
|
|
|
826
886
|
export * from './bto-optimize.js';
|
|
827
887
|
export * from './discrimination-gate.js';
|
|
828
888
|
export * from './guard.js';
|
|
889
|
+
export * from './guard-volume.js';
|
|
829
890
|
// Root AGENTS.md policy projection: pure anchor extraction/render/hash drift +
|
|
830
891
|
// the shared-emitter I/O entry point exported from operations.
|
|
831
892
|
export * from './agents-policy.js';
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/** Journaled, ownership-aware project carrier transaction. */
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from 'node:fs';
|
|
19
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { canonicalIntegrationJson, parseStrictJson, type CarrierFragment, type IntegrationReasonCode } from '@dzhechkov/core';
|
|
22
|
+
|
|
23
|
+
import { withNamedLockSync } from './named-lock.js';
|
|
24
|
+
|
|
25
|
+
const MAX_CARRIER_BYTES = 1024 * 1024;
|
|
26
|
+
const JOURNAL_REL = '.dz/integrations-ownership.json';
|
|
27
|
+
|
|
28
|
+
type JournalState = 'pending' | 'committed';
|
|
29
|
+
|
|
30
|
+
interface JournalEntry {
|
|
31
|
+
readonly state: JournalState;
|
|
32
|
+
readonly carrierPath: string;
|
|
33
|
+
readonly entryId: string;
|
|
34
|
+
readonly previousCarrierDigest: string;
|
|
35
|
+
readonly desiredCarrierDigest: string;
|
|
36
|
+
readonly desiredContentHash: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JournalV1 {
|
|
40
|
+
readonly version: 1;
|
|
41
|
+
readonly entries: Record<string, JournalEntry>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type IntegrationApplyFault = 'after-pending' | 'after-carrier' | 'after-committed';
|
|
45
|
+
|
|
46
|
+
export interface ApplyIntegrationOptions {
|
|
47
|
+
readonly projectRoot: string;
|
|
48
|
+
readonly fragments: readonly CarrierFragment[];
|
|
49
|
+
/** Test-only fault seam; production callers omit it. */
|
|
50
|
+
readonly injectFault?: IntegrationApplyFault;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IntegrationApplyReport {
|
|
54
|
+
readonly written: readonly string[];
|
|
55
|
+
readonly alreadyCurrent: readonly string[];
|
|
56
|
+
readonly journalPath: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class IntegrationApplyError extends Error {
|
|
60
|
+
constructor(
|
|
61
|
+
readonly reasonCode: IntegrationReasonCode,
|
|
62
|
+
message: string,
|
|
63
|
+
/** Carrier bytes may already be durable while ownership remains unverified. */
|
|
64
|
+
readonly applied = false,
|
|
65
|
+
) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'IntegrationApplyError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sha(value: string | Buffer): string {
|
|
72
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function absentOrDigest(bytes: string | undefined): string {
|
|
76
|
+
return bytes === undefined ? 'sha256:absent' : sha(bytes);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertPlainObject(value: unknown, label: string): Record<string, unknown> {
|
|
80
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
81
|
+
throw new IntegrationApplyError('CONFIG_MALFORMED', `${label} must contain a JSON object`);
|
|
82
|
+
}
|
|
83
|
+
return value as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readBoundedJson(path: string, label: string): { bytes: string; value: Record<string, unknown> } | undefined {
|
|
87
|
+
if (!existsSync(path)) return undefined;
|
|
88
|
+
const st = lstatSync(path);
|
|
89
|
+
if (st.isSymbolicLink() || !st.isFile()) throw new IntegrationApplyError('UNSAFE_PATH', `${label} must be a regular non-symlink file`);
|
|
90
|
+
if (st.size > MAX_CARRIER_BYTES) throw new IntegrationApplyError('CONFIG_MALFORMED', `${label} exceeds ${MAX_CARRIER_BYTES} bytes`);
|
|
91
|
+
const bytes = readFileSync(path, 'utf8');
|
|
92
|
+
try {
|
|
93
|
+
return { bytes, value: assertPlainObject(parseStrictJson(bytes, { label, maxBytes: MAX_CARRIER_BYTES }), label) };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof IntegrationApplyError) throw error;
|
|
96
|
+
throw new IntegrationApplyError('CONFIG_MALFORMED', error instanceof Error ? error.message : String(error));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function journalFrom(value: Record<string, unknown>): JournalV1 {
|
|
101
|
+
if (value['version'] !== 1) throw new IntegrationApplyError('CONFIG_MALFORMED', 'integration journal version must be 1');
|
|
102
|
+
const rawEntries = assertPlainObject(value['entries'], 'integration journal entries');
|
|
103
|
+
const entries = Object.create(null) as Record<string, JournalEntry>;
|
|
104
|
+
for (const [key, raw] of Object.entries(rawEntries)) {
|
|
105
|
+
const row = assertPlainObject(raw, `integration journal entry ${key}`);
|
|
106
|
+
const state = row['state'];
|
|
107
|
+
if (state !== 'pending' && state !== 'committed') throw new IntegrationApplyError('CONFIG_MALFORMED', `integration journal entry ${key} has invalid state`);
|
|
108
|
+
for (const field of ['carrierPath', 'entryId', 'previousCarrierDigest', 'desiredCarrierDigest', 'desiredContentHash'] as const) {
|
|
109
|
+
if (typeof row[field] !== 'string') throw new IntegrationApplyError('CONFIG_MALFORMED', `integration journal entry ${key} lacks ${field}`);
|
|
110
|
+
}
|
|
111
|
+
entries[key] = {
|
|
112
|
+
state,
|
|
113
|
+
carrierPath: row['carrierPath'] as string,
|
|
114
|
+
entryId: row['entryId'] as string,
|
|
115
|
+
previousCarrierDigest: row['previousCarrierDigest'] as string,
|
|
116
|
+
desiredCarrierDigest: row['desiredCarrierDigest'] as string,
|
|
117
|
+
desiredContentHash: row['desiredContentHash'] as string,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { version: 1, entries };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ensureSafeProjectRoot(projectRoot: string): string {
|
|
124
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
125
|
+
const lexical = resolve(projectRoot);
|
|
126
|
+
const actual = realpathSync(projectRoot);
|
|
127
|
+
if (lexical !== actual || lstatSync(projectRoot).isSymbolicLink()) {
|
|
128
|
+
throw new IntegrationApplyError('UNSAFE_PATH', 'project root must be a real non-symlink directory');
|
|
129
|
+
}
|
|
130
|
+
const dz = join(actual, '.dz');
|
|
131
|
+
if (!existsSync(dz)) mkdirSync(dz, { mode: 0o700 });
|
|
132
|
+
const dzStat = lstatSync(dz);
|
|
133
|
+
if (dzStat.isSymbolicLink() || !dzStat.isDirectory() || realpathSync(dz) !== dz) {
|
|
134
|
+
throw new IntegrationApplyError('UNSAFE_PATH', '.dz must be a real non-symlink directory');
|
|
135
|
+
}
|
|
136
|
+
return actual;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function confinedPath(root: string, relativePath: string): string {
|
|
140
|
+
if (relativePath.startsWith('/') || relativePath.split('/').includes('..')) {
|
|
141
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `unsafe carrier path ${JSON.stringify(relativePath)}`);
|
|
142
|
+
}
|
|
143
|
+
const absolute = resolve(root, ...relativePath.split('/'));
|
|
144
|
+
const rel = relative(root, absolute);
|
|
145
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || resolve(root, rel) !== absolute) {
|
|
146
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `carrier escapes project root: ${relativePath}`);
|
|
147
|
+
}
|
|
148
|
+
const parent = dirname(absolute);
|
|
149
|
+
if (parent !== root) {
|
|
150
|
+
mkdirSync(parent, { recursive: true });
|
|
151
|
+
let walk = root;
|
|
152
|
+
for (const part of relative(root, parent).split(sep).filter(Boolean)) {
|
|
153
|
+
walk = join(walk, part);
|
|
154
|
+
const st = lstatSync(walk);
|
|
155
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new IntegrationApplyError('UNSAFE_PATH', `carrier parent is not a real directory: ${relativePath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) {
|
|
159
|
+
throw new IntegrationApplyError('UNSAFE_PATH', `carrier must not be a symlink: ${relativePath}`);
|
|
160
|
+
}
|
|
161
|
+
return absolute;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fsyncDirectory(path: string): void {
|
|
165
|
+
let fd: number | undefined;
|
|
166
|
+
try {
|
|
167
|
+
fd = openSync(path, constants.O_RDONLY);
|
|
168
|
+
fsyncSync(fd);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
171
|
+
if (!['EINVAL', 'ENOTSUP', 'EISDIR', 'EPERM'].includes(code ?? '')) throw error;
|
|
172
|
+
} finally {
|
|
173
|
+
if (fd !== undefined) closeSync(fd);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function durableReplace(path: string, bytes: string): void {
|
|
178
|
+
const parent = dirname(path);
|
|
179
|
+
const temp = join(parent, `.${relative(parent, path)}.dz-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
180
|
+
let fd: number | undefined;
|
|
181
|
+
try {
|
|
182
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
183
|
+
fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollow, 0o600);
|
|
184
|
+
writeFileSync(fd, bytes, 'utf8');
|
|
185
|
+
fsyncSync(fd);
|
|
186
|
+
closeSync(fd); fd = undefined;
|
|
187
|
+
renameSync(temp, path);
|
|
188
|
+
const reread = readFileSync(path, 'utf8');
|
|
189
|
+
if (reread !== bytes) throw new IntegrationApplyError('APPLY_FAILED', `durable reread mismatch for ${path}`);
|
|
190
|
+
fsyncDirectory(parent);
|
|
191
|
+
} finally {
|
|
192
|
+
if (fd !== undefined) closeSync(fd);
|
|
193
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function entryHash(value: unknown): string {
|
|
198
|
+
return sha(canonicalIntegrationJson(value));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isKnownAgentDbLegacy(id: string, value: unknown, root: string): boolean {
|
|
202
|
+
if (id !== 'mcpServers.agentdb' || value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
203
|
+
const row = value as Record<string, unknown>;
|
|
204
|
+
if (!Object.keys(row).every((key) => ['command', 'args', 'env'].includes(key))) return false;
|
|
205
|
+
if (row['command'] !== 'npx' || !Array.isArray(row['args'])) return false;
|
|
206
|
+
const args = row['args'];
|
|
207
|
+
if (args.length !== 3 || typeof args[0] !== 'string' || !/^agentdb@(?:latest|[^\s/]+)$/.test(args[0]) || args[1] !== 'mcp' || args[2] !== 'start') return false;
|
|
208
|
+
if (row['env'] === undefined) return true;
|
|
209
|
+
const env = row['env'];
|
|
210
|
+
if (env === null || typeof env !== 'object' || Array.isArray(env) || Object.keys(env).length !== 1) return false;
|
|
211
|
+
const path = (env as Record<string, unknown>)['AGENTDB_PATH'];
|
|
212
|
+
return path === join(root, '.dz', 'agentdb.db') || path === join(root, '.dz', 'agentdb-mcp.db');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Apply all project JSON fragments under one short project-journal lock. */
|
|
216
|
+
export function applyIntegrationFragments(options: ApplyIntegrationOptions): IntegrationApplyReport {
|
|
217
|
+
const root = ensureSafeProjectRoot(options.projectRoot);
|
|
218
|
+
const journalPath = confinedPath(root, JOURNAL_REL);
|
|
219
|
+
if (options.fragments.some((fragment) => fragment.scope !== 'project' || fragment.format !== 'json')) {
|
|
220
|
+
throw new IntegrationApplyError('INTENT_NOT_EXPRESSIBLE', 'initial transaction supports project JSON carriers only');
|
|
221
|
+
}
|
|
222
|
+
const groups = new Map<string, CarrierFragment[]>();
|
|
223
|
+
for (const fragment of options.fragments) {
|
|
224
|
+
const rows = groups.get(fragment.carrierPath) ?? [];
|
|
225
|
+
rows.push(fragment); groups.set(fragment.carrierPath, rows);
|
|
226
|
+
}
|
|
227
|
+
if (groups.size === 0) return { written: [], alreadyCurrent: [], journalPath: JOURNAL_REL };
|
|
228
|
+
|
|
229
|
+
return withNamedLockSync(root, 'integrations', () => {
|
|
230
|
+
const rootBefore = statSync(root);
|
|
231
|
+
const dzBefore = statSync(join(root, '.dz'));
|
|
232
|
+
const journalRead = readBoundedJson(journalPath, JOURNAL_REL);
|
|
233
|
+
const journal: JournalV1 = journalRead === undefined ? { version: 1, entries: Object.create(null) } : journalFrom(journalRead.value);
|
|
234
|
+
const candidates: { rel: string; abs: string; before: string | undefined; bytes: string; tuples: string[] }[] = [];
|
|
235
|
+
const pendingEntries: Record<string, JournalEntry> = { ...journal.entries };
|
|
236
|
+
|
|
237
|
+
for (const [carrierRel, fragments] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
238
|
+
const carrierAbs = confinedPath(root, carrierRel);
|
|
239
|
+
const existing = readBoundedJson(carrierAbs, carrierRel);
|
|
240
|
+
const rootObject = existing?.value ?? Object.create(null) as Record<string, unknown>;
|
|
241
|
+
const candidate = { ...rootObject } as Record<string, unknown>;
|
|
242
|
+
const tuples: string[] = [];
|
|
243
|
+
for (const fragment of fragments) {
|
|
244
|
+
const rootValue = candidate[fragment.rootKey];
|
|
245
|
+
const bucket = rootValue === undefined ? Object.create(null) as Record<string, unknown> : assertPlainObject(rootValue, `${carrierRel}.${fragment.rootKey}`);
|
|
246
|
+
const nextBucket = { ...bucket };
|
|
247
|
+
for (const [id, desired] of Object.entries(fragment.entries)) {
|
|
248
|
+
const tuple = `${carrierRel}::${fragment.rootKey}.${id}`;
|
|
249
|
+
const current = bucket[id];
|
|
250
|
+
const ledger = journal.entries[tuple];
|
|
251
|
+
const desiredHash = entryHash(desired);
|
|
252
|
+
if (current !== undefined) {
|
|
253
|
+
const currentHash = entryHash(current);
|
|
254
|
+
const recoverablePending = ledger?.state === 'pending' &&
|
|
255
|
+
currentHash === ledger.desiredContentHash &&
|
|
256
|
+
desiredHash === ledger.desiredContentHash;
|
|
257
|
+
const committedCurrent = ledger?.state === 'committed' && currentHash === ledger.desiredContentHash;
|
|
258
|
+
const legacy = isKnownAgentDbLegacy(`${fragment.rootKey}.${id}`, current, root);
|
|
259
|
+
if (!recoverablePending && !committedCurrent && !legacy) {
|
|
260
|
+
throw new IntegrationApplyError('OWNERSHIP_AMBIGUOUS', `${tuple} already exists without matching dz ownership; original bytes preserved`);
|
|
261
|
+
}
|
|
262
|
+
} else if (ledger?.state === 'pending') {
|
|
263
|
+
// Explicitly recoverable: pending publication may have stopped before carrier rename.
|
|
264
|
+
} else if (ledger?.state === 'committed') {
|
|
265
|
+
throw new IntegrationApplyError('OWNERSHIP_AMBIGUOUS', `${tuple} is absent but journal is committed; refuse implicit recreation`);
|
|
266
|
+
}
|
|
267
|
+
nextBucket[id] = desired;
|
|
268
|
+
tuples.push(tuple);
|
|
269
|
+
pendingEntries[tuple] = {
|
|
270
|
+
state: 'pending',
|
|
271
|
+
carrierPath: carrierRel,
|
|
272
|
+
entryId: `${fragment.rootKey}.${id}`,
|
|
273
|
+
previousCarrierDigest: absentOrDigest(existing?.bytes),
|
|
274
|
+
desiredCarrierDigest: '',
|
|
275
|
+
desiredContentHash: desiredHash,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
candidate[fragment.rootKey] = nextBucket;
|
|
279
|
+
}
|
|
280
|
+
const bytes = `${JSON.stringify(candidate, null, 2)}\n`;
|
|
281
|
+
const desiredCarrierDigest = sha(bytes);
|
|
282
|
+
for (const tuple of tuples) pendingEntries[tuple] = { ...pendingEntries[tuple]!, desiredCarrierDigest };
|
|
283
|
+
candidates.push({ rel: carrierRel, abs: carrierAbs, before: existing?.bytes, bytes, tuples });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (statSync(root).dev !== rootBefore.dev || statSync(root).ino !== rootBefore.ino ||
|
|
287
|
+
statSync(join(root, '.dz')).dev !== dzBefore.dev || statSync(join(root, '.dz')).ino !== dzBefore.ino) {
|
|
288
|
+
throw new IntegrationApplyError('CONCURRENT_MODIFICATION', 'project root or .dz inode changed during planning');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const pendingBytes = `${JSON.stringify({ version: 1, entries: pendingEntries }, null, 2)}\n`;
|
|
292
|
+
durableReplace(journalPath, pendingBytes);
|
|
293
|
+
if (options.injectFault === 'after-pending') throw new IntegrationApplyError('APPLY_FAILED', 'injected fault after pending journal publication');
|
|
294
|
+
|
|
295
|
+
const written: string[] = [];
|
|
296
|
+
const alreadyCurrent: string[] = [];
|
|
297
|
+
let carrierPublished = false;
|
|
298
|
+
try {
|
|
299
|
+
for (const candidate of candidates) {
|
|
300
|
+
const current = readBoundedJson(candidate.abs, candidate.rel)?.bytes;
|
|
301
|
+
if (current !== candidate.before) throw new IntegrationApplyError('CONCURRENT_MODIFICATION', `${candidate.rel} changed after planning`);
|
|
302
|
+
if (current === candidate.bytes) alreadyCurrent.push(candidate.rel);
|
|
303
|
+
else {
|
|
304
|
+
durableReplace(candidate.abs, candidate.bytes);
|
|
305
|
+
written.push(candidate.rel);
|
|
306
|
+
carrierPublished = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (options.injectFault === 'after-carrier') {
|
|
310
|
+
throw new IntegrationApplyError('APPLY_FAILED', 'carrier was written but committed ownership journal was not verified', true);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const committedEntries: Record<string, JournalEntry> = { ...pendingEntries };
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
for (const tuple of candidate.tuples) committedEntries[tuple] = { ...committedEntries[tuple]!, state: 'committed' };
|
|
316
|
+
}
|
|
317
|
+
durableReplace(journalPath, `${JSON.stringify({ version: 1, entries: committedEntries }, null, 2)}\n`);
|
|
318
|
+
if (options.injectFault === 'after-committed') throw new IntegrationApplyError('APPLY_FAILED', 'injected fault after committed journal publication', true);
|
|
319
|
+
return { written, alreadyCurrent, journalPath: JOURNAL_REL };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof IntegrationApplyError && error.applied) throw error;
|
|
322
|
+
if (carrierPublished) {
|
|
323
|
+
throw new IntegrationApplyError(
|
|
324
|
+
'APPLY_FAILED',
|
|
325
|
+
`carrier was written but committed ownership journal was not verified: ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
true,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|