open-multi-agent-kit 0.97.0 → 0.98.0
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/CHANGELOG.md +35 -0
- package/README.md +41 -9
- package/dist/cli/args.d.ts +2 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -189
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +194 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +2 -1
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/resource-paths.d.ts +4 -0
- package/dist/cli/resource-paths.d.ts.map +1 -0
- package/dist/cli/resource-paths.js +59 -0
- package/dist/cli/resource-paths.js.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.js +115 -0
- package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
- package/dist/commands/resource-doctor-cli.d.ts +4 -0
- package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
- package/dist/commands/resource-doctor-cli.js +35 -5
- package/dist/commands/resource-doctor-cli.js.map +1 -1
- package/dist/core/adaptorch-bridge.d.ts +11 -13
- package/dist/core/adaptorch-bridge.d.ts.map +1 -1
- package/dist/core/adaptorch-bridge.js +11 -13
- package/dist/core/adaptorch-bridge.js.map +1 -1
- package/dist/core/agent-session-snapshot.d.ts +16 -0
- package/dist/core/agent-session-snapshot.d.ts.map +1 -0
- package/dist/core/agent-session-snapshot.js +111 -0
- package/dist/core/agent-session-snapshot.js.map +1 -0
- package/dist/core/agent-session.d.ts +5 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +27 -219
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +10 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -1
- package/dist/core/agent-tool-settings.js +10 -1
- package/dist/core/agent-tool-settings.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +7 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +25 -61
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/index.d.ts +1 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +1 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
- package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage-format.js +71 -0
- package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
- package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
- package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage-types.js +2 -0
- package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
- package/dist/core/compaction/knowledge-triage.d.ts +29 -0
- package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage.js +189 -0
- package/dist/core/compaction/knowledge-triage.js.map +1 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +1 -1
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +4 -0
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +37 -0
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/completion-sound-io.d.ts +14 -0
- package/dist/core/completion-sound-io.d.ts.map +1 -0
- package/dist/core/completion-sound-io.js +64 -0
- package/dist/core/completion-sound-io.js.map +1 -0
- package/dist/core/completion-sound.d.ts +4 -10
- package/dist/core/completion-sound.d.ts.map +1 -1
- package/dist/core/completion-sound.js +28 -58
- package/dist/core/completion-sound.js.map +1 -1
- package/dist/core/context-budget-relevance.d.ts +15 -2
- package/dist/core/context-budget-relevance.d.ts.map +1 -1
- package/dist/core/context-budget-relevance.js +21 -9
- package/dist/core/context-budget-relevance.js.map +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/context-budget-system-prompt.d.ts +1 -1
- package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt.js.map +1 -1
- package/dist/core/context-file.d.ts +23 -0
- package/dist/core/context-file.d.ts.map +1 -0
- package/dist/core/context-file.js +17 -0
- package/dist/core/context-file.js.map +1 -0
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +6 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-prompt-policy.d.ts +1 -1
- package/dist/core/model-prompt-policy.d.ts.map +1 -1
- package/dist/core/model-prompt-policy.js.map +1 -1
- package/dist/core/prompt-settlement.d.ts +6 -3
- package/dist/core/prompt-settlement.d.ts.map +1 -1
- package/dist/core/prompt-settlement.js +10 -14
- package/dist/core/prompt-settlement.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +41 -1
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -1
- package/dist/core/reasoning-router-promotion.js +31 -2
- package/dist/core/reasoning-router-promotion.js.map +1 -1
- package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
- package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
- package/dist/core/reasoning-router-replay-stability.js +67 -0
- package/dist/core/reasoning-router-replay-stability.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +3 -3
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +6 -6
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +1 -1
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/resource-admission.d.ts +2 -1
- package/dist/core/resource-admission.d.ts.map +1 -1
- package/dist/core/resource-admission.js +11 -0
- package/dist/core/resource-admission.js.map +1 -1
- package/dist/core/resource-governor-format.d.ts +1 -1
- package/dist/core/resource-governor-format.d.ts.map +1 -1
- package/dist/core/resource-governor-format.js.map +1 -1
- package/dist/core/resource-governor-settings.d.ts +6 -12
- package/dist/core/resource-governor-settings.d.ts.map +1 -1
- package/dist/core/resource-governor-settings.js.map +1 -1
- package/dist/core/resource-governor-types.d.ts +24 -0
- package/dist/core/resource-governor-types.d.ts.map +1 -0
- package/dist/core/resource-governor-types.js +15 -0
- package/dist/core/resource-governor-types.js.map +1 -0
- package/dist/core/resource-loader.d.ts +1 -7
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/resource-observation-journal.d.ts +2 -1
- package/dist/core/resource-observation-journal.d.ts.map +1 -1
- package/dist/core/resource-observation-journal.js +4 -3
- package/dist/core/resource-observation-journal.js.map +1 -1
- package/dist/core/resource-observation-report-reader.d.ts +7 -0
- package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
- package/dist/core/resource-observation-report-reader.js +157 -0
- package/dist/core/resource-observation-report-reader.js.map +1 -0
- package/dist/core/resource-observation-report.d.ts +24 -0
- package/dist/core/resource-observation-report.d.ts.map +1 -0
- package/dist/core/resource-observation-report.js +101 -0
- package/dist/core/resource-observation-report.js.map +1 -0
- package/dist/core/router-feedback-collector.d.ts +1 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts +33 -0
- package/dist/core/session-failure-cause.d.ts.map +1 -0
- package/dist/core/session-failure-cause.js +141 -0
- package/dist/core/session-failure-cause.js.map +1 -0
- package/dist/core/session-system-prompt.d.ts +1 -1
- package/dist/core/session-system-prompt.d.ts.map +1 -1
- package/dist/core/session-system-prompt.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +7 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/workload-classifier.d.ts +2 -10
- package/dist/core/workload-classifier.d.ts.map +1 -1
- package/dist/core/workload-classifier.js +17 -0
- package/dist/core/workload-classifier.js.map +1 -1
- package/dist/core/workload-family-matcher.d.ts +1 -1
- package/dist/core/workload-family-matcher.d.ts.map +1 -1
- package/dist/core/workload-family-matcher.js.map +1 -1
- package/dist/core/workload-shard-execution-types.d.ts +40 -0
- package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
- package/dist/core/workload-shard-execution-types.js +14 -0
- package/dist/core/workload-shard-execution-types.js.map +1 -0
- package/dist/core/workload-shard-executor.d.ts +3 -27
- package/dist/core/workload-shard-executor.d.ts.map +1 -1
- package/dist/core/workload-shard-executor.js.map +1 -1
- package/dist/core/workload-shard-runner.d.ts +1 -1
- package/dist/core/workload-shard-runner.d.ts.map +1 -1
- package/dist/core/workload-shard-runner.js.map +1 -1
- package/dist/core/workload-types.d.ts +24 -0
- package/dist/core/workload-types.d.ts.map +1 -0
- package/dist/core/workload-types.js +15 -0
- package/dist/core/workload-types.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +27 -47
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +42 -33
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +14 -2
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/adaptorch-preview-spec.md +20 -18
- package/docs/adaptorch-preview.md +9 -9
- package/docs/compaction.md +24 -3
- package/docs/correctness-wall.md +10 -10
- package/docs/docs.json +12 -0
- package/docs/environment-variables.md +9 -2
- package/docs/index.md +3 -0
- package/docs/runtime-algorithms.md +359 -0
- package/docs/sdk.md +2 -2
- package/docs/settings.md +11 -10
- package/docs/usage.md +18 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
- package/dist/core/context-budget-governor.d.ts +0 -67
- package/dist/core/context-budget-governor.d.ts.map +0 -1
- package/dist/core/context-budget-governor.js +0 -224
- package/dist/core/context-budget-governor.js.map +0 -1
- package/dist/core/lean-ctx.d.ts +0 -99
- package/dist/core/lean-ctx.d.ts.map +0 -1
- package/dist/core/lean-ctx.js +0 -225
- package/dist/core/lean-ctx.js.map +0 -1
- package/dist/core/read-anchors.d.ts +0 -110
- package/dist/core/read-anchors.d.ts.map +0 -1
- package/dist/core/read-anchors.js +0 -193
- package/dist/core/read-anchors.js.map +0 -1
- package/dist/core/recovery-checkpoint.d.ts +0 -196
- package/dist/core/recovery-checkpoint.d.ts.map +0 -1
- package/dist/core/recovery-checkpoint.js +0 -382
- package/dist/core/recovery-checkpoint.js.map +0 -1
- package/dist/core/sandbox-policy-v2.d.ts +0 -49
- package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
- package/dist/core/sandbox-policy-v2.js +0 -368
- package/dist/core/sandbox-policy-v2.js.map +0 -1
- package/dist/guardrails/audit-logger.d.ts +0 -4
- package/dist/guardrails/audit-logger.d.ts.map +0 -1
- package/dist/guardrails/audit-logger.js +0 -28
- package/dist/guardrails/audit-logger.js.map +0 -1
- package/dist/guardrails/freedomd-adapter.d.ts +0 -16
- package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
- package/dist/guardrails/freedomd-adapter.js +0 -87
- package/dist/guardrails/freedomd-adapter.js.map +0 -1
- package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
- package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
- package/dist/orchestration/lane-grant-auditor.js +0 -87
- package/dist/orchestration/lane-grant-auditor.js.map +0 -1
- package/dist/types/guardrails.d.ts +0 -61
- package/dist/types/guardrails.d.ts.map +0 -1
- package/dist/types/guardrails.js +0 -9
- package/dist/types/guardrails.js.map +0 -1
- package/dist/types/lane-grant.d.ts +0 -58
- package/dist/types/lane-grant.d.ts.map +0 -1
- package/dist/types/lane-grant.js +0 -9
- package/dist/types/lane-grant.js.map +0 -1
package/dist/core/lean-ctx.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// OMK v6.2 — Lean CTX Engine (TypeScript)
|
|
2
|
-
// =========================================
|
|
3
|
-
// packages/coding-agent/src/core/lean-ctx.ts
|
|
4
|
-
//
|
|
5
|
-
// 매 프롬프트 입력 시 자동으로 context 분석/압축을 수행하는 엔진.
|
|
6
|
-
// 순수 로컬 모드 — API 호출 0회.
|
|
7
|
-
import { EventEmitter } from "events";
|
|
8
|
-
// ── 상수 및 설정 ────────────────────────────────────────────────────────
|
|
9
|
-
const DEFAULT_CONFIG = {
|
|
10
|
-
recencyWindow: 4,
|
|
11
|
-
relevanceThreshold: 0.3,
|
|
12
|
-
keyTurnReserve: 2,
|
|
13
|
-
summaryEvery: 3,
|
|
14
|
-
maxContextTokens: 2000,
|
|
15
|
-
compressionTarget: 0.6,
|
|
16
|
-
tfidfWeight: 0.5,
|
|
17
|
-
semanticWeight: 0.3,
|
|
18
|
-
recencyWeight: 0.2,
|
|
19
|
-
keywordBoost: 1.5,
|
|
20
|
-
lruWeight: 0.4,
|
|
21
|
-
frequencyWeight: 0.3,
|
|
22
|
-
recencyDecayWeight: 0.3,
|
|
23
|
-
protectedKeywords: ["refusal", "error", "strategy_change", "phase_transition"],
|
|
24
|
-
hardTokenLimit: 15000,
|
|
25
|
-
softTokenLimit: 12000,
|
|
26
|
-
warningThreshold: 0.8,
|
|
27
|
-
criticalThreshold: 0.95,
|
|
28
|
-
gracefulDegradation: true,
|
|
29
|
-
opportunisticExpansion: true,
|
|
30
|
-
autoCompress: true,
|
|
31
|
-
compressOnEveryPrompt: true,
|
|
32
|
-
minTokensToCompress: 500,
|
|
33
|
-
maxTurnsBeforeCompress: 8,
|
|
34
|
-
};
|
|
35
|
-
// ── ContextCompressor ───────────────────────────────────────────────────
|
|
36
|
-
export class ContextCompressor extends EventEmitter {
|
|
37
|
-
config;
|
|
38
|
-
charsPerToken = 4;
|
|
39
|
-
constructor(config) {
|
|
40
|
-
super();
|
|
41
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
42
|
-
}
|
|
43
|
-
compress(context, currentTurn) {
|
|
44
|
-
const originalTokens = this.estimateTokens(context);
|
|
45
|
-
if (originalTokens < this.config.minTokensToCompress) {
|
|
46
|
-
return {
|
|
47
|
-
turns: context,
|
|
48
|
-
summary: "",
|
|
49
|
-
originalTokens,
|
|
50
|
-
compressedTokens: originalTokens,
|
|
51
|
-
compressionRatio: 1.0,
|
|
52
|
-
techniqueUsed: "none",
|
|
53
|
-
keyTurnsPreserved: 0,
|
|
54
|
-
evictedCount: 0,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const keyTurns = this.extractKeyTurns(context);
|
|
58
|
-
const recent = this.applyRecencyWindow(context);
|
|
59
|
-
const relevant = this.filterByRelevance(context, currentTurn, keyTurns);
|
|
60
|
-
const summary = this.hierarchicalSummarize(context, recent, keyTurns, relevant);
|
|
61
|
-
const compressed = this.assembleCompressed(keyTurns, relevant, recent, summary);
|
|
62
|
-
const compressedTokens = this.estimateTokens(compressed);
|
|
63
|
-
const ratio = originalTokens > 0 ? compressedTokens / originalTokens : 1.0;
|
|
64
|
-
return {
|
|
65
|
-
turns: compressed,
|
|
66
|
-
summary,
|
|
67
|
-
originalTokens,
|
|
68
|
-
compressedTokens,
|
|
69
|
-
compressionRatio: ratio,
|
|
70
|
-
techniqueUsed: "hierarchical+selective",
|
|
71
|
-
keyTurnsPreserved: keyTurns.length,
|
|
72
|
-
evictedCount: context.length - compressed.length,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
estimateTokens(turns) {
|
|
76
|
-
return turns.reduce((sum, t) => sum + Math.ceil(t.content.length / this.charsPerToken), 0);
|
|
77
|
-
}
|
|
78
|
-
extractKeyTurns(context) {
|
|
79
|
-
const keyTurns = [];
|
|
80
|
-
const protectedKeywords = this.config.protectedKeywords;
|
|
81
|
-
for (const turn of context) {
|
|
82
|
-
const contentLower = turn.content.toLowerCase();
|
|
83
|
-
let isKey = protectedKeywords.some((kw) => contentLower.includes(kw.toLowerCase()));
|
|
84
|
-
if (turn.metadata?.isKeyTurn || turn.metadata?.phaseTransition) {
|
|
85
|
-
isKey = true;
|
|
86
|
-
}
|
|
87
|
-
if (turn.role === "assistant" &&
|
|
88
|
-
["refuse", "cannot", "unable", "sorry", "거부", "죄송"].some((w) => contentLower.includes(w))) {
|
|
89
|
-
isKey = true;
|
|
90
|
-
}
|
|
91
|
-
if (isKey) {
|
|
92
|
-
turn.isKeyTurn = true;
|
|
93
|
-
keyTurns.push(turn);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return keyTurns;
|
|
97
|
-
}
|
|
98
|
-
applyRecencyWindow(context) {
|
|
99
|
-
const windowSize = this.config.recencyWindow;
|
|
100
|
-
if (context.length <= windowSize)
|
|
101
|
-
return [...context];
|
|
102
|
-
return context.slice(-windowSize);
|
|
103
|
-
}
|
|
104
|
-
filterByRelevance(context, currentTurn, keyTurns) {
|
|
105
|
-
if (!currentTurn)
|
|
106
|
-
return [...context];
|
|
107
|
-
const threshold = this.config.relevanceThreshold;
|
|
108
|
-
const keyIndices = new Set(keyTurns?.map((t) => t.index) ?? []);
|
|
109
|
-
return context.filter((turn) => {
|
|
110
|
-
if (keyIndices.has(turn.index))
|
|
111
|
-
return true;
|
|
112
|
-
const score = this.calculateRelevance(turn, currentTurn);
|
|
113
|
-
return score >= threshold;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
calculateRelevance(turn, currentTurn) {
|
|
117
|
-
const tfidfScore = this.tfidfSimilarity(turn.content, currentTurn.content);
|
|
118
|
-
const recencyScore = Math.exp(-(Date.now() - turn.timestamp) / 3600000);
|
|
119
|
-
return tfidfScore * this.config.tfidfWeight + recencyScore * this.config.recencyWeight;
|
|
120
|
-
}
|
|
121
|
-
tfidfSimilarity(text1, text2) {
|
|
122
|
-
const words1 = text1.toLowerCase().split(/\s+/);
|
|
123
|
-
const words2 = text2.toLowerCase().split(/\s+/);
|
|
124
|
-
const set1 = new Set(words1);
|
|
125
|
-
const set2 = new Set(words2);
|
|
126
|
-
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
127
|
-
return intersection.size / Math.sqrt(set1.size * set2.size);
|
|
128
|
-
}
|
|
129
|
-
hierarchicalSummarize(_context, recent, keyTurns, relevant) {
|
|
130
|
-
const allImportant = [...new Set([...keyTurns, ...relevant, ...recent])].sort((a, b) => a.index - b.index);
|
|
131
|
-
const chunks = [];
|
|
132
|
-
for (let i = 0; i < allImportant.length; i += this.config.summaryEvery) {
|
|
133
|
-
const chunk = allImportant.slice(i, i + this.config.summaryEvery);
|
|
134
|
-
const summary = chunk.map((t) => `[${t.role}]: ${t.content.substring(0, 100)}`).join(" | ");
|
|
135
|
-
chunks.push(summary);
|
|
136
|
-
}
|
|
137
|
-
return chunks.join("\n");
|
|
138
|
-
}
|
|
139
|
-
assembleCompressed(keyTurns, relevant, recent, _summary) {
|
|
140
|
-
const important = new Map();
|
|
141
|
-
for (const turn of keyTurns)
|
|
142
|
-
important.set(turn.index, turn);
|
|
143
|
-
for (const turn of relevant)
|
|
144
|
-
important.set(turn.index, turn);
|
|
145
|
-
for (const turn of recent)
|
|
146
|
-
important.set(turn.index, turn);
|
|
147
|
-
return Array.from(important.values()).sort((a, b) => a.index - b.index);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// ── TokenBudgetEnforcer ─────────────────────────────────────────────────
|
|
151
|
-
export class TokenBudgetEnforcer extends EventEmitter {
|
|
152
|
-
config;
|
|
153
|
-
constructor(config) {
|
|
154
|
-
super();
|
|
155
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
156
|
-
}
|
|
157
|
-
check(usedTokens) {
|
|
158
|
-
const total = this.config.hardTokenLimit;
|
|
159
|
-
const ratio = usedTokens / total;
|
|
160
|
-
const remaining = total - usedTokens;
|
|
161
|
-
let level = "green";
|
|
162
|
-
if (ratio >= 1.0)
|
|
163
|
-
level = "exceeded";
|
|
164
|
-
else if (ratio >= this.config.criticalThreshold)
|
|
165
|
-
level = "red";
|
|
166
|
-
else if (ratio >= this.config.warningThreshold)
|
|
167
|
-
level = "orange";
|
|
168
|
-
else if (ratio >= 0.6)
|
|
169
|
-
level = "yellow";
|
|
170
|
-
return {
|
|
171
|
-
usedTokens,
|
|
172
|
-
totalTokens: total,
|
|
173
|
-
ratio,
|
|
174
|
-
level,
|
|
175
|
-
remaining,
|
|
176
|
-
canAddAgents: level === "green" || level === "yellow",
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
autoCompressIfNeeded(context) {
|
|
180
|
-
const tokens = context.reduce((sum, t) => sum + Math.ceil(t.content.length / 4), 0);
|
|
181
|
-
const status = this.check(tokens);
|
|
182
|
-
if (status.level === "red" || status.level === "exceeded") {
|
|
183
|
-
const compressor = new ContextCompressor(this.config);
|
|
184
|
-
const result = compressor.compress(context);
|
|
185
|
-
return { compressed: result.turns, status: this.check(result.compressedTokens) };
|
|
186
|
-
}
|
|
187
|
-
return { compressed: context, status };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// ── LeanCTXEngine (통합 엔진) ───────────────────────────────────────────
|
|
191
|
-
export class LeanCTXEngine extends EventEmitter {
|
|
192
|
-
compressor;
|
|
193
|
-
budgetEnforcer;
|
|
194
|
-
history = [];
|
|
195
|
-
constructor(config) {
|
|
196
|
-
super();
|
|
197
|
-
this.compressor = new ContextCompressor(config);
|
|
198
|
-
this.budgetEnforcer = new TokenBudgetEnforcer(config);
|
|
199
|
-
}
|
|
200
|
-
processPrompt(prompt, role = "user") {
|
|
201
|
-
const currentTurn = {
|
|
202
|
-
index: this.history.length,
|
|
203
|
-
role,
|
|
204
|
-
content: prompt,
|
|
205
|
-
timestamp: Date.now(),
|
|
206
|
-
};
|
|
207
|
-
const result = this.compressor.compress(this.history, currentTurn);
|
|
208
|
-
this.budgetEnforcer.autoCompressIfNeeded(result.turns);
|
|
209
|
-
this.history.push(currentTurn);
|
|
210
|
-
if (this.history.length > 100)
|
|
211
|
-
this.history = this.history.slice(-50);
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
preToolCall(_toolName, _toolArgs, context) {
|
|
215
|
-
return this.budgetEnforcer.autoCompressIfNeeded(context);
|
|
216
|
-
}
|
|
217
|
-
getStats() {
|
|
218
|
-
return { historyLength: this.history.length, lastCompression: Date.now() };
|
|
219
|
-
}
|
|
220
|
-
clearHistory() {
|
|
221
|
-
this.history = [];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
export default LeanCTXEngine;
|
|
225
|
-
//# sourceMappingURL=lean-ctx.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lean-ctx.js","sourceRoot":"","sources":["../../src/core/lean-ctx.ts"],"names":[],"mappings":"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","sourcesContent":["// OMK v6.2 — Lean CTX Engine (TypeScript)\n// =========================================\n// packages/coding-agent/src/core/lean-ctx.ts\n//\n// 매 프롬프트 입력 시 자동으로 context 분석/압축을 수행하는 엔진.\n// 순수 로컬 모드 — API 호출 0회.\n\nimport { EventEmitter } from \"events\";\n\n// ── 상수 및 설정 ────────────────────────────────────────────────────────\n\nconst DEFAULT_CONFIG = {\n\trecencyWindow: 4,\n\trelevanceThreshold: 0.3,\n\tkeyTurnReserve: 2,\n\tsummaryEvery: 3,\n\tmaxContextTokens: 2000,\n\tcompressionTarget: 0.6,\n\n\ttfidfWeight: 0.5,\n\tsemanticWeight: 0.3,\n\trecencyWeight: 0.2,\n\tkeywordBoost: 1.5,\n\n\tlruWeight: 0.4,\n\tfrequencyWeight: 0.3,\n\trecencyDecayWeight: 0.3,\n\tprotectedKeywords: [\"refusal\", \"error\", \"strategy_change\", \"phase_transition\"],\n\n\thardTokenLimit: 15000,\n\tsoftTokenLimit: 12000,\n\twarningThreshold: 0.8,\n\tcriticalThreshold: 0.95,\n\tgracefulDegradation: true,\n\topportunisticExpansion: true,\n\n\tautoCompress: true,\n\tcompressOnEveryPrompt: true,\n\tminTokensToCompress: 500,\n\tmaxTurnsBeforeCompress: 8,\n};\n\n// ── 데이터 모델 ─────────────────────────────────────────────────────────\n\nexport interface TurnEntry {\n\tindex: number;\n\trole: \"user\" | \"assistant\" | \"system\" | \"tool\";\n\tcontent: string;\n\ttimestamp: number;\n\tmetadata?: Record<string, any>;\n\ttokenCount?: number;\n\tisKeyTurn?: boolean;\n\trelevanceScore?: number;\n\taccessCount?: number;\n\tlastAccessed?: number;\n}\n\nexport interface CompressedContext {\n\tturns: TurnEntry[];\n\tsummary: string;\n\toriginalTokens: number;\n\tcompressedTokens: number;\n\tcompressionRatio: number;\n\ttechniqueUsed: string;\n\tkeyTurnsPreserved: number;\n\tevictedCount: number;\n\tmetadata?: Record<string, any>;\n}\n\nexport interface BudgetStatus {\n\tusedTokens: number;\n\ttotalTokens: number;\n\tratio: number;\n\tlevel: \"green\" | \"yellow\" | \"orange\" | \"red\" | \"exceeded\";\n\tremaining: number;\n\tcanAddAgents: boolean;\n}\n\n// ── ContextCompressor ───────────────────────────────────────────────────\n\nexport class ContextCompressor extends EventEmitter {\n\tprivate config: typeof DEFAULT_CONFIG;\n\tprivate charsPerToken = 4;\n\n\tconstructor(config?: Partial<typeof DEFAULT_CONFIG>) {\n\t\tsuper();\n\t\tthis.config = { ...DEFAULT_CONFIG, ...config };\n\t}\n\n\tcompress(context: TurnEntry[], currentTurn?: TurnEntry): CompressedContext {\n\t\tconst originalTokens = this.estimateTokens(context);\n\n\t\tif (originalTokens < this.config.minTokensToCompress) {\n\t\t\treturn {\n\t\t\t\tturns: context,\n\t\t\t\tsummary: \"\",\n\t\t\t\toriginalTokens,\n\t\t\t\tcompressedTokens: originalTokens,\n\t\t\t\tcompressionRatio: 1.0,\n\t\t\t\ttechniqueUsed: \"none\",\n\t\t\t\tkeyTurnsPreserved: 0,\n\t\t\t\tevictedCount: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst keyTurns = this.extractKeyTurns(context);\n\t\tconst recent = this.applyRecencyWindow(context);\n\t\tconst relevant = this.filterByRelevance(context, currentTurn, keyTurns);\n\t\tconst summary = this.hierarchicalSummarize(context, recent, keyTurns, relevant);\n\t\tconst compressed = this.assembleCompressed(keyTurns, relevant, recent, summary);\n\n\t\tconst compressedTokens = this.estimateTokens(compressed);\n\t\tconst ratio = originalTokens > 0 ? compressedTokens / originalTokens : 1.0;\n\n\t\treturn {\n\t\t\tturns: compressed,\n\t\t\tsummary,\n\t\t\toriginalTokens,\n\t\t\tcompressedTokens,\n\t\t\tcompressionRatio: ratio,\n\t\t\ttechniqueUsed: \"hierarchical+selective\",\n\t\t\tkeyTurnsPreserved: keyTurns.length,\n\t\t\tevictedCount: context.length - compressed.length,\n\t\t};\n\t}\n\n\tprivate estimateTokens(turns: TurnEntry[]): number {\n\t\treturn turns.reduce((sum, t) => sum + Math.ceil(t.content.length / this.charsPerToken), 0);\n\t}\n\n\tprivate extractKeyTurns(context: TurnEntry[]): TurnEntry[] {\n\t\tconst keyTurns: TurnEntry[] = [];\n\t\tconst protectedKeywords = this.config.protectedKeywords;\n\n\t\tfor (const turn of context) {\n\t\t\tconst contentLower = turn.content.toLowerCase();\n\t\t\tlet isKey = protectedKeywords.some((kw) => contentLower.includes(kw.toLowerCase()));\n\n\t\t\tif (turn.metadata?.isKeyTurn || turn.metadata?.phaseTransition) {\n\t\t\t\tisKey = true;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tturn.role === \"assistant\" &&\n\t\t\t\t[\"refuse\", \"cannot\", \"unable\", \"sorry\", \"거부\", \"죄송\"].some((w) => contentLower.includes(w))\n\t\t\t) {\n\t\t\t\tisKey = true;\n\t\t\t}\n\n\t\t\tif (isKey) {\n\t\t\t\tturn.isKeyTurn = true;\n\t\t\t\tkeyTurns.push(turn);\n\t\t\t}\n\t\t}\n\n\t\treturn keyTurns;\n\t}\n\n\tprivate applyRecencyWindow(context: TurnEntry[]): TurnEntry[] {\n\t\tconst windowSize = this.config.recencyWindow;\n\t\tif (context.length <= windowSize) return [...context];\n\t\treturn context.slice(-windowSize);\n\t}\n\n\tprivate filterByRelevance(context: TurnEntry[], currentTurn?: TurnEntry, keyTurns?: TurnEntry[]): TurnEntry[] {\n\t\tif (!currentTurn) return [...context];\n\n\t\tconst threshold = this.config.relevanceThreshold;\n\t\tconst keyIndices = new Set(keyTurns?.map((t) => t.index) ?? []);\n\n\t\treturn context.filter((turn) => {\n\t\t\tif (keyIndices.has(turn.index)) return true;\n\t\t\tconst score = this.calculateRelevance(turn, currentTurn);\n\t\t\treturn score >= threshold;\n\t\t});\n\t}\n\n\tprivate calculateRelevance(turn: TurnEntry, currentTurn: TurnEntry): number {\n\t\tconst tfidfScore = this.tfidfSimilarity(turn.content, currentTurn.content);\n\t\tconst recencyScore = Math.exp(-(Date.now() - turn.timestamp) / 3600000);\n\t\treturn tfidfScore * this.config.tfidfWeight + recencyScore * this.config.recencyWeight;\n\t}\n\n\tprivate tfidfSimilarity(text1: string, text2: string): number {\n\t\tconst words1 = text1.toLowerCase().split(/\\s+/);\n\t\tconst words2 = text2.toLowerCase().split(/\\s+/);\n\t\tconst set1 = new Set(words1);\n\t\tconst set2 = new Set(words2);\n\t\tconst intersection = new Set([...set1].filter((x) => set2.has(x)));\n\t\treturn intersection.size / Math.sqrt(set1.size * set2.size);\n\t}\n\n\tprivate hierarchicalSummarize(\n\t\t_context: TurnEntry[],\n\t\trecent: TurnEntry[],\n\t\tkeyTurns: TurnEntry[],\n\t\trelevant: TurnEntry[],\n\t): string {\n\t\tconst allImportant = [...new Set([...keyTurns, ...relevant, ...recent])].sort((a, b) => a.index - b.index);\n\t\tconst chunks: string[] = [];\n\n\t\tfor (let i = 0; i < allImportant.length; i += this.config.summaryEvery) {\n\t\t\tconst chunk = allImportant.slice(i, i + this.config.summaryEvery);\n\t\t\tconst summary = chunk.map((t) => `[${t.role}]: ${t.content.substring(0, 100)}`).join(\" | \");\n\t\t\tchunks.push(summary);\n\t\t}\n\n\t\treturn chunks.join(\"\\n\");\n\t}\n\n\tprivate assembleCompressed(\n\t\tkeyTurns: TurnEntry[],\n\t\trelevant: TurnEntry[],\n\t\trecent: TurnEntry[],\n\t\t_summary: string,\n\t): TurnEntry[] {\n\t\tconst important = new Map<number, TurnEntry>();\n\n\t\tfor (const turn of keyTurns) important.set(turn.index, turn);\n\t\tfor (const turn of relevant) important.set(turn.index, turn);\n\t\tfor (const turn of recent) important.set(turn.index, turn);\n\n\t\treturn Array.from(important.values()).sort((a, b) => a.index - b.index);\n\t}\n}\n\n// ── TokenBudgetEnforcer ─────────────────────────────────────────────────\n\nexport class TokenBudgetEnforcer extends EventEmitter {\n\tprivate config: typeof DEFAULT_CONFIG;\n\n\tconstructor(config?: Partial<typeof DEFAULT_CONFIG>) {\n\t\tsuper();\n\t\tthis.config = { ...DEFAULT_CONFIG, ...config };\n\t}\n\n\tcheck(usedTokens: number): BudgetStatus {\n\t\tconst total = this.config.hardTokenLimit;\n\t\tconst ratio = usedTokens / total;\n\t\tconst remaining = total - usedTokens;\n\n\t\tlet level: BudgetStatus[\"level\"] = \"green\";\n\t\tif (ratio >= 1.0) level = \"exceeded\";\n\t\telse if (ratio >= this.config.criticalThreshold) level = \"red\";\n\t\telse if (ratio >= this.config.warningThreshold) level = \"orange\";\n\t\telse if (ratio >= 0.6) level = \"yellow\";\n\n\t\treturn {\n\t\t\tusedTokens,\n\t\t\ttotalTokens: total,\n\t\t\tratio,\n\t\t\tlevel,\n\t\t\tremaining,\n\t\t\tcanAddAgents: level === \"green\" || level === \"yellow\",\n\t\t};\n\t}\n\n\tautoCompressIfNeeded(context: TurnEntry[]): { compressed: TurnEntry[]; status: BudgetStatus } {\n\t\tconst tokens = context.reduce((sum, t) => sum + Math.ceil(t.content.length / 4), 0);\n\t\tconst status = this.check(tokens);\n\n\t\tif (status.level === \"red\" || status.level === \"exceeded\") {\n\t\t\tconst compressor = new ContextCompressor(this.config);\n\t\t\tconst result = compressor.compress(context);\n\t\t\treturn { compressed: result.turns, status: this.check(result.compressedTokens) };\n\t\t}\n\n\t\treturn { compressed: context, status };\n\t}\n}\n\n// ── LeanCTXEngine (통합 엔진) ───────────────────────────────────────────\n\nexport class LeanCTXEngine extends EventEmitter {\n\tprivate compressor: ContextCompressor;\n\tprivate budgetEnforcer: TokenBudgetEnforcer;\n\tprivate history: TurnEntry[] = [];\n\n\tconstructor(config?: Partial<typeof DEFAULT_CONFIG>) {\n\t\tsuper();\n\t\tthis.compressor = new ContextCompressor(config);\n\t\tthis.budgetEnforcer = new TokenBudgetEnforcer(config);\n\t}\n\n\tprocessPrompt(prompt: string, role: \"user\" | \"assistant\" | \"system\" = \"user\"): CompressedContext {\n\t\tconst currentTurn: TurnEntry = {\n\t\t\tindex: this.history.length,\n\t\t\trole,\n\t\t\tcontent: prompt,\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\tconst result = this.compressor.compress(this.history, currentTurn);\n\t\tthis.budgetEnforcer.autoCompressIfNeeded(result.turns);\n\n\t\tthis.history.push(currentTurn);\n\t\tif (this.history.length > 100) this.history = this.history.slice(-50);\n\n\t\treturn result;\n\t}\n\n\tpreToolCall(\n\t\t_toolName: string,\n\t\t_toolArgs: Record<string, any>,\n\t\tcontext: TurnEntry[],\n\t): { compressed: TurnEntry[]; status: BudgetStatus } {\n\t\treturn this.budgetEnforcer.autoCompressIfNeeded(context);\n\t}\n\n\tgetStats(): { historyLength: number; lastCompression: number } {\n\t\treturn { historyLength: this.history.length, lastCompression: Date.now() };\n\t}\n\n\tclearHistory(): void {\n\t\tthis.history = [];\n\t}\n}\n\nexport default LeanCTXEngine;\n"]}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `omk.read-anchor` — pure hash-anchored read/edit primitives.
|
|
3
|
-
*
|
|
4
|
-
* A read anchor binds a read result to the exact bytes the model saw: the
|
|
5
|
-
* newline-normalized SHA-256 of the whole file plus the SHA-256 of the specific
|
|
6
|
-
* line block that was read. Edits can then carry the anchor so the core can
|
|
7
|
-
* reject stale edits (the file changed under the model) before applying them.
|
|
8
|
-
*
|
|
9
|
-
* All hashing in this module is performed on newline-normalized (LF) UTF-8
|
|
10
|
-
* content so anchors are robust to CRLF/CR differences, matching the edit
|
|
11
|
-
* pipeline which normalizes to LF before matching.
|
|
12
|
-
*
|
|
13
|
-
* Side-effect free: no filesystem access; `node:crypto` only.
|
|
14
|
-
*
|
|
15
|
-
* Derived clean-room from the lane C plan
|
|
16
|
-
* (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`); no legacy
|
|
17
|
-
* package source is imported or copied.
|
|
18
|
-
*/
|
|
19
|
-
export declare const READ_ANCHOR_SCHEMA_VERSION = "omk.read-anchor.v1";
|
|
20
|
-
export interface ReadAnchorRange {
|
|
21
|
-
/** 1-indexed start line (resolved). */
|
|
22
|
-
readonly offset: number;
|
|
23
|
-
/** Number of lines included in the block (resolved). */
|
|
24
|
-
readonly limit: number;
|
|
25
|
-
/** 1-indexed last line included, inclusive. Equals `offset - 1` for an empty block. */
|
|
26
|
-
readonly endLine: number;
|
|
27
|
-
}
|
|
28
|
-
export interface ReadAnchor {
|
|
29
|
-
readonly schemaVersion: typeof READ_ANCHOR_SCHEMA_VERSION;
|
|
30
|
-
readonly readId: string;
|
|
31
|
-
readonly path: string;
|
|
32
|
-
/** SHA-256 (hex) of the newline-normalized full file content. */
|
|
33
|
-
readonly fileSha256: string;
|
|
34
|
-
readonly range: ReadAnchorRange;
|
|
35
|
-
/** SHA-256 (hex) of the newline-normalized read block content. */
|
|
36
|
-
readonly blockSha256: string;
|
|
37
|
-
}
|
|
38
|
-
export interface CreateReadAnchorInput {
|
|
39
|
-
readonly path: string;
|
|
40
|
-
/** Full file content as read. */
|
|
41
|
-
readonly content: string;
|
|
42
|
-
/** 1-indexed start line; defaults to 1 (whole file). */
|
|
43
|
-
readonly offset?: number;
|
|
44
|
-
/** Maximum number of lines; defaults to the rest of the file. */
|
|
45
|
-
readonly limit?: number;
|
|
46
|
-
/** Optional deterministic id (otherwise a UUID is generated). */
|
|
47
|
-
readonly readId?: string;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Create a hash anchor for a read. Hashes both the full (normalized) file and
|
|
51
|
-
* the specific line block so a later edit can prove it is operating on the same
|
|
52
|
-
* bytes the model saw.
|
|
53
|
-
*/
|
|
54
|
-
export declare function createReadAnchor(input: CreateReadAnchorInput): ReadAnchor;
|
|
55
|
-
export interface ReadAnchorVerification {
|
|
56
|
-
readonly ok: boolean;
|
|
57
|
-
readonly fileMatches: boolean;
|
|
58
|
-
readonly blockMatches: boolean;
|
|
59
|
-
readonly currentFileSha256: string;
|
|
60
|
-
readonly currentBlockSha256: string;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Verify an anchor against the current file content. `fileMatches` is the strict
|
|
64
|
-
* whole-file check; `blockMatches` re-extracts the anchored range and compares
|
|
65
|
-
* the block hash (useful for lenient relocation diagnostics).
|
|
66
|
-
*/
|
|
67
|
-
export declare function verifyReadAnchor(anchor: ReadAnchor, currentContent: string): ReadAnchorVerification;
|
|
68
|
-
/** Validate an untrusted value as a {@link ReadAnchor}. Pure and fail-closed. */
|
|
69
|
-
export declare function validateReadAnchor(value: unknown): {
|
|
70
|
-
ok: boolean;
|
|
71
|
-
errors: readonly string[];
|
|
72
|
-
anchor?: ReadAnchor;
|
|
73
|
-
};
|
|
74
|
-
export type AnchoredEditMode = "strict" | "lenient";
|
|
75
|
-
export type AnchoredEditVerdict = "allow" | "allow-lenient" | "reject-stale";
|
|
76
|
-
export interface AnchoredEditDecisionInput {
|
|
77
|
-
readonly anchor: ReadAnchor;
|
|
78
|
-
/** Current full file content at edit time. */
|
|
79
|
-
readonly currentContent: string;
|
|
80
|
-
/** Defaults to `strict`. */
|
|
81
|
-
readonly mode?: AnchoredEditMode;
|
|
82
|
-
/**
|
|
83
|
-
* The exact anchored block text (typically the edit's `oldText`). Required
|
|
84
|
-
* for lenient relocation when the file has changed; must hash to
|
|
85
|
-
* `anchor.blockSha256` to be trusted.
|
|
86
|
-
*/
|
|
87
|
-
readonly anchoredBlockText?: string;
|
|
88
|
-
}
|
|
89
|
-
export interface AnchoredEditDecision {
|
|
90
|
-
readonly verdict: AnchoredEditVerdict;
|
|
91
|
-
readonly reason: string;
|
|
92
|
-
readonly currentFileSha256: string;
|
|
93
|
-
readonly fileChanged: boolean;
|
|
94
|
-
/** Lenient mode only: whether the anchored block was uniquely relocated. */
|
|
95
|
-
readonly blockRelocated?: boolean;
|
|
96
|
-
/** Lenient mode only: occurrences of the anchored block in current content. */
|
|
97
|
-
readonly occurrences?: number;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Decide whether an anchored edit may proceed.
|
|
101
|
-
*
|
|
102
|
-
* - Unchanged file -> `allow` (the common fast path).
|
|
103
|
-
* - Changed file, strict mode -> `reject-stale` (the default; forces a re-read).
|
|
104
|
-
* - Changed file, lenient mode -> `allow-lenient` only when the anchored block
|
|
105
|
-
* text is supplied, hashes to the recorded `blockSha256`, and still occurs
|
|
106
|
-
* exactly once in the current content (unique, non-overlapping relocation).
|
|
107
|
-
* Zero or multiple occurrences -> `reject-stale`.
|
|
108
|
-
*/
|
|
109
|
-
export declare function decideAnchoredEdit(input: AnchoredEditDecisionInput): AnchoredEditDecision;
|
|
110
|
-
//# sourceMappingURL=read-anchors.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-anchors.d.ts","sourceRoot":"","sources":["../../src/core/read-anchors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,0BAA0B,CAAC;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAqCD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,UAAU,CAWzE;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,GAAG,sBAAsB,CAcnG;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAE,CA+BlH;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpD,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,eAAe,GAAG,cAAc,CAAC;AAE7E,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAeD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,yBAAyB,GAAG,oBAAoB,CAwDzF","sourcesContent":["/**\n * `omk.read-anchor` — pure hash-anchored read/edit primitives.\n *\n * A read anchor binds a read result to the exact bytes the model saw: the\n * newline-normalized SHA-256 of the whole file plus the SHA-256 of the specific\n * line block that was read. Edits can then carry the anchor so the core can\n * reject stale edits (the file changed under the model) before applying them.\n *\n * All hashing in this module is performed on newline-normalized (LF) UTF-8\n * content so anchors are robust to CRLF/CR differences, matching the edit\n * pipeline which normalizes to LF before matching.\n *\n * Side-effect free: no filesystem access; `node:crypto` only.\n *\n * Derived clean-room from the lane C plan\n * (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`); no legacy\n * package source is imported or copied.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport const READ_ANCHOR_SCHEMA_VERSION = \"omk.read-anchor.v1\";\n\nexport interface ReadAnchorRange {\n\t/** 1-indexed start line (resolved). */\n\treadonly offset: number;\n\t/** Number of lines included in the block (resolved). */\n\treadonly limit: number;\n\t/** 1-indexed last line included, inclusive. Equals `offset - 1` for an empty block. */\n\treadonly endLine: number;\n}\n\nexport interface ReadAnchor {\n\treadonly schemaVersion: typeof READ_ANCHOR_SCHEMA_VERSION;\n\treadonly readId: string;\n\treadonly path: string;\n\t/** SHA-256 (hex) of the newline-normalized full file content. */\n\treadonly fileSha256: string;\n\treadonly range: ReadAnchorRange;\n\t/** SHA-256 (hex) of the newline-normalized read block content. */\n\treadonly blockSha256: string;\n}\n\nconst HEX64 = /^[0-9a-f]{64}$/i;\n\nfunction normalizeNewlines(content: string): string {\n\treturn content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nfunction sha256Hex(content: string): string {\n\treturn createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\");\n}\n\ninterface ExtractedBlock {\n\treadonly text: string;\n\treadonly range: ReadAnchorRange;\n}\n\n/**\n * Extract a line block from already newline-normalized content using read-tool\n * semantics: `offset` is a 1-indexed start line, `limit` is the maximum number\n * of lines. Out-of-range requests clamp to an empty block at the end of file.\n */\nfunction extractBlock(normalized: string, offset: number | undefined, limit: number | undefined): ExtractedBlock {\n\tconst lines = normalized.split(\"\\n\");\n\tconst start = Math.max(1, Math.floor(offset ?? 1));\n\tif (start > lines.length) {\n\t\treturn { text: \"\", range: { offset: start, limit: 0, endLine: start - 1 } };\n\t}\n\tconst available = lines.length - (start - 1);\n\tconst count = limit === undefined ? available : Math.max(0, Math.min(Math.floor(limit), available));\n\tconst blockLines = lines.slice(start - 1, start - 1 + count);\n\treturn {\n\t\ttext: blockLines.join(\"\\n\"),\n\t\trange: { offset: start, limit: count, endLine: start - 1 + count },\n\t};\n}\n\nexport interface CreateReadAnchorInput {\n\treadonly path: string;\n\t/** Full file content as read. */\n\treadonly content: string;\n\t/** 1-indexed start line; defaults to 1 (whole file). */\n\treadonly offset?: number;\n\t/** Maximum number of lines; defaults to the rest of the file. */\n\treadonly limit?: number;\n\t/** Optional deterministic id (otherwise a UUID is generated). */\n\treadonly readId?: string;\n}\n\n/**\n * Create a hash anchor for a read. Hashes both the full (normalized) file and\n * the specific line block so a later edit can prove it is operating on the same\n * bytes the model saw.\n */\nexport function createReadAnchor(input: CreateReadAnchorInput): ReadAnchor {\n\tconst normalized = normalizeNewlines(input.content);\n\tconst block = extractBlock(normalized, input.offset, input.limit);\n\treturn {\n\t\tschemaVersion: READ_ANCHOR_SCHEMA_VERSION,\n\t\treadId: input.readId ?? randomUUID(),\n\t\tpath: input.path,\n\t\tfileSha256: sha256Hex(normalized),\n\t\trange: block.range,\n\t\tblockSha256: sha256Hex(block.text),\n\t};\n}\n\nexport interface ReadAnchorVerification {\n\treadonly ok: boolean;\n\treadonly fileMatches: boolean;\n\treadonly blockMatches: boolean;\n\treadonly currentFileSha256: string;\n\treadonly currentBlockSha256: string;\n}\n\n/**\n * Verify an anchor against the current file content. `fileMatches` is the strict\n * whole-file check; `blockMatches` re-extracts the anchored range and compares\n * the block hash (useful for lenient relocation diagnostics).\n */\nexport function verifyReadAnchor(anchor: ReadAnchor, currentContent: string): ReadAnchorVerification {\n\tconst normalized = normalizeNewlines(currentContent);\n\tconst currentFileSha256 = sha256Hex(normalized);\n\tconst block = extractBlock(normalized, anchor.range.offset, anchor.range.limit);\n\tconst currentBlockSha256 = sha256Hex(block.text);\n\tconst fileMatches = currentFileSha256 === anchor.fileSha256;\n\tconst blockMatches = currentBlockSha256 === anchor.blockSha256;\n\treturn {\n\t\tok: fileMatches && blockMatches,\n\t\tfileMatches,\n\t\tblockMatches,\n\t\tcurrentFileSha256,\n\t\tcurrentBlockSha256,\n\t};\n}\n\n/** Validate an untrusted value as a {@link ReadAnchor}. Pure and fail-closed. */\nexport function validateReadAnchor(value: unknown): { ok: boolean; errors: readonly string[]; anchor?: ReadAnchor } {\n\tconst errors: string[] = [];\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn { ok: false, errors: [\"anchor must be an object\"] };\n\t}\n\tconst anchor = value as Record<string, unknown>;\n\tif (anchor.schemaVersion !== READ_ANCHOR_SCHEMA_VERSION) {\n\t\terrors.push(`schemaVersion must be ${READ_ANCHOR_SCHEMA_VERSION}`);\n\t}\n\tif (typeof anchor.readId !== \"string\" || anchor.readId.length === 0)\n\t\terrors.push(\"readId must be a non-empty string\");\n\tif (typeof anchor.path !== \"string\" || anchor.path.length === 0) errors.push(\"path must be a non-empty string\");\n\tif (typeof anchor.fileSha256 !== \"string\" || !HEX64.test(anchor.fileSha256)) {\n\t\terrors.push(\"fileSha256 must be a 64-char hex digest\");\n\t}\n\tif (typeof anchor.blockSha256 !== \"string\" || !HEX64.test(anchor.blockSha256)) {\n\t\terrors.push(\"blockSha256 must be a 64-char hex digest\");\n\t}\n\tconst range = anchor.range;\n\tif (typeof range !== \"object\" || range === null || Array.isArray(range)) {\n\t\terrors.push(\"range must be an object\");\n\t} else {\n\t\tconst r = range as Record<string, unknown>;\n\t\tfor (const key of [\"offset\", \"limit\", \"endLine\"]) {\n\t\t\tif (typeof r[key] !== \"number\" || !Number.isInteger(r[key]) || (r[key] as number) < 0) {\n\t\t\t\terrors.push(`range.${key} must be a non-negative integer`);\n\t\t\t}\n\t\t}\n\t}\n\tif (errors.length > 0) return { ok: false, errors };\n\treturn { ok: true, errors: [], anchor: value as unknown as ReadAnchor };\n}\n\nexport type AnchoredEditMode = \"strict\" | \"lenient\";\n\nexport type AnchoredEditVerdict = \"allow\" | \"allow-lenient\" | \"reject-stale\";\n\nexport interface AnchoredEditDecisionInput {\n\treadonly anchor: ReadAnchor;\n\t/** Current full file content at edit time. */\n\treadonly currentContent: string;\n\t/** Defaults to `strict`. */\n\treadonly mode?: AnchoredEditMode;\n\t/**\n\t * The exact anchored block text (typically the edit's `oldText`). Required\n\t * for lenient relocation when the file has changed; must hash to\n\t * `anchor.blockSha256` to be trusted.\n\t */\n\treadonly anchoredBlockText?: string;\n}\n\nexport interface AnchoredEditDecision {\n\treadonly verdict: AnchoredEditVerdict;\n\treadonly reason: string;\n\treadonly currentFileSha256: string;\n\treadonly fileChanged: boolean;\n\t/** Lenient mode only: whether the anchored block was uniquely relocated. */\n\treadonly blockRelocated?: boolean;\n\t/** Lenient mode only: occurrences of the anchored block in current content. */\n\treadonly occurrences?: number;\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n\tif (needle.length === 0) return 0;\n\tlet count = 0;\n\tlet index = haystack.indexOf(needle);\n\twhile (index !== -1) {\n\t\tcount += 1;\n\t\tindex = haystack.indexOf(needle, index + needle.length);\n\t}\n\treturn count;\n}\n\nconst STALE_REREAD_HINT = \"stale read: the file changed since it was read; re-read the file before editing\";\n\n/**\n * Decide whether an anchored edit may proceed.\n *\n * - Unchanged file -> `allow` (the common fast path).\n * - Changed file, strict mode -> `reject-stale` (the default; forces a re-read).\n * - Changed file, lenient mode -> `allow-lenient` only when the anchored block\n * text is supplied, hashes to the recorded `blockSha256`, and still occurs\n * exactly once in the current content (unique, non-overlapping relocation).\n * Zero or multiple occurrences -> `reject-stale`.\n */\nexport function decideAnchoredEdit(input: AnchoredEditDecisionInput): AnchoredEditDecision {\n\tconst mode = input.mode ?? \"strict\";\n\tconst normalized = normalizeNewlines(input.currentContent);\n\tconst currentFileSha256 = sha256Hex(normalized);\n\tconst fileChanged = currentFileSha256 !== input.anchor.fileSha256;\n\n\tif (!fileChanged) {\n\t\treturn { verdict: \"allow\", reason: \"current file hash matches the anchor\", currentFileSha256, fileChanged };\n\t}\n\n\tif (mode === \"strict\") {\n\t\treturn { verdict: \"reject-stale\", reason: STALE_REREAD_HINT, currentFileSha256, fileChanged };\n\t}\n\n\tconst blockText = input.anchoredBlockText;\n\tif (blockText === undefined || blockText.length === 0) {\n\t\treturn {\n\t\t\tverdict: \"reject-stale\",\n\t\t\treason: \"lenient relocation requires the anchored block text\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t};\n\t}\n\n\tconst normalizedBlock = normalizeNewlines(blockText);\n\tif (sha256Hex(normalizedBlock) !== input.anchor.blockSha256) {\n\t\treturn {\n\t\t\tverdict: \"reject-stale\",\n\t\t\treason: \"supplied block text does not match the anchored block hash\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t};\n\t}\n\n\tconst occurrences = countOccurrences(normalized, normalizedBlock);\n\tif (occurrences === 1) {\n\t\treturn {\n\t\t\tverdict: \"allow-lenient\",\n\t\t\treason: \"file changed but the anchored block is still uniquely locatable\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t\tblockRelocated: true,\n\t\t\toccurrences,\n\t\t};\n\t}\n\treturn {\n\t\tverdict: \"reject-stale\",\n\t\treason:\n\t\t\toccurrences === 0\n\t\t\t\t? \"anchored block no longer exists in the current file\"\n\t\t\t\t: \"anchored block is ambiguous (multiple matches) in the current file\",\n\t\tcurrentFileSha256,\n\t\tfileChanged,\n\t\tblockRelocated: false,\n\t\toccurrences,\n\t};\n}\n"]}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `omk.read-anchor` — pure hash-anchored read/edit primitives.
|
|
3
|
-
*
|
|
4
|
-
* A read anchor binds a read result to the exact bytes the model saw: the
|
|
5
|
-
* newline-normalized SHA-256 of the whole file plus the SHA-256 of the specific
|
|
6
|
-
* line block that was read. Edits can then carry the anchor so the core can
|
|
7
|
-
* reject stale edits (the file changed under the model) before applying them.
|
|
8
|
-
*
|
|
9
|
-
* All hashing in this module is performed on newline-normalized (LF) UTF-8
|
|
10
|
-
* content so anchors are robust to CRLF/CR differences, matching the edit
|
|
11
|
-
* pipeline which normalizes to LF before matching.
|
|
12
|
-
*
|
|
13
|
-
* Side-effect free: no filesystem access; `node:crypto` only.
|
|
14
|
-
*
|
|
15
|
-
* Derived clean-room from the lane C plan
|
|
16
|
-
* (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`); no legacy
|
|
17
|
-
* package source is imported or copied.
|
|
18
|
-
*/
|
|
19
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
20
|
-
export const READ_ANCHOR_SCHEMA_VERSION = "omk.read-anchor.v1";
|
|
21
|
-
const HEX64 = /^[0-9a-f]{64}$/i;
|
|
22
|
-
function normalizeNewlines(content) {
|
|
23
|
-
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
24
|
-
}
|
|
25
|
-
function sha256Hex(content) {
|
|
26
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Extract a line block from already newline-normalized content using read-tool
|
|
30
|
-
* semantics: `offset` is a 1-indexed start line, `limit` is the maximum number
|
|
31
|
-
* of lines. Out-of-range requests clamp to an empty block at the end of file.
|
|
32
|
-
*/
|
|
33
|
-
function extractBlock(normalized, offset, limit) {
|
|
34
|
-
const lines = normalized.split("\n");
|
|
35
|
-
const start = Math.max(1, Math.floor(offset ?? 1));
|
|
36
|
-
if (start > lines.length) {
|
|
37
|
-
return { text: "", range: { offset: start, limit: 0, endLine: start - 1 } };
|
|
38
|
-
}
|
|
39
|
-
const available = lines.length - (start - 1);
|
|
40
|
-
const count = limit === undefined ? available : Math.max(0, Math.min(Math.floor(limit), available));
|
|
41
|
-
const blockLines = lines.slice(start - 1, start - 1 + count);
|
|
42
|
-
return {
|
|
43
|
-
text: blockLines.join("\n"),
|
|
44
|
-
range: { offset: start, limit: count, endLine: start - 1 + count },
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Create a hash anchor for a read. Hashes both the full (normalized) file and
|
|
49
|
-
* the specific line block so a later edit can prove it is operating on the same
|
|
50
|
-
* bytes the model saw.
|
|
51
|
-
*/
|
|
52
|
-
export function createReadAnchor(input) {
|
|
53
|
-
const normalized = normalizeNewlines(input.content);
|
|
54
|
-
const block = extractBlock(normalized, input.offset, input.limit);
|
|
55
|
-
return {
|
|
56
|
-
schemaVersion: READ_ANCHOR_SCHEMA_VERSION,
|
|
57
|
-
readId: input.readId ?? randomUUID(),
|
|
58
|
-
path: input.path,
|
|
59
|
-
fileSha256: sha256Hex(normalized),
|
|
60
|
-
range: block.range,
|
|
61
|
-
blockSha256: sha256Hex(block.text),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Verify an anchor against the current file content. `fileMatches` is the strict
|
|
66
|
-
* whole-file check; `blockMatches` re-extracts the anchored range and compares
|
|
67
|
-
* the block hash (useful for lenient relocation diagnostics).
|
|
68
|
-
*/
|
|
69
|
-
export function verifyReadAnchor(anchor, currentContent) {
|
|
70
|
-
const normalized = normalizeNewlines(currentContent);
|
|
71
|
-
const currentFileSha256 = sha256Hex(normalized);
|
|
72
|
-
const block = extractBlock(normalized, anchor.range.offset, anchor.range.limit);
|
|
73
|
-
const currentBlockSha256 = sha256Hex(block.text);
|
|
74
|
-
const fileMatches = currentFileSha256 === anchor.fileSha256;
|
|
75
|
-
const blockMatches = currentBlockSha256 === anchor.blockSha256;
|
|
76
|
-
return {
|
|
77
|
-
ok: fileMatches && blockMatches,
|
|
78
|
-
fileMatches,
|
|
79
|
-
blockMatches,
|
|
80
|
-
currentFileSha256,
|
|
81
|
-
currentBlockSha256,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/** Validate an untrusted value as a {@link ReadAnchor}. Pure and fail-closed. */
|
|
85
|
-
export function validateReadAnchor(value) {
|
|
86
|
-
const errors = [];
|
|
87
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
88
|
-
return { ok: false, errors: ["anchor must be an object"] };
|
|
89
|
-
}
|
|
90
|
-
const anchor = value;
|
|
91
|
-
if (anchor.schemaVersion !== READ_ANCHOR_SCHEMA_VERSION) {
|
|
92
|
-
errors.push(`schemaVersion must be ${READ_ANCHOR_SCHEMA_VERSION}`);
|
|
93
|
-
}
|
|
94
|
-
if (typeof anchor.readId !== "string" || anchor.readId.length === 0)
|
|
95
|
-
errors.push("readId must be a non-empty string");
|
|
96
|
-
if (typeof anchor.path !== "string" || anchor.path.length === 0)
|
|
97
|
-
errors.push("path must be a non-empty string");
|
|
98
|
-
if (typeof anchor.fileSha256 !== "string" || !HEX64.test(anchor.fileSha256)) {
|
|
99
|
-
errors.push("fileSha256 must be a 64-char hex digest");
|
|
100
|
-
}
|
|
101
|
-
if (typeof anchor.blockSha256 !== "string" || !HEX64.test(anchor.blockSha256)) {
|
|
102
|
-
errors.push("blockSha256 must be a 64-char hex digest");
|
|
103
|
-
}
|
|
104
|
-
const range = anchor.range;
|
|
105
|
-
if (typeof range !== "object" || range === null || Array.isArray(range)) {
|
|
106
|
-
errors.push("range must be an object");
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
const r = range;
|
|
110
|
-
for (const key of ["offset", "limit", "endLine"]) {
|
|
111
|
-
if (typeof r[key] !== "number" || !Number.isInteger(r[key]) || r[key] < 0) {
|
|
112
|
-
errors.push(`range.${key} must be a non-negative integer`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (errors.length > 0)
|
|
117
|
-
return { ok: false, errors };
|
|
118
|
-
return { ok: true, errors: [], anchor: value };
|
|
119
|
-
}
|
|
120
|
-
function countOccurrences(haystack, needle) {
|
|
121
|
-
if (needle.length === 0)
|
|
122
|
-
return 0;
|
|
123
|
-
let count = 0;
|
|
124
|
-
let index = haystack.indexOf(needle);
|
|
125
|
-
while (index !== -1) {
|
|
126
|
-
count += 1;
|
|
127
|
-
index = haystack.indexOf(needle, index + needle.length);
|
|
128
|
-
}
|
|
129
|
-
return count;
|
|
130
|
-
}
|
|
131
|
-
const STALE_REREAD_HINT = "stale read: the file changed since it was read; re-read the file before editing";
|
|
132
|
-
/**
|
|
133
|
-
* Decide whether an anchored edit may proceed.
|
|
134
|
-
*
|
|
135
|
-
* - Unchanged file -> `allow` (the common fast path).
|
|
136
|
-
* - Changed file, strict mode -> `reject-stale` (the default; forces a re-read).
|
|
137
|
-
* - Changed file, lenient mode -> `allow-lenient` only when the anchored block
|
|
138
|
-
* text is supplied, hashes to the recorded `blockSha256`, and still occurs
|
|
139
|
-
* exactly once in the current content (unique, non-overlapping relocation).
|
|
140
|
-
* Zero or multiple occurrences -> `reject-stale`.
|
|
141
|
-
*/
|
|
142
|
-
export function decideAnchoredEdit(input) {
|
|
143
|
-
const mode = input.mode ?? "strict";
|
|
144
|
-
const normalized = normalizeNewlines(input.currentContent);
|
|
145
|
-
const currentFileSha256 = sha256Hex(normalized);
|
|
146
|
-
const fileChanged = currentFileSha256 !== input.anchor.fileSha256;
|
|
147
|
-
if (!fileChanged) {
|
|
148
|
-
return { verdict: "allow", reason: "current file hash matches the anchor", currentFileSha256, fileChanged };
|
|
149
|
-
}
|
|
150
|
-
if (mode === "strict") {
|
|
151
|
-
return { verdict: "reject-stale", reason: STALE_REREAD_HINT, currentFileSha256, fileChanged };
|
|
152
|
-
}
|
|
153
|
-
const blockText = input.anchoredBlockText;
|
|
154
|
-
if (blockText === undefined || blockText.length === 0) {
|
|
155
|
-
return {
|
|
156
|
-
verdict: "reject-stale",
|
|
157
|
-
reason: "lenient relocation requires the anchored block text",
|
|
158
|
-
currentFileSha256,
|
|
159
|
-
fileChanged,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const normalizedBlock = normalizeNewlines(blockText);
|
|
163
|
-
if (sha256Hex(normalizedBlock) !== input.anchor.blockSha256) {
|
|
164
|
-
return {
|
|
165
|
-
verdict: "reject-stale",
|
|
166
|
-
reason: "supplied block text does not match the anchored block hash",
|
|
167
|
-
currentFileSha256,
|
|
168
|
-
fileChanged,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const occurrences = countOccurrences(normalized, normalizedBlock);
|
|
172
|
-
if (occurrences === 1) {
|
|
173
|
-
return {
|
|
174
|
-
verdict: "allow-lenient",
|
|
175
|
-
reason: "file changed but the anchored block is still uniquely locatable",
|
|
176
|
-
currentFileSha256,
|
|
177
|
-
fileChanged,
|
|
178
|
-
blockRelocated: true,
|
|
179
|
-
occurrences,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
verdict: "reject-stale",
|
|
184
|
-
reason: occurrences === 0
|
|
185
|
-
? "anchored block no longer exists in the current file"
|
|
186
|
-
: "anchored block is ambiguous (multiple matches) in the current file",
|
|
187
|
-
currentFileSha256,
|
|
188
|
-
fileChanged,
|
|
189
|
-
blockRelocated: false,
|
|
190
|
-
occurrences,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
//# sourceMappingURL=read-anchors.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-anchors.js","sourceRoot":"","sources":["../../src/core/read-anchors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `omk.read-anchor` — pure hash-anchored read/edit primitives.\n *\n * A read anchor binds a read result to the exact bytes the model saw: the\n * newline-normalized SHA-256 of the whole file plus the SHA-256 of the specific\n * line block that was read. Edits can then carry the anchor so the core can\n * reject stale edits (the file changed under the model) before applying them.\n *\n * All hashing in this module is performed on newline-normalized (LF) UTF-8\n * content so anchors are robust to CRLF/CR differences, matching the edit\n * pipeline which normalizes to LF before matching.\n *\n * Side-effect free: no filesystem access; `node:crypto` only.\n *\n * Derived clean-room from the lane C plan\n * (`.omk/runs/omk-package-hardening-plan/recovery-readseek.md`); no legacy\n * package source is imported or copied.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport const READ_ANCHOR_SCHEMA_VERSION = \"omk.read-anchor.v1\";\n\nexport interface ReadAnchorRange {\n\t/** 1-indexed start line (resolved). */\n\treadonly offset: number;\n\t/** Number of lines included in the block (resolved). */\n\treadonly limit: number;\n\t/** 1-indexed last line included, inclusive. Equals `offset - 1` for an empty block. */\n\treadonly endLine: number;\n}\n\nexport interface ReadAnchor {\n\treadonly schemaVersion: typeof READ_ANCHOR_SCHEMA_VERSION;\n\treadonly readId: string;\n\treadonly path: string;\n\t/** SHA-256 (hex) of the newline-normalized full file content. */\n\treadonly fileSha256: string;\n\treadonly range: ReadAnchorRange;\n\t/** SHA-256 (hex) of the newline-normalized read block content. */\n\treadonly blockSha256: string;\n}\n\nconst HEX64 = /^[0-9a-f]{64}$/i;\n\nfunction normalizeNewlines(content: string): string {\n\treturn content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nfunction sha256Hex(content: string): string {\n\treturn createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\");\n}\n\ninterface ExtractedBlock {\n\treadonly text: string;\n\treadonly range: ReadAnchorRange;\n}\n\n/**\n * Extract a line block from already newline-normalized content using read-tool\n * semantics: `offset` is a 1-indexed start line, `limit` is the maximum number\n * of lines. Out-of-range requests clamp to an empty block at the end of file.\n */\nfunction extractBlock(normalized: string, offset: number | undefined, limit: number | undefined): ExtractedBlock {\n\tconst lines = normalized.split(\"\\n\");\n\tconst start = Math.max(1, Math.floor(offset ?? 1));\n\tif (start > lines.length) {\n\t\treturn { text: \"\", range: { offset: start, limit: 0, endLine: start - 1 } };\n\t}\n\tconst available = lines.length - (start - 1);\n\tconst count = limit === undefined ? available : Math.max(0, Math.min(Math.floor(limit), available));\n\tconst blockLines = lines.slice(start - 1, start - 1 + count);\n\treturn {\n\t\ttext: blockLines.join(\"\\n\"),\n\t\trange: { offset: start, limit: count, endLine: start - 1 + count },\n\t};\n}\n\nexport interface CreateReadAnchorInput {\n\treadonly path: string;\n\t/** Full file content as read. */\n\treadonly content: string;\n\t/** 1-indexed start line; defaults to 1 (whole file). */\n\treadonly offset?: number;\n\t/** Maximum number of lines; defaults to the rest of the file. */\n\treadonly limit?: number;\n\t/** Optional deterministic id (otherwise a UUID is generated). */\n\treadonly readId?: string;\n}\n\n/**\n * Create a hash anchor for a read. Hashes both the full (normalized) file and\n * the specific line block so a later edit can prove it is operating on the same\n * bytes the model saw.\n */\nexport function createReadAnchor(input: CreateReadAnchorInput): ReadAnchor {\n\tconst normalized = normalizeNewlines(input.content);\n\tconst block = extractBlock(normalized, input.offset, input.limit);\n\treturn {\n\t\tschemaVersion: READ_ANCHOR_SCHEMA_VERSION,\n\t\treadId: input.readId ?? randomUUID(),\n\t\tpath: input.path,\n\t\tfileSha256: sha256Hex(normalized),\n\t\trange: block.range,\n\t\tblockSha256: sha256Hex(block.text),\n\t};\n}\n\nexport interface ReadAnchorVerification {\n\treadonly ok: boolean;\n\treadonly fileMatches: boolean;\n\treadonly blockMatches: boolean;\n\treadonly currentFileSha256: string;\n\treadonly currentBlockSha256: string;\n}\n\n/**\n * Verify an anchor against the current file content. `fileMatches` is the strict\n * whole-file check; `blockMatches` re-extracts the anchored range and compares\n * the block hash (useful for lenient relocation diagnostics).\n */\nexport function verifyReadAnchor(anchor: ReadAnchor, currentContent: string): ReadAnchorVerification {\n\tconst normalized = normalizeNewlines(currentContent);\n\tconst currentFileSha256 = sha256Hex(normalized);\n\tconst block = extractBlock(normalized, anchor.range.offset, anchor.range.limit);\n\tconst currentBlockSha256 = sha256Hex(block.text);\n\tconst fileMatches = currentFileSha256 === anchor.fileSha256;\n\tconst blockMatches = currentBlockSha256 === anchor.blockSha256;\n\treturn {\n\t\tok: fileMatches && blockMatches,\n\t\tfileMatches,\n\t\tblockMatches,\n\t\tcurrentFileSha256,\n\t\tcurrentBlockSha256,\n\t};\n}\n\n/** Validate an untrusted value as a {@link ReadAnchor}. Pure and fail-closed. */\nexport function validateReadAnchor(value: unknown): { ok: boolean; errors: readonly string[]; anchor?: ReadAnchor } {\n\tconst errors: string[] = [];\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn { ok: false, errors: [\"anchor must be an object\"] };\n\t}\n\tconst anchor = value as Record<string, unknown>;\n\tif (anchor.schemaVersion !== READ_ANCHOR_SCHEMA_VERSION) {\n\t\terrors.push(`schemaVersion must be ${READ_ANCHOR_SCHEMA_VERSION}`);\n\t}\n\tif (typeof anchor.readId !== \"string\" || anchor.readId.length === 0)\n\t\terrors.push(\"readId must be a non-empty string\");\n\tif (typeof anchor.path !== \"string\" || anchor.path.length === 0) errors.push(\"path must be a non-empty string\");\n\tif (typeof anchor.fileSha256 !== \"string\" || !HEX64.test(anchor.fileSha256)) {\n\t\terrors.push(\"fileSha256 must be a 64-char hex digest\");\n\t}\n\tif (typeof anchor.blockSha256 !== \"string\" || !HEX64.test(anchor.blockSha256)) {\n\t\terrors.push(\"blockSha256 must be a 64-char hex digest\");\n\t}\n\tconst range = anchor.range;\n\tif (typeof range !== \"object\" || range === null || Array.isArray(range)) {\n\t\terrors.push(\"range must be an object\");\n\t} else {\n\t\tconst r = range as Record<string, unknown>;\n\t\tfor (const key of [\"offset\", \"limit\", \"endLine\"]) {\n\t\t\tif (typeof r[key] !== \"number\" || !Number.isInteger(r[key]) || (r[key] as number) < 0) {\n\t\t\t\terrors.push(`range.${key} must be a non-negative integer`);\n\t\t\t}\n\t\t}\n\t}\n\tif (errors.length > 0) return { ok: false, errors };\n\treturn { ok: true, errors: [], anchor: value as unknown as ReadAnchor };\n}\n\nexport type AnchoredEditMode = \"strict\" | \"lenient\";\n\nexport type AnchoredEditVerdict = \"allow\" | \"allow-lenient\" | \"reject-stale\";\n\nexport interface AnchoredEditDecisionInput {\n\treadonly anchor: ReadAnchor;\n\t/** Current full file content at edit time. */\n\treadonly currentContent: string;\n\t/** Defaults to `strict`. */\n\treadonly mode?: AnchoredEditMode;\n\t/**\n\t * The exact anchored block text (typically the edit's `oldText`). Required\n\t * for lenient relocation when the file has changed; must hash to\n\t * `anchor.blockSha256` to be trusted.\n\t */\n\treadonly anchoredBlockText?: string;\n}\n\nexport interface AnchoredEditDecision {\n\treadonly verdict: AnchoredEditVerdict;\n\treadonly reason: string;\n\treadonly currentFileSha256: string;\n\treadonly fileChanged: boolean;\n\t/** Lenient mode only: whether the anchored block was uniquely relocated. */\n\treadonly blockRelocated?: boolean;\n\t/** Lenient mode only: occurrences of the anchored block in current content. */\n\treadonly occurrences?: number;\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n\tif (needle.length === 0) return 0;\n\tlet count = 0;\n\tlet index = haystack.indexOf(needle);\n\twhile (index !== -1) {\n\t\tcount += 1;\n\t\tindex = haystack.indexOf(needle, index + needle.length);\n\t}\n\treturn count;\n}\n\nconst STALE_REREAD_HINT = \"stale read: the file changed since it was read; re-read the file before editing\";\n\n/**\n * Decide whether an anchored edit may proceed.\n *\n * - Unchanged file -> `allow` (the common fast path).\n * - Changed file, strict mode -> `reject-stale` (the default; forces a re-read).\n * - Changed file, lenient mode -> `allow-lenient` only when the anchored block\n * text is supplied, hashes to the recorded `blockSha256`, and still occurs\n * exactly once in the current content (unique, non-overlapping relocation).\n * Zero or multiple occurrences -> `reject-stale`.\n */\nexport function decideAnchoredEdit(input: AnchoredEditDecisionInput): AnchoredEditDecision {\n\tconst mode = input.mode ?? \"strict\";\n\tconst normalized = normalizeNewlines(input.currentContent);\n\tconst currentFileSha256 = sha256Hex(normalized);\n\tconst fileChanged = currentFileSha256 !== input.anchor.fileSha256;\n\n\tif (!fileChanged) {\n\t\treturn { verdict: \"allow\", reason: \"current file hash matches the anchor\", currentFileSha256, fileChanged };\n\t}\n\n\tif (mode === \"strict\") {\n\t\treturn { verdict: \"reject-stale\", reason: STALE_REREAD_HINT, currentFileSha256, fileChanged };\n\t}\n\n\tconst blockText = input.anchoredBlockText;\n\tif (blockText === undefined || blockText.length === 0) {\n\t\treturn {\n\t\t\tverdict: \"reject-stale\",\n\t\t\treason: \"lenient relocation requires the anchored block text\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t};\n\t}\n\n\tconst normalizedBlock = normalizeNewlines(blockText);\n\tif (sha256Hex(normalizedBlock) !== input.anchor.blockSha256) {\n\t\treturn {\n\t\t\tverdict: \"reject-stale\",\n\t\t\treason: \"supplied block text does not match the anchored block hash\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t};\n\t}\n\n\tconst occurrences = countOccurrences(normalized, normalizedBlock);\n\tif (occurrences === 1) {\n\t\treturn {\n\t\t\tverdict: \"allow-lenient\",\n\t\t\treason: \"file changed but the anchored block is still uniquely locatable\",\n\t\t\tcurrentFileSha256,\n\t\t\tfileChanged,\n\t\t\tblockRelocated: true,\n\t\t\toccurrences,\n\t\t};\n\t}\n\treturn {\n\t\tverdict: \"reject-stale\",\n\t\treason:\n\t\t\toccurrences === 0\n\t\t\t\t? \"anchored block no longer exists in the current file\"\n\t\t\t\t: \"anchored block is ambiguous (multiple matches) in the current file\",\n\t\tcurrentFileSha256,\n\t\tfileChanged,\n\t\tblockRelocated: false,\n\t\toccurrences,\n\t};\n}\n"]}
|