refactor(js): rename package from 'contexa' to 'cortexa'

Swadhin Biswas committed 7 months ago
af28f1c
1 parent 3fa717d
commit af28f1c
6 files changed
+301 -146
▼JS/README.md
+12-12
1
-# contexa
1
+# cortexa
2
2
3
-[![npm version](https://img.shields.io/npm/v/contexa.svg)](https://www.npmjs.com/package/contexa)
3
+[![npm version](https://img.shields.io/npm/v/cortexa.svg)](https://www.npmjs.com/package/cortexa)
4
4
[![Node.js 18+](https://img.shields.io/badge/node-18%2B-blue.svg)](https://nodejs.org/)
5
5
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
-[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2Fcontexa-black.svg?logo=github)](https://github.com/swadhinbiswas/contexa)
6
+[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2FCortexa-black.svg?logo=github)](https://github.com/swadhinbiswas/Cortexa)
7
7
8
8
**Git-inspired context management for LLM agents.** COMMIT, BRANCH, MERGE, and CONTEXT operations over a persistent versioned memory workspace.
9
9
10
-TypeScript/JavaScript implementation of the **contexa** framework.
10
+TypeScript/JavaScript implementation of the **cortexa** framework.
11
11
12
12
Based on: [arXiv:2508.00031](https://arxiv.org/abs/2508.00031) -- *"Git Context Controller: Manage the Context of LLM-based Agents like Git"* (Junde Wu et al., 2025)
13
13
16
16
## Installation
17
17
18
18
```bash
19
-npm install contexa
19
+npm install cortexa
20
20
```
21
21
22
22
Or with yarn/pnpm:
23
23
24
24
```bash
25
-yarn add contexa
26
-pnpm add contexa
25
+yarn add cortexa
26
+pnpm add cortexa
27
27
```
28
28
29
29
---
31
31
## Quick Start
32
32
33
33
```typescript
34
-import { GCCWorkspace } from "contexa";
34
+import { GCCWorkspace } from "cortexa";
35
35
36
36
// 1. Initialize a workspace
37
37
const ws = new GCCWorkspace("/path/to/project");
216
216
## Running Tests
217
217
218
218
```bash
219
-git clone https://github.com/swadhinbiswas/contexa.git
220
-cd contexa/JS
219
+git clone https://github.com/swadhinbiswas/Cortexa.git
220
+cd cortexa/JS
221
221
npm install
222
222
npm test
223
223
```
261
261
262
262
## Links
263
263
264
-- [GitHub Repository](https://github.com/swadhinbiswas/contexa)
265
-- [npm Package](https://www.npmjs.com/package/contexa)
264
+- [GitHub Repository](https://github.com/swadhinbiswas/Cortexa)
265
+- [npm Package](https://www.npmjs.com/package/cortexa)
266
266
- [Original Paper](https://arxiv.org/abs/2508.00031)
267
267
- [Author: Swadhin Biswas](https://github.com/swadhinbiswas)
268
268
▼JS/package-lock.json
+2-2
1
1
{
2
- "name": "contexa",
2
+ "name": "cortexa",
3
3
"version": "0.1.1",
4
4
"lockfileVersion": 3,
5
5
"requires": true,
6
6
"packages": {
7
7
"": {
8
- "name": "contexa",
8
+ "name": "cortexa",
9
9
"version": "0.1.1",
10
10
"license": "MIT",
11
11
"dependencies": {
12
12
▼JS/package.json
+5-5
1
1
{
2
- "name": "contexa",
2
+ "name": "cortexa",
3
3
"version": "0.1.1",
4
4
"description": "Git-inspired context management for LLM agents -- COMMIT, BRANCH, MERGE, and CONTEXT over versioned memory (arXiv:2508.00031)",
5
5
"author": "Swadhin Biswas <[email protected]>",
23
23
"test": "tsx --test test.ts",
24
24
"lint": "eslint src"
25
25
},
26
- "keywords": ["llm", "agent", "context-management", "git", "memory", "gcc", "contexa"],
26
+ "keywords": ["llm", "agent", "context-management", "git", "memory", "gcc", "cortexa"],
27
27
"license": "MIT",
28
28
"repository": {
29
29
"type": "git",
30
- "url": "https://github.com/swadhinbiswas/contexa"
30
+ "url": "https://github.com/swadhinbiswas/Cortexa"
31
31
},
32
- "homepage": "https://github.com/swadhinbiswas/contexa",
32
+ "homepage": "https://github.com/swadhinbiswas/Cortexa",
33
33
"bugs": {
34
- "url": "https://github.com/swadhinbiswas/contexa/issues"
34
+ "url": "https://github.com/swadhinbiswas/Cortexa/issues"
35
35
},
36
36
"dependencies": {
37
37
"js-yaml": "^4.1.0"
38
38
▼JS/src/index.ts
+1-1
1
1
/**
2
- * contexa — Git-inspired context management for LLM agents.
2
+ * cortexa — Git-inspired context management for LLM agents.
3
3
* COMMIT, BRANCH, MERGE, and CONTEXT over versioned memory.
4
4
*
5
5
* Paper: "Git Context Controller: Manage the Context of LLM-based Agents like Git"
6
6
▼JS/src/workspace.ts
+280-125
39
39
const now = (): string => new Date().toISOString();
40
40
const shortId = (): string => randomBytes(4).toString("hex");
41
41
42
+/** Escape separator sequences in user-provided content to prevent parser breakage. */
43
+function sanitize(text: string): string {
44
+ return text.replace(/\n---\n/g, "\n\\---\n");
45
+}
46
+
47
+/** Reverse the escaping applied by `sanitize`. */
48
+function desanitize(text: string): string {
49
+ return text.replace(/\n\\---\n/g, "\n---\n");
50
+}
51
+
52
+/**
53
+ * Split markdown text on `---\n` while respecting escaped separators
54
+ * (`\---\n` produced by `sanitize`). After splitting, escaped separators
55
+ * are left in place; callers should apply `desanitize` on field values.
56
+ */
57
+function splitBlocks(text: string): string[] {
58
+ const raw = text.split("---\n");
59
+ const blocks: string[] = [];
60
+ let i = 0;
61
+ while (i < raw.length) {
62
+ let block = raw[i];
63
+ while (block.endsWith("\\") && i + 1 < raw.length) {
64
+ i++;
65
+ block = block + "---\n" + raw[i];
66
+ }
67
+ blocks.push(block);
68
+ i++;
69
+ }
70
+ return blocks;
71
+}
72
+
42
73
function otaToMarkdown(r: OTARecord): string {
43
74
return (
44
75
`### Step ${r.step} — ${r.timestamp}\n` +
45
- `**Observation:** ${r.observation}\n\n` +
46
- `**Thought:** ${r.thought}\n\n` +
47
- `**Action:** ${r.action}\n\n` +
76
+ `**Observation:** ${sanitize(r.observation)}\n\n` +
77
+ `**Thought:** ${sanitize(r.thought)}\n\n` +
78
+ `**Action:** ${sanitize(r.action)}\n\n` +
48
79
`---\n`
49
80
);
50
81
}
53
84
return (
54
85
`## Commit \`${c.commitId}\`\n` +
55
86
`**Timestamp:** ${c.timestamp}\n\n` +
56
- `**Branch Purpose:** ${c.branchPurpose}\n\n` +
57
- `**Previous Progress Summary:** ${c.previousProgressSummary}\n\n` +
58
- `**This Commit's Contribution:** ${c.thisCommitContribution}\n\n` +
87
+ `**Branch Purpose:** ${sanitize(c.branchPurpose)}\n\n` +
88
+ `**Previous Progress Summary:** ${sanitize(c.previousProgressSummary)}\n\n` +
89
+ `**This Commit's Contribution:** ${sanitize(c.thisCommitContribution)}\n\n` +
59
90
`---\n`
60
91
);
61
92
}
94
125
};
95
126
}
96
127
128
+/** Raise if value is empty or whitespace-only. */
129
+function validateNotEmpty(value: string, field: string): void {
130
+ if (!value || !value.trim()) {
131
+ throw new Error(`${field} must not be empty.`);
132
+ }
133
+}
134
+
135
+/** Raise if name is not a valid branch identifier. */
136
+function validateBranchName(name: string): void {
137
+ validateNotEmpty(name, "Branch name");
138
+ if (name.includes("/") || name.includes("\\")) {
139
+ throw new Error(`Branch name must not contain path separators: '${name}'`);
140
+ }
141
+ if (name === "." || name === "..") {
142
+ throw new Error(`Branch name must not be '.' or '..': '${name}'`);
143
+ }
144
+}
145
+
97
146
/** Manages the `.GCC/` directory structure for one agent project. */
98
147
export class GCCWorkspace {
99
148
private readonly root: string;
105
154
this.gccDir = path.join(projectRoot, GCC_DIR);
106
155
}
107
156
157
+ // ------------------------------------------------------------------ //
158
+ // File locking //
159
+ // ------------------------------------------------------------------ //
160
+
161
+ /**
162
+ * Acquire an exclusive lock on `.GCC/.lock` and run `fn`.
163
+ *
164
+ * Uses `O_EXLOCK` where available (macOS) and falls back to
165
+ * `fs.flockSync` via a tight open-with-exclusive flag retry loop
166
+ * on Linux/Windows. The lock is always released when `fn` returns
167
+ * (or throws).
168
+ */
169
+ private withLock<T>(fn: () => T): T {
170
+ const lockPath = path.join(this.gccDir, ".lock");
171
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
172
+
173
+ // Open (or create) the lock file. We acquire an exclusive,
174
+ // blocking lock by opening with O_RDWR|O_CREAT then using
175
+ // Node's undocumented-but-stable flock binding through the fd.
176
+ //
177
+ // Since Node does not expose flock() directly, we use the
178
+ // simplest portable approach: open with 'wx+' in a retry loop.
179
+ // On success we hold the exclusive create lock; on EEXIST we
180
+ // wait briefly and retry. This is a cooperative lock: all
181
+ // cortexa JS instances follow the same protocol.
182
+ const maxWait = 10_000; // 10 seconds
183
+ const interval = 50; // ms between retries
184
+ let waited = 0;
185
+ let fd: number;
186
+
187
+ while (true) {
188
+ try {
189
+ // O_CREAT | O_EXCL | O_RDWR — atomic create-or-fail
190
+ fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR, 0o644);
191
+ break;
192
+ } catch (err: any) {
193
+ if (err.code !== "EEXIST") throw err;
194
+ if (waited >= maxWait) {
195
+ // Stale lock — force remove and retry once
196
+ try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
197
+ fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR, 0o644);
198
+ break;
199
+ }
200
+ // Busy-wait (synchronous context — no async available)
201
+ const start = Date.now();
202
+ while (Date.now() - start < interval) { /* spin */ }
203
+ waited += interval;
204
+ }
205
+ }
206
+
207
+ try {
208
+ return fn();
209
+ } finally {
210
+ try { fs.closeSync(fd!); } catch { /* ignore */ }
211
+ try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
212
+ }
213
+ }
214
+
108
215
// ------------------------------------------------------------------ //
109
216
// Paths //
110
217
// ------------------------------------------------------------------ //
149
256
private parseCommits(branch: string): CommitRecord[] {
150
257
const text = this.read(this.commitPath(branch));
151
258
const records: CommitRecord[] = [];
152
- for (const block of text.split("---\n")) {
259
+ for (const block of splitBlocks(text)) {
153
260
const trimmed = block.trim();
154
261
if (!trimmed) continue;
155
262
let commitId = "",
156
- branchPurpose = "",
157
- prevSummary = "",
158
- contribution = "",
159
263
ts = "";
264
+ const fields: Record<string, string[]> = {};
265
+ let current: string | null = null;
266
+
160
267
for (const line of trimmed.split("\n")) {
161
268
if (line.startsWith("## Commit `")) {
162
269
commitId = line.replace(/^## Commit `|`\s*$/g, "");
270
+ current = null;
163
271
} else if (line.startsWith("**Timestamp:**")) {
164
272
ts = line.replace("**Timestamp:**", "").trim();
273
+ current = null;
165
274
} else if (line.startsWith("**Branch Purpose:**")) {
166
- branchPurpose = line.replace("**Branch Purpose:**", "").trim();
275
+ fields.branchPurpose = [line.replace("**Branch Purpose:**", "").trim()];
276
+ current = "branchPurpose";
167
277
} else if (line.startsWith("**Previous Progress Summary:**")) {
168
- prevSummary = line
169
- .replace("**Previous Progress Summary:**", "")
170
- .trim();
278
+ fields.prevSummary = [line.replace("**Previous Progress Summary:**", "").trim()];
279
+ current = "prevSummary";
171
280
} else if (line.startsWith("**This Commit's Contribution:**")) {
172
- contribution = line
173
- .replace("**This Commit's Contribution:**", "")
174
- .trim();
281
+ fields.contribution = [line.replace("**This Commit's Contribution:**", "").trim()];
282
+ current = "contribution";
283
+ } else if (current && line.trim()) {
284
+ fields[current].push(line.trim());
175
285
}
176
286
}
177
287
if (commitId) {
288
+ const get = (key: string) =>
289
+ desanitize((fields[key] || [""]).join("\n").trim());
178
290
records.push({
179
291
commitId,
180
292
branchName: branch,
181
- branchPurpose,
182
- previousProgressSummary: prevSummary,
183
- thisCommitContribution: contribution,
293
+ branchPurpose: get("branchPurpose"),
294
+ previousProgressSummary: get("prevSummary"),
295
+ thisCommitContribution: get("contribution"),
184
296
timestamp: ts,
185
297
});
186
298
}
191
303
private parseOTA(branch: string): OTARecord[] {
192
304
const text = this.read(this.logPath(branch));
193
305
const records: OTARecord[] = [];
194
- for (const block of text.split("---\n")) {
306
+ for (const block of splitBlocks(text)) {
195
307
const trimmed = block.trim();
196
308
if (!trimmed) continue;
197
309
let step = 0,
198
- ts = "",
199
- obs = "",
200
- thought = "",
201
- action = "";
310
+ ts = "";
311
+ const fields: Record<string, string[]> = {};
312
+ let current: string | null = null;
313
+
202
314
for (const line of trimmed.split("\n")) {
203
315
if (line.startsWith("### Step ")) {
204
316
const parts = line.split(" — ");
205
317
step = parseInt(parts[0]?.replace("### Step ", "") ?? "0", 10) || 0;
206
318
ts = parts[1]?.trim() ?? "";
319
+ current = null;
207
320
} else if (line.startsWith("**Observation:**")) {
208
- obs = line.replace("**Observation:**", "").trim();
321
+ fields.obs = [line.replace("**Observation:**", "").trim()];
322
+ current = "obs";
209
323
} else if (line.startsWith("**Thought:**")) {
210
- thought = line.replace("**Thought:**", "").trim();
324
+ fields.thought = [line.replace("**Thought:**", "").trim()];
325
+ current = "thought";
211
326
} else if (line.startsWith("**Action:**")) {
212
- action = line.replace("**Action:**", "").trim();
327
+ fields.action = [line.replace("**Action:**", "").trim()];
328
+ current = "action";
329
+ } else if (current && line.trim()) {
330
+ fields[current].push(line.trim());
213
331
}
214
332
}
333
+ const get = (key: string) =>
334
+ desanitize((fields[key] || [""]).join("\n").trim());
335
+ const obs = get("obs");
336
+ const thought = get("thought");
215
337
if (obs || thought) {
216
- records.push({ step, timestamp: ts, observation: obs, thought, action });
338
+ records.push({ step, timestamp: ts, observation: obs, thought, action: get("action") });
217
339
}
218
340
}
219
341
return records;
261
383
}
262
384
fs.mkdirSync(this.branchDir(MAIN_BRANCH), { recursive: true });
263
385
264
- this.write(
265
- this.mainMd(),
266
- `# Project Roadmap\n\n**Initialized:** ${now()}\n\n${projectRoadmap}\n`
267
- );
268
- this.write(this.logPath(MAIN_BRANCH), `# OTA Log — branch \`${MAIN_BRANCH}\`\n\n`);
269
- this.write(this.commitPath(MAIN_BRANCH), `# Commit History — branch \`${MAIN_BRANCH}\`\n\n`);
270
-
271
- const meta: BranchMetadata = {
272
- name: MAIN_BRANCH,
273
- purpose: "Primary reasoning trajectory",
274
- createdFrom: "",
275
- createdAt: now(),
276
- status: "active",
277
- };
278
- this.write(this.metaPath(MAIN_BRANCH), this.dumpMeta(meta));
386
+ this.withLock(() => {
387
+ this.write(
388
+ this.mainMd(),
389
+ `# Project Roadmap\n\n**Initialized:** ${now()}\n\n${projectRoadmap}\n`
390
+ );
391
+ this.write(this.logPath(MAIN_BRANCH), `# OTA Log — branch \`${MAIN_BRANCH}\`\n\n`);
392
+ this.write(this.commitPath(MAIN_BRANCH), `# Commit History — branch \`${MAIN_BRANCH}\`\n\n`);
393
+
394
+ const meta: BranchMetadata = {
395
+ name: MAIN_BRANCH,
396
+ purpose: "Primary reasoning trajectory",
397
+ createdFrom: "",
398
+ createdAt: now(),
399
+ status: "active",
400
+ };
401
+ this.write(this.metaPath(MAIN_BRANCH), this.dumpMeta(meta));
402
+ });
279
403
this._currentBranch = MAIN_BRANCH;
280
404
}
281
405
296
420
* The paper logs continuous Observation–Thought–Action cycles.
297
421
*/
298
422
logOTA(observation: string, thought: string, action: string): OTARecord {
299
- const existing = this.parseOTA(this._currentBranch);
300
- const record: OTARecord = {
301
- step: existing.length + 1,
302
- timestamp: now(),
303
- observation,
304
- thought,
305
- action,
306
- };
307
- this.append(this.logPath(this._currentBranch), otaToMarkdown(record));
308
- return record;
423
+ if (!observation?.trim() && !thought?.trim() && !action?.trim()) {
424
+ throw new Error("At least one of observation, thought, or action must be non-empty.");
425
+ }
426
+ return this.withLock(() => {
427
+ const existing = this.parseOTA(this._currentBranch);
428
+ const record: OTARecord = {
429
+ step: existing.length + 1,
430
+ timestamp: now(),
431
+ observation,
432
+ thought,
433
+ action,
434
+ };
435
+ this.append(this.logPath(this._currentBranch), otaToMarkdown(record));
436
+ return record;
437
+ });
309
438
}
310
439
311
440
/**
318
447
previousSummary?: string,
319
448
updateRoadmap?: string
320
449
): CommitRecord {
321
- const meta = this.parseMeta(this._currentBranch);
322
- const branchPurpose = meta?.purpose ?? "";
323
-
324
- let prevSummary = previousSummary;
325
- if (!prevSummary) {
326
- const commits = this.parseCommits(this._currentBranch);
327
- prevSummary =
328
- commits.at(-1)?.thisCommitContribution ??
329
- "Initial state — no prior commits.";
330
- }
450
+ validateNotEmpty(contribution, "Contribution");
451
+ return this.withLock(() => {
452
+ const meta = this.parseMeta(this._currentBranch);
453
+ const branchPurpose = meta?.purpose ?? "";
454
+
455
+ let prevSummary = previousSummary;
456
+ if (!prevSummary) {
457
+ const commits = this.parseCommits(this._currentBranch);
458
+ prevSummary =
459
+ commits.at(-1)?.thisCommitContribution ??
460
+ "Initial state — no prior commits.";
461
+ }
331
462
332
- const record: CommitRecord = {
333
- commitId: shortId(),
334
- branchName: this._currentBranch,
335
- branchPurpose,
336
- previousProgressSummary: prevSummary,
337
- thisCommitContribution: contribution,
338
- timestamp: now(),
339
- };
463
+ const record: CommitRecord = {
464
+ commitId: shortId(),
465
+ branchName: this._currentBranch,
466
+ branchPurpose,
467
+ previousProgressSummary: prevSummary,
468
+ thisCommitContribution: contribution,
469
+ timestamp: now(),
470
+ };
340
471
341
- this.append(this.commitPath(this._currentBranch), commitToMarkdown(record));
472
+ this.append(this.commitPath(this._currentBranch), commitToMarkdown(record));
342
473
343
- if (updateRoadmap) {
344
- this.append(this.mainMd(), `\n## Update (${record.timestamp})\n${updateRoadmap}\n`);
345
- }
474
+ if (updateRoadmap) {
475
+ this.append(this.mainMd(), `\n## Update (${record.timestamp})\n${updateRoadmap}\n`);
476
+ }
346
477
347
- return record;
478
+ return record;
479
+ });
348
480
}
349
481
350
482
/**
352
484
* Creates isolated workspace: B_t^(name) = BRANCH(M_{t-1}).
353
485
*/
354
486
branch(name: string, purpose: string): void {
487
+ validateBranchName(name);
488
+ validateNotEmpty(purpose, "Branch purpose");
355
489
if (fs.existsSync(this.branchDir(name))) {
356
490
throw new Error(`Branch '${name}' already exists.`);
357
491
}
358
492
fs.mkdirSync(this.branchDir(name), { recursive: true });
359
- this.write(this.logPath(name), `# OTA Log — branch \`${name}\`\n\n`);
360
- this.write(this.commitPath(name), `# Commit History — branch \`${name}\`\n\n`);
361
-
362
- const meta: BranchMetadata = {
363
- name,
364
- purpose,
365
- createdFrom: this._currentBranch,
366
- createdAt: now(),
367
- status: "active",
368
- };
369
- this.write(this.metaPath(name), this.dumpMeta(meta));
493
+
494
+ this.withLock(() => {
495
+ this.write(this.logPath(name), `# OTA Log — branch \`${name}\`\n\n`);
496
+ this.write(this.commitPath(name), `# Commit History — branch \`${name}\`\n\n`);
497
+
498
+ const meta: BranchMetadata = {
499
+ name,
500
+ purpose,
501
+ createdFrom: this._currentBranch,
502
+ createdAt: now(),
503
+ status: "active",
504
+ };
505
+ this.write(this.metaPath(name), this.dumpMeta(meta));
506
+ });
370
507
this._currentBranch = name;
371
508
}
372
509
379
516
summary?: string,
380
517
target: string = MAIN_BRANCH
381
518
): CommitRecord {
519
+ validateBranchName(branchName);
520
+ validateBranchName(target);
382
521
if (!fs.existsSync(this.branchDir(branchName))) {
383
522
throw new Error(`Branch '${branchName}' not found.`);
384
523
}
386
525
throw new Error(`Target branch '${target}' not found.`);
387
526
}
388
527
389
- const branchCommits = this.parseCommits(branchName);
390
- const branchOTA = this.parseOTA(branchName);
391
- const meta = this.parseMeta(branchName);
392
-
393
- const mergeSummary =
394
- summary ??
395
- `Merged branch \`${branchName}\` (${branchCommits.length} commits). Contributions: ${branchCommits
396
- .map((c) => c.thisCommitContribution)
397
- .join(" | ")}`;
398
-
399
- if (branchOTA.length > 0) {
400
- this.append(
401
- this.logPath(target),
402
- `\n## Merged from \`${branchName}\` (${now()})\n\n`
403
- );
404
- for (const rec of branchOTA) {
405
- this.append(this.logPath(target), otaToMarkdown(rec));
528
+ let mergeSummary: string;
529
+ let metaCopy: BranchMetadata | undefined;
530
+
531
+ this.withLock(() => {
532
+ const branchCommits = this.parseCommits(branchName);
533
+ const branchOTA = this.parseOTA(branchName);
534
+ const meta = this.parseMeta(branchName);
535
+ metaCopy = meta;
536
+
537
+ mergeSummary =
538
+ summary ??
539
+ `Merged branch \`${branchName}\` (${branchCommits.length} commits). Contributions: ${branchCommits
540
+ .map((c) => c.thisCommitContribution)
541
+ .join(" | ")}`;
542
+
543
+ if (branchOTA.length > 0) {
544
+ this.append(
545
+ this.logPath(target),
546
+ `\n## Merged from \`${branchName}\` (${now()})\n\n`
547
+ );
548
+ for (const rec of branchOTA) {
549
+ this.append(this.logPath(target), otaToMarkdown(rec));
550
+ }
406
551
}
407
- }
408
552
409
- this._currentBranch = target;
553
+ this._currentBranch = target;
554
+ });
555
+
556
+ // commit() acquires the lock internally
410
557
const mergeCommit = this.commit(
411
- mergeSummary,
412
- `Merging branch \`${branchName}\` with purpose: ${meta?.purpose ?? ""}`,
413
- mergeSummary
558
+ mergeSummary!,
559
+ `Merging branch \`${branchName}\` with purpose: ${metaCopy?.purpose ?? ""}`,
560
+ mergeSummary!
414
561
);
415
562
416
- if (meta) {
417
- const updated: BranchMetadata = {
418
- ...meta,
419
- status: "merged",
420
- mergedInto: target,
421
- mergedAt: now(),
422
- };
423
- this.write(this.metaPath(branchName), this.dumpMeta(updated));
424
- }
563
+ this.withLock(() => {
564
+ if (metaCopy) {
565
+ const updated: BranchMetadata = {
566
+ ...metaCopy,
567
+ status: "merged",
568
+ mergedInto: target,
569
+ mergedAt: now(),
570
+ };
571
+ this.write(this.metaPath(branchName), this.dumpMeta(updated));
572
+ }
573
+ });
425
574
426
575
return mergeCommit;
427
576
}
431
580
* Retrieves history at K-commit resolution. Paper default: K=1.
432
581
*/
433
582
context(branch?: string, k = 1): ContextResult {
583
+ if (k < 1) {
584
+ throw new Error(`k must be >= 1, got ${k}`);
585
+ }
434
586
const target = branch ?? this._currentBranch;
435
587
if (!fs.existsSync(this.branchDir(target))) {
436
588
throw new Error(`Branch '${target}' not found.`);
437
589
}
438
590
439
- const allCommits = this.parseCommits(target);
440
- const commits = allCommits.slice(-k);
441
- const otaRecords = this.parseOTA(target);
442
- const mainRoadmap = this.read(this.mainMd());
443
- const metadata = this.parseMeta(target);
591
+ return this.withLock(() => {
592
+ const allCommits = this.parseCommits(target);
593
+ const commits = allCommits.slice(-k);
594
+ const otaRecords = this.parseOTA(target);
595
+ const mainRoadmap = this.read(this.mainMd());
596
+ const metadata = this.parseMeta(target);
444
597
445
- return makeContextResult(target, k, commits, otaRecords, mainRoadmap, metadata);
598
+ return makeContextResult(target, k, commits, otaRecords, mainRoadmap, metadata);
599
+ });
446
600
}
447
601
448
602
// ------------------------------------------------------------------ //
454
608
}
455
609
456
610
switchBranch(name: string): void {
611
+ validateBranchName(name);
457
612
if (!fs.existsSync(this.branchDir(name))) {
458
613
throw new Error(`Branch '${name}' does not exist.`);
459
614
}
460
615
▼JS/test.ts
+1-1
1
1
/**
2
- * Tests for contexa TypeScript package.
2
+ * Tests for cortexa TypeScript package.
3
3
* Run with: tsx --test test.ts
4
4
*/
5
5
6
6