@ngockhoale/ukit 2.3.4 → 2.3.5

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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.3.5 - 2026-09-10
6
+
7
+ Source-audit hardening release: the code index cannot corrupt itself, and a corrupt runtime
8
+ cache can no longer take down `ukit status` or `ukit memory`.
9
+
10
+ Index artifacts (`files`, `symbols`, `imports`, `calls`, `tests-map`, `hotspots`,
11
+ `archetypes`, `relations`, `analogs`, `meta`) are now written through same-directory
12
+ `.tmp-<timestamp>-<random>` files followed by an atomic `rename`, in both the source builder
13
+ (`src/index/buildIndex.js`) and the shipped `templates/.claude/ukit/index/lib/index-core.mjs`
14
+ runtime mirror. A build that crashes mid-write — or two builds racing in one repo — can no
15
+ longer leave a half-written artifact that poisons every subsequent index read; failed writes
16
+ clean their temp file up before rethrowing.
17
+
18
+ `ukit status` and `ukit memory` now read their cache/memory JSON through tolerant readers:
19
+ a corrupt `compact-history.json`, `compact-pressure.json`, `prompt-cache.json`,
20
+ `output-history.json`, or memory file prints a warning naming the exact file path and falls
21
+ back to defaults instead of crashing the whole command mid-report. `readJsonIfExists` itself
22
+ stays strict everywhere else, so genuinely bad callers still fail loudly.
23
+
24
+ Nested test layouts (`packages/widget/tests/unit/...`) are now recognized as tests by the
25
+ source path classifier and both runtime-mirror predicates — those files previously slipped
26
+ past test detection, broke archetype classification, and never appeared in the test map.
27
+
28
+ Each fix ships with regression coverage: a rename-spy test asserting all ten artifacts move
29
+ through temp files, corrupt-fixture tests asserting the warned paths and surviving output,
30
+ and a nested-`tests/` mapping/archetype test. Full suite: 77 files, 1,348 tests green.
31
+
5
32
  ## 2.3.4 - 2026-09-10
6
33
 
7
34
  Silent-stall audit and compact-recovery release. Two themes: every block reports a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.3.4",
3
+ "version": "2.3.5",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,15 @@ function defaultUserMemory() {
14
14
  };
15
15
  }
16
16
 
17
+ async function readMemoryJson(filePath) {
18
+ try {
19
+ return await readJsonIfExists(filePath);
20
+ } catch (error) {
21
+ console.warn(`[UKit] Ignoring corrupt memory JSON file ${filePath}: ${error?.message ?? String(error)}`);
22
+ return null;
23
+ }
24
+ }
25
+
17
26
  async function readDirectoryJsonItems(dirPath) {
18
27
  let entries = [];
19
28
  try {
@@ -29,7 +38,7 @@ async function readDirectoryJsonItems(dirPath) {
29
38
  }
30
39
 
31
40
  const fullPath = path.join(dirPath, entry.name);
32
- const content = await readJsonIfExists(fullPath);
41
+ const content = await readMemoryJson(fullPath);
33
42
  if (content) {
34
43
  items.push({
35
44
  fileName: entry.name,
@@ -91,7 +100,7 @@ function createSearchableText(type, content) {
91
100
 
92
101
  export async function exportMemory(projectRoot) {
93
102
  const runtimePaths = buildRuntimePaths(projectRoot);
94
- const user = (await readJsonIfExists(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
103
+ const user = (await readMemoryJson(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
95
104
  const projects = (await readDirectoryJsonItems(runtimePaths.projectsDir)).map((item) => item.content);
96
105
  const sessions = (await readDirectoryJsonItems(runtimePaths.sessionsDir)).map((item) => item.content);
97
106
 
@@ -100,7 +109,7 @@ export async function exportMemory(projectRoot) {
100
109
 
101
110
  export async function listMemoryItems(projectRoot) {
102
111
  const runtimePaths = buildRuntimePaths(projectRoot);
103
- const userMemory = (await readJsonIfExists(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
112
+ const userMemory = (await readMemoryJson(runtimePaths.userMemoryPath)) ?? defaultUserMemory();
104
113
  const projectMemories = await readDirectoryJsonItems(runtimePaths.projectsDir);
105
114
  const sessionMemories = await readDirectoryJsonItems(runtimePaths.sessionsDir);
106
115
 
@@ -188,7 +197,7 @@ function projectMemoryPath(runtimePaths, projectId) {
188
197
 
189
198
  async function readProjectMemoryForId(runtimePaths, projectId) {
190
199
  const filePath = projectMemoryPath(runtimePaths, projectId);
191
- const existing = await readJsonIfExists(filePath);
200
+ const existing = await readMemoryJson(filePath);
192
201
  const memory = {
193
202
  id: projectId,
194
203
  conventions: [],
@@ -204,7 +213,7 @@ async function appendSessionArchive(runtimePaths, projectId, archivedSessions) {
204
213
  }
205
214
 
206
215
  const archivePath = path.join(runtimePaths.projectsDir, `${sanitizeProjectId(projectId)}.archive.json`);
207
- const existing = (await readJsonIfExists(archivePath)) ?? { sessions: [] };
216
+ const existing = (await readMemoryJson(archivePath)) ?? { sessions: [] };
208
217
  const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
209
218
  await writeJson(archivePath, { sessions: [...sessions, ...archivedSessions] });
210
219
  }
@@ -5,7 +5,7 @@ import { loadRuntimeConfig } from './runtimeConfig.js';
5
5
  import { summarizeOutputHistory } from './output/index.js';
6
6
  import { buildPromptCacheStats } from './token/index.js';
7
7
  import { countMemoryItems } from './memory/store.js';
8
- import { readCompactPressureState } from './compact/threshold.js';
8
+ import { buildCompactPressureState } from './compact/threshold.js';
9
9
  import { detectProjectContext } from '../context/detectProjectContext.js';
10
10
 
11
11
  function formatPrimaryAgent(agentKey) {
@@ -122,6 +122,15 @@ function formatRecoveryLabel(count) {
122
122
  return ` / ${count} recover${count === 1 ? 'y' : 'ies'}`;
123
123
  }
124
124
 
125
+ async function readStatusJson(filePath) {
126
+ try {
127
+ return await readJsonIfExists(filePath);
128
+ } catch (error) {
129
+ console.warn(`[UKit] Ignoring corrupt status JSON file ${filePath}: ${error?.message ?? String(error)}`);
130
+ return null;
131
+ }
132
+ }
133
+
125
134
  export async function buildStatusReport(projectRoot) {
126
135
  const runtimePaths = buildRuntimePaths(projectRoot);
127
136
  const runtimeExists = await pathExists(runtimePaths.runtimeRoot);
@@ -131,11 +140,14 @@ export async function buildStatusReport(projectRoot) {
131
140
 
132
141
  const projectContext = await detectProjectContext(projectRoot);
133
142
  const config = await loadRuntimeConfig(projectRoot);
134
- const compactHistory = normalizeCompactEntries(await readJsonIfExists(runtimePaths.compactHistoryPath));
143
+ const compactHistory = normalizeCompactEntries(await readStatusJson(runtimePaths.compactHistoryPath));
135
144
  const compactSummary = buildCompactSummary(compactHistory);
136
- const compactPressure = await readCompactPressureState(projectRoot, config);
137
- const promptCacheStats = buildPromptCacheStats(await readJsonIfExists(runtimePaths.promptCachePath));
138
- const outputCompressionStats = summarizeOutputHistory(await readJsonIfExists(runtimePaths.outputHistoryPath));
145
+ const compactPressure = buildCompactPressureState(
146
+ await readStatusJson(runtimePaths.compactPressurePath),
147
+ config,
148
+ );
149
+ const promptCacheStats = buildPromptCacheStats(await readStatusJson(runtimePaths.promptCachePath));
150
+ const outputCompressionStats = summarizeOutputHistory(await readStatusJson(runtimePaths.outputHistoryPath));
139
151
  const memoryCounts = await countMemoryItems(projectRoot);
140
152
  const adapters = await detectAdapterLabels(projectRoot, config.agent);
141
153
 
@@ -1197,7 +1197,20 @@ function areSourceFingerprintsEqual(previousFingerprint, nextFingerprint) {
1197
1197
 
1198
1198
  async function writeArtifact(rootDir, artifactName, payload) {
1199
1199
  const artifactPath = getArtifactPath(rootDir, artifactName);
1200
- await fs.writeFile(artifactPath, JSON.stringify(payload, null, 2));
1200
+ const temporaryPath = `${artifactPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1201
+
1202
+ try {
1203
+ await fs.writeFile(temporaryPath, JSON.stringify(payload, null, 2));
1204
+ await fs.rename(temporaryPath, artifactPath);
1205
+ } catch (error) {
1206
+ try {
1207
+ await fs.rm(temporaryPath, { force: true });
1208
+ } catch {
1209
+ // ignore cleanup errors
1210
+ }
1211
+
1212
+ throw error;
1213
+ }
1201
1214
  }
1202
1215
 
1203
1216
  async function readArtifactIfExists(rootDir, artifactName) {
@@ -37,6 +37,7 @@ export function isLikelyTestFilePath(filePath) {
37
37
  || lower.startsWith('specs/')
38
38
  || lower.includes('/__tests__/')
39
39
  || lower.includes('/test/')
40
+ || lower.includes('/tests/')
40
41
  || lower.includes('/spec/')
41
42
  || lower.includes('/specs/')
42
43
  || lower.endsWith('.test.js')
@@ -2028,7 +2028,21 @@ function areBugIndexSnapshotsEqual(previousSnapshot, nextSnapshot) {
2028
2028
  }
2029
2029
 
2030
2030
  async function writeArtifact(rootDir, artifactName, payload) {
2031
- await fs.writeFile(getArtifactPath(rootDir, artifactName), JSON.stringify(payload, null, 2));
2031
+ const artifactPath = getArtifactPath(rootDir, artifactName);
2032
+ const temporaryPath = `${artifactPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
2033
+
2034
+ try {
2035
+ await fs.writeFile(temporaryPath, JSON.stringify(payload, null, 2));
2036
+ await fs.rename(temporaryPath, artifactPath);
2037
+ } catch (error) {
2038
+ try {
2039
+ await fs.rm(temporaryPath, { force: true });
2040
+ } catch {
2041
+ // ignore cleanup errors
2042
+ }
2043
+
2044
+ throw error;
2045
+ }
2032
2046
  }
2033
2047
 
2034
2048
  async function readArtifact(rootDir, artifactName) {
@@ -2557,6 +2571,7 @@ function isLikelyTestFilePath(filePath) {
2557
2571
  || lower.startsWith('specs/')
2558
2572
  || lower.includes('/__tests__/')
2559
2573
  || lower.includes('/test/')
2574
+ || lower.includes('/tests/')
2560
2575
  || lower.includes('/spec/')
2561
2576
  || lower.includes('/specs/')
2562
2577
  || lower.endsWith('.test.js')
@@ -2988,6 +3003,7 @@ function isLikelyTestFile(filePath) {
2988
3003
  || lower.startsWith('specs/')
2989
3004
  || lower.includes('/__tests__/')
2990
3005
  || lower.includes('/test/')
3006
+ || lower.includes('/tests/')
2991
3007
  || lower.includes('/spec/')
2992
3008
  || lower.includes('/specs/')
2993
3009
  || lower.endsWith('.test.js')