@kognai/orchestrator-core 0.1.1 → 0.1.3

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.
@@ -0,0 +1,18 @@
1
+ export interface ExtractProposal {
2
+ sourceFile: string;
3
+ startAnchor: string;
4
+ endAnchor: string;
5
+ targetModule: string;
6
+ targetHeader: string;
7
+ sourceReplacement: string;
8
+ }
9
+ export interface ExtractResult {
10
+ extracted: boolean;
11
+ reverted: boolean;
12
+ reason: string;
13
+ linesMoved: number;
14
+ }
15
+ export declare function extractToModule(p: ExtractProposal, opts: {
16
+ verify?: () => boolean | Promise<boolean>;
17
+ root?: string;
18
+ }): Promise<ExtractResult>;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractToModule = extractToModule;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const patcher_js_1 = require("./patcher.js");
7
+ function resolvePath(p, root) {
8
+ if ((0, node_path_1.isAbsolute)(p))
9
+ return p;
10
+ return (0, node_path_1.resolve)(root ?? process.cwd(), p);
11
+ }
12
+ function countOccurrences(haystack, needle) {
13
+ if (needle.length === 0)
14
+ return 0;
15
+ let count = 0;
16
+ let idx = 0;
17
+ while (true) {
18
+ const found = haystack.indexOf(needle, idx);
19
+ if (found === -1)
20
+ break;
21
+ count++;
22
+ idx = found + needle.length;
23
+ }
24
+ return count;
25
+ }
26
+ function findLineStart(content, index) {
27
+ if (index <= 0)
28
+ return 0;
29
+ const prevNewline = content.lastIndexOf('\n', index - 1);
30
+ return prevNewline === -1 ? 0 : prevNewline + 1;
31
+ }
32
+ function findLineEnd(content, index) {
33
+ const nextNewline = content.indexOf('\n', index);
34
+ return nextNewline === -1 ? content.length : nextNewline;
35
+ }
36
+ function countLines(chunk) {
37
+ if (chunk.length === 0)
38
+ return 0;
39
+ let count = 1;
40
+ for (let i = 0; i < chunk.length; i++) {
41
+ if (chunk.charCodeAt(i) === 10)
42
+ count++;
43
+ }
44
+ // If chunk ends with newline, the trailing "line" isn't a real line
45
+ if (chunk.charCodeAt(chunk.length - 1) === 10)
46
+ count--;
47
+ return Math.max(count, 1);
48
+ }
49
+ async function extractToModule(p, opts) {
50
+ const sourcePath = resolvePath(p.sourceFile, opts.root);
51
+ const targetPath = resolvePath(p.targetModule, opts.root);
52
+ let originalSource;
53
+ try {
54
+ originalSource = await (0, promises_1.readFile)(sourcePath, 'utf8');
55
+ }
56
+ catch (err) {
57
+ return {
58
+ extracted: false,
59
+ reverted: false,
60
+ reason: `failed to read source file: ${err.message}`,
61
+ linesMoved: 0,
62
+ };
63
+ }
64
+ if (p.startAnchor.length === 0) {
65
+ return {
66
+ extracted: false,
67
+ reverted: false,
68
+ reason: 'startAnchor must be non-empty',
69
+ linesMoved: 0,
70
+ };
71
+ }
72
+ if (p.endAnchor.length === 0) {
73
+ return {
74
+ extracted: false,
75
+ reverted: false,
76
+ reason: 'endAnchor must be non-empty',
77
+ linesMoved: 0,
78
+ };
79
+ }
80
+ const startCount = countOccurrences(originalSource, p.startAnchor);
81
+ if (startCount !== 1) {
82
+ return {
83
+ extracted: false,
84
+ reverted: false,
85
+ reason: `startAnchor must occur exactly once (found ${startCount})`,
86
+ linesMoved: 0,
87
+ };
88
+ }
89
+ const endCount = countOccurrences(originalSource, p.endAnchor);
90
+ if (endCount !== 1) {
91
+ return {
92
+ extracted: false,
93
+ reverted: false,
94
+ reason: `endAnchor must occur exactly once (found ${endCount})`,
95
+ linesMoved: 0,
96
+ };
97
+ }
98
+ const startIdx = originalSource.indexOf(p.startAnchor);
99
+ const endIdx = originalSource.indexOf(p.endAnchor);
100
+ if (endIdx < startIdx) {
101
+ return {
102
+ extracted: false,
103
+ reverted: false,
104
+ reason: 'endAnchor occurs before startAnchor',
105
+ linesMoved: 0,
106
+ };
107
+ }
108
+ const chunkStart = findLineStart(originalSource, startIdx);
109
+ const chunkEnd = findLineEnd(originalSource, endIdx + p.endAnchor.length - 1);
110
+ if (chunkEnd < chunkStart) {
111
+ return {
112
+ extracted: false,
113
+ reverted: false,
114
+ reason: 'computed chunk range is invalid',
115
+ linesMoved: 0,
116
+ };
117
+ }
118
+ const chunk = originalSource.slice(chunkStart, chunkEnd);
119
+ const linesMoved = countLines(chunk);
120
+ const newModuleContent = p.targetHeader + '\n' + chunk;
121
+ // Build new source: replace the chunk with sourceReplacement.
122
+ // chunkEnd points to the position of the trailing newline (or EOF).
123
+ // We replace only the chunk content (not the trailing newline) so the
124
+ // existing line break structure is preserved.
125
+ const newSourceContent = originalSource.slice(0, chunkStart) +
126
+ p.sourceReplacement +
127
+ originalSource.slice(chunkEnd);
128
+ // At this point, validation has passed. Begin writes.
129
+ // Step 1: write the new module file.
130
+ let targetWritten = false;
131
+ try {
132
+ await (0, patcher_js_1.writeFileAtomic)(targetPath, newModuleContent);
133
+ targetWritten = true;
134
+ }
135
+ catch (err) {
136
+ return {
137
+ extracted: false,
138
+ reverted: false,
139
+ reason: `failed to write target module: ${err.message}`,
140
+ linesMoved: 0,
141
+ };
142
+ }
143
+ // Step 2: rewrite the source file.
144
+ let sourceWritten = false;
145
+ try {
146
+ await (0, patcher_js_1.writeFileAtomic)(sourcePath, newSourceContent);
147
+ sourceWritten = true;
148
+ }
149
+ catch (err) {
150
+ // Failed to write the source; revert the target module write.
151
+ if (targetWritten) {
152
+ try {
153
+ await (0, promises_1.unlink)(targetPath);
154
+ }
155
+ catch {
156
+ // best effort; ignore
157
+ }
158
+ }
159
+ return {
160
+ extracted: false,
161
+ reverted: false,
162
+ reason: `failed to write source file: ${err.message}`,
163
+ linesMoved: 0,
164
+ };
165
+ }
166
+ // Step 3: verify.
167
+ let verifyOk = true;
168
+ let verifyError = null;
169
+ if (opts.verify) {
170
+ try {
171
+ const result = await opts.verify();
172
+ verifyOk = result !== false;
173
+ }
174
+ catch (err) {
175
+ verifyOk = false;
176
+ verifyError = err.message;
177
+ }
178
+ }
179
+ if (!verifyOk) {
180
+ // RESTORE: delete the new module and rewrite the original source from backup.
181
+ let revertReason = verifyError
182
+ ? `verify threw: ${verifyError}`
183
+ : 'verify returned false';
184
+ if (sourceWritten) {
185
+ try {
186
+ await (0, patcher_js_1.writeFileAtomic)(sourcePath, originalSource);
187
+ }
188
+ catch (err) {
189
+ revertReason += `; failed to restore source: ${err.message}`;
190
+ }
191
+ }
192
+ if (targetWritten) {
193
+ try {
194
+ await (0, promises_1.unlink)(targetPath);
195
+ }
196
+ catch (err) {
197
+ revertReason += `; failed to delete target: ${err.message}`;
198
+ }
199
+ }
200
+ return {
201
+ extracted: false,
202
+ reverted: true,
203
+ reason: revertReason,
204
+ linesMoved: 0,
205
+ };
206
+ }
207
+ return {
208
+ extracted: true,
209
+ reverted: false,
210
+ reason: 'ok',
211
+ linesMoved,
212
+ };
213
+ }
@@ -0,0 +1,75 @@
1
+ import { type Patch } from "./patcher";
2
+ /**
3
+ * Classification of a proposed change, per TICKET-230.
4
+ *
5
+ * - `'ops'` — operations / workflow changes. Safe to auto-apply through
6
+ * the deterministic, verify-gated, reversible Plumber path.
7
+ * - `'contract'` — architecture / security / compliance / supervision /
8
+ * scoring changes. These touch the system's contract with
9
+ * its operators and MUST NOT be auto-applied. The fixer
10
+ * returns an escalation result; the runner is responsible
11
+ * for routing to the Council-Brief flow.
12
+ */
13
+ export type ChangeClass = "ops" | "contract";
14
+ /**
15
+ * A single proposed fix targeting one file.
16
+ *
17
+ * `patches` are anchored string replacements consumed by `applyPatches` from
18
+ * `./patcher`. `changeClass` gates whether the fix is auto-applyable.
19
+ * `rationale` is operator-readable context (the runner logs it; the fixer
20
+ * itself does not).
21
+ */
22
+ export interface FixProposal {
23
+ file: string;
24
+ patches: Patch[];
25
+ changeClass: ChangeClass;
26
+ rationale: string;
27
+ }
28
+ /**
29
+ * Terminal outcome of `applyFix`.
30
+ *
31
+ * - `applied` — patches were written to disk AND (if a verifier was
32
+ * supplied) verify returned true. If verify failed, the
33
+ * file was restored and `applied` is false.
34
+ * - `escalated` — proposal was a `'contract'`-class change; nothing was
35
+ * read, written, or executed against the filesystem.
36
+ * - `reverted` — patches were written, verify returned false, and the
37
+ * original file content was restored via atomic write.
38
+ * - `reason` — machine- and human-readable explanation of the outcome.
39
+ */
40
+ export interface FixResult {
41
+ applied: boolean;
42
+ escalated: boolean;
43
+ reverted: boolean;
44
+ reason: string;
45
+ }
46
+ /**
47
+ * Apply a single fix proposal under TICKET-216 (deterministic, verify-gated,
48
+ * reversible) governed by TICKET-230 (change-class escalation).
49
+ *
50
+ * Flow:
51
+ * 1. If `changeClass === 'contract'` → escalate immediately. No I/O.
52
+ * 2. Otherwise (`'ops'`):
53
+ * a. Read the target file's current bytes (kept in memory as the
54
+ * rollback snapshot).
55
+ * b. Run `applyPatches` against the snapshot. If it fails (anchor
56
+ * missing / ambiguous / etc.), return `applied: false` with the
57
+ * patcher's failure reasons; the file is NEVER written.
58
+ * c. Atomically write the patched content.
59
+ * d. If a `verify` callback was supplied, await it.
60
+ * - If it returns truthy → success (`applied: true`).
61
+ * - If it returns falsy → atomically restore the snapshot and
62
+ * return `reverted: true, applied: false`.
63
+ * - If it throws → restore the snapshot, propagate as
64
+ * `reverted: true, applied: false` with the error message.
65
+ * e. If no `verify` was supplied, the write itself is the success
66
+ * condition.
67
+ *
68
+ * Notes:
69
+ * - KSL/audit logging is the runner's responsibility, not this layer's.
70
+ * - This function performs no network or process I/O of its own.
71
+ */
72
+ export declare function applyFix(proposal: FixProposal, opts: {
73
+ verify?: () => boolean | Promise<boolean>;
74
+ root?: string;
75
+ }): Promise<FixResult>;
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyFix = applyFix;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const patcher_1 = require("./patcher");
7
+ /**
8
+ * Resolve a proposal's file path against an optional `root`.
9
+ *
10
+ * - Absolute paths are honored as-is (no silent rewriting under `root`).
11
+ * - Relative paths are resolved against `root` if provided, else cwd.
12
+ *
13
+ * This keeps the fixer usable both in production (where the runner supplies
14
+ * a sandbox root) and in unit tests (where cwd is fine).
15
+ */
16
+ function resolveTarget(file, root) {
17
+ if ((0, node_path_1.isAbsolute)(file))
18
+ return file;
19
+ if (root && root.length > 0)
20
+ return (0, node_path_1.resolve)(root, file);
21
+ return (0, node_path_1.resolve)(file);
22
+ }
23
+ /**
24
+ * Apply a single fix proposal under TICKET-216 (deterministic, verify-gated,
25
+ * reversible) governed by TICKET-230 (change-class escalation).
26
+ *
27
+ * Flow:
28
+ * 1. If `changeClass === 'contract'` → escalate immediately. No I/O.
29
+ * 2. Otherwise (`'ops'`):
30
+ * a. Read the target file's current bytes (kept in memory as the
31
+ * rollback snapshot).
32
+ * b. Run `applyPatches` against the snapshot. If it fails (anchor
33
+ * missing / ambiguous / etc.), return `applied: false` with the
34
+ * patcher's failure reasons; the file is NEVER written.
35
+ * c. Atomically write the patched content.
36
+ * d. If a `verify` callback was supplied, await it.
37
+ * - If it returns truthy → success (`applied: true`).
38
+ * - If it returns falsy → atomically restore the snapshot and
39
+ * return `reverted: true, applied: false`.
40
+ * - If it throws → restore the snapshot, propagate as
41
+ * `reverted: true, applied: false` with the error message.
42
+ * e. If no `verify` was supplied, the write itself is the success
43
+ * condition.
44
+ *
45
+ * Notes:
46
+ * - KSL/audit logging is the runner's responsibility, not this layer's.
47
+ * - This function performs no network or process I/O of its own.
48
+ */
49
+ async function applyFix(proposal, opts) {
50
+ // -- TICKET-230 gate: never auto-apply contract-class changes. ----------
51
+ if (proposal.changeClass === "contract") {
52
+ return {
53
+ applied: false,
54
+ escalated: true,
55
+ reverted: false,
56
+ reason: "contract-class change requires Council-Brief escalation (TICKET-230)",
57
+ };
58
+ }
59
+ // From here on, the proposal is `'ops'`-class and eligible for
60
+ // deterministic, reversible application.
61
+ const targetPath = resolveTarget(proposal.file, opts.root);
62
+ // -- Snapshot the original bytes for rollback. -------------------------
63
+ // Failure to read is a hard stop: we cannot guarantee reversibility
64
+ // without a known-good baseline, so we refuse to touch anything.
65
+ let original;
66
+ try {
67
+ original = (0, node_fs_1.readFileSync)(targetPath, "utf8");
68
+ }
69
+ catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ return {
72
+ applied: false,
73
+ escalated: false,
74
+ reverted: false,
75
+ reason: `failed to read target file (${targetPath}): ${msg}`,
76
+ };
77
+ }
78
+ // -- Apply patches in memory (pure, no I/O). ---------------------------
79
+ const patchResult = (0, patcher_1.applyPatches)(original, proposal.patches);
80
+ if (!patchResult.ok || typeof patchResult.content !== "string") {
81
+ return {
82
+ applied: false,
83
+ escalated: false,
84
+ reverted: false,
85
+ reason: `patch application failed: ${patchResult.failures.join("; ")}`,
86
+ };
87
+ }
88
+ // No-op fixes (zero patches, or patches that produced identical content)
89
+ // are still treated as a successful application: the proposal's intent
90
+ // — "make the file look like X" — is satisfied.
91
+ const patched = patchResult.content;
92
+ // -- Write the patched content atomically. -----------------------------
93
+ try {
94
+ (0, patcher_1.writeFileAtomic)(targetPath, patched);
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ // The write failed before any bytes could replace the original on disk
99
+ // (writeFileAtomic does temp+rename), so there is nothing to revert.
100
+ return {
101
+ applied: false,
102
+ escalated: false,
103
+ reverted: false,
104
+ reason: `atomic write failed (${targetPath}): ${msg}`,
105
+ };
106
+ }
107
+ // -- Verify-gate. If no verifier, the write is the success condition. --
108
+ if (typeof opts.verify === "function") {
109
+ let verified;
110
+ try {
111
+ const result = await opts.verify();
112
+ verified = result === true;
113
+ }
114
+ catch (err) {
115
+ // Treat verifier exceptions as a failed verification: revert and
116
+ // surface the error message so the runner can log it.
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ try {
119
+ (0, patcher_1.writeFileAtomic)(targetPath, original);
120
+ }
121
+ catch (restoreErr) {
122
+ const rmsg = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
123
+ return {
124
+ applied: false,
125
+ escalated: false,
126
+ reverted: false,
127
+ reason: `verify threw (${msg}); rollback ALSO failed (${rmsg}) — manual intervention required`,
128
+ };
129
+ }
130
+ return {
131
+ applied: false,
132
+ escalated: false,
133
+ reverted: true,
134
+ reason: `verify threw, reverted: ${msg}`,
135
+ };
136
+ }
137
+ if (!verified) {
138
+ // Verification rejected the patched state. Restore the snapshot.
139
+ try {
140
+ (0, patcher_1.writeFileAtomic)(targetPath, original);
141
+ }
142
+ catch (restoreErr) {
143
+ const rmsg = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
144
+ return {
145
+ applied: false,
146
+ escalated: false,
147
+ reverted: false,
148
+ reason: `verify returned false; rollback ALSO failed (${rmsg}) — manual intervention required`,
149
+ };
150
+ }
151
+ return {
152
+ applied: false,
153
+ escalated: false,
154
+ reverted: true,
155
+ reason: "verify returned false; original content restored",
156
+ };
157
+ }
158
+ }
159
+ return {
160
+ applied: true,
161
+ escalated: false,
162
+ reverted: false,
163
+ reason: `applied ${patchResult.applied} patch(es) to ${targetPath}`,
164
+ };
165
+ }
@@ -7,3 +7,6 @@
7
7
  export * from './types';
8
8
  export * from './conformance';
9
9
  export * from './observer';
10
+ export * from './patcher';
11
+ export * from './fixer';
12
+ export * from './extractor';
@@ -23,3 +23,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
23
23
  __exportStar(require("./types"), exports);
24
24
  __exportStar(require("./conformance"), exports);
25
25
  __exportStar(require("./observer"), exports);
26
+ __exportStar(require("./patcher"), exports);
27
+ __exportStar(require("./fixer"), exports);
28
+ __exportStar(require("./extractor"), exports);
@@ -0,0 +1,44 @@
1
+ export type Patch = {
2
+ old: string;
3
+ new: string;
4
+ };
5
+ export type PatchResult = {
6
+ ok: boolean;
7
+ content?: string;
8
+ applied: number;
9
+ failures: string[];
10
+ };
11
+ /**
12
+ * Apply a list of anchored string-replacement patches to `source`.
13
+ *
14
+ * Contract:
15
+ * - Pure: does NOT touch the filesystem or any other I/O.
16
+ * - For each patch, `old` MUST occur EXACTLY ONCE in `source`. If it occurs
17
+ * zero times or more than once, that patch is recorded as a failure.
18
+ * - All-or-nothing: if ANY patch fails, the function aborts and applies
19
+ * NOTHING. `ok` is false, `content` is undefined, `applied` is 0, and
20
+ * `failures` contains one entry per failing patch.
21
+ * - On full success, every patch's `old` is replaced by its `new` in the
22
+ * order given, and the final `content` is returned with `ok: true`.
23
+ *
24
+ * Validation is performed against the ORIGINAL source (pre-mutation) so that
25
+ * patch ordering cannot accidentally mask a duplicate anchor. We re-validate
26
+ * uniqueness against the in-progress buffer during application only as a
27
+ * defensive check; if that ever trips, the entire operation is aborted.
28
+ */
29
+ export declare function applyPatches(source: string, patches: Patch[]): PatchResult;
30
+ /**
31
+ * Atomically write `content` to `path`.
32
+ *
33
+ * Strategy:
34
+ * 1. Ensure the parent directory exists (mkdir -p).
35
+ * 2. Write to a sibling temp file (same directory, so rename is atomic
36
+ * on POSIX filesystems — cross-device renames are not atomic).
37
+ * 3. Rename temp file over the target path.
38
+ * 4. On any failure after the temp write, attempt to clean up the temp
39
+ * file; surface the original error to the caller.
40
+ *
41
+ * This guarantees readers never observe a partially-written file: they
42
+ * either see the old bytes or the new bytes, never a torn mix.
43
+ */
44
+ export declare function writeFileAtomic(path: string, content: string): void;