@ngockhoale/ukit 3.0.4 → 3.0.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 +9 -0
- package/package.json +1 -1
- package/src/cli/commands/memory.js +3 -6
- package/src/core/memory/userMemory.js +15 -0
- package/src/index/resolveContext.js +7 -4
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +18 -9
- package/template_project/.claude/ukit/index/lib/index-core.mjs +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
## 3.0.5 - 2026-09-24
|
|
7
|
+
|
|
8
|
+
- **Review-fix hardening (cycle C53 fix round 1)** — defects found by the release reviewer after 3.0.4 shipped:
|
|
9
|
+
- **Memory**: `appendCompactHistory` read-modify-write now runs under the store lock — concurrent appends can no longer lose entries (unlocked RMW race).
|
|
10
|
+
- **Installed mirror**: restored the missing `index-core` declaration in the installed `.claude/ukit/` mirror so dev-mirror parity checks pass on hydrated workspaces.
|
|
11
|
+
- **Shipped hooks**: `sensitive-data-guard` no longer bypassed by wrapper commands (`sudo`/`env`/`bash -c`/`xargs`) or basename-obfuscated invocations — the guard now unwraps wrappers and matches on the real binary name.
|
|
12
|
+
- **Test scoping**: `payloadDocs` stale-prose sweep restricted to shipped surfaces (root docs, `template_project`, `src`, `bin`, `scripts`) — it no longer walks `docs/AI_HANDOFF/**` reports or gitignored runtime dirs; `mirrorPayload` parity now tolerates stale/runtime files under the gitignored installed mirror.
|
|
13
|
+
- **Installed-mirror resync**: stale `index/detectProjectContext.js`, query-index cache, lock dirs, and resolve-context cache regenerated so the installed mirror matches the template payload.
|
|
14
|
+
|
|
6
15
|
## 3.0.4 - 2026-09-24
|
|
7
16
|
|
|
8
17
|
- **Full source audit (cycle C53) — ~86 defects fixed across all 14 audit areas**; every fix carries a RED→GREEN regression test under `tests/audit/`:
|
package/package.json
CHANGED
|
@@ -18,9 +18,8 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
addUserRecord,
|
|
20
20
|
getUserRecord,
|
|
21
|
-
loadUserRecords,
|
|
22
21
|
queryUserRecords,
|
|
23
|
-
|
|
22
|
+
removeUserRecord,
|
|
24
23
|
updateUserRecord,
|
|
25
24
|
userMemoryStats,
|
|
26
25
|
} from '../../core/memory/userMemory.js';
|
|
@@ -297,12 +296,10 @@ async function runUserMemory(homeDir, args) {
|
|
|
297
296
|
if (!id) {
|
|
298
297
|
throw new Error('Missing record id. Usage: ukit memory --user forget <id>');
|
|
299
298
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (remaining.length === records.length) {
|
|
299
|
+
const removed = await removeUserRecord(id, { homeDir });
|
|
300
|
+
if (!removed) {
|
|
303
301
|
throw new Error(`User memory record not found: ${id}`);
|
|
304
302
|
}
|
|
305
|
-
await saveUserRecords(remaining, { homeDir });
|
|
306
303
|
console.log(`[UKit] Forgot user record ${id}.`);
|
|
307
304
|
return;
|
|
308
305
|
}
|
|
@@ -63,6 +63,21 @@ export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* removeUserRecord(id, {homeDir}={}) → record | null
|
|
68
|
+
* Deletes the record with `id` under the shared store lock — a concurrent
|
|
69
|
+
* add/update cannot be clobbered by the removal. Returns the removed record,
|
|
70
|
+
* or null when the id is absent.
|
|
71
|
+
*/
|
|
72
|
+
export async function removeUserRecord(id, { homeDir } = {}) {
|
|
73
|
+
return mutateRecordStore(userRecordsPath(homeDir), (records) => {
|
|
74
|
+
const index = records.findIndex((r) => r.id === id);
|
|
75
|
+
if (index === -1) return null;
|
|
76
|
+
const removed = records[index];
|
|
77
|
+
return { result: removed, records: records.filter((r) => r.id !== id) };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
/**
|
|
67
82
|
* getUserRecord(id, {homeDir}={}) → record | null
|
|
68
83
|
*/
|
|
@@ -10,9 +10,9 @@ const TASK_TYPE_BUDGETS = {
|
|
|
10
10
|
|
|
11
11
|
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
12
12
|
const RISKY_SIGNALS = [
|
|
13
|
-
'auth', '
|
|
14
|
-
'
|
|
15
|
-
'core', 'shared', 'runtime',
|
|
13
|
+
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
14
|
+
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
15
|
+
'flakiness', 'intermittent', 'timeout', 'deadlock', 'core', 'shared', 'runtime',
|
|
16
16
|
];
|
|
17
17
|
const EXPANSION_SIGNALS = [
|
|
18
18
|
'similar',
|
|
@@ -39,12 +39,15 @@ const EXPANSION_SIGNALS = [
|
|
|
39
39
|
|
|
40
40
|
// Signals must match whole words: bare `includes` let 'score' fire 'core',
|
|
41
41
|
// 'author' fire 'auth', 'context' fire 'text', and 'install ' fire 'all '.
|
|
42
|
+
// Common inflections (plural/past/-ing) still count — 'timeouts', 'migrations',
|
|
43
|
+
// 'deadlocked' — but 'authentication'/'authorization'/'flakiness' need explicit
|
|
44
|
+
// entries since their stems diverge from the base signal.
|
|
42
45
|
function escapeSignalRegExp(signal) {
|
|
43
46
|
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
function containsSignalWord(lower, signals) {
|
|
47
|
-
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}
|
|
50
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}(?:s|es|ed|d|ing)?\\b`).test(lower));
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export async function resolveContext({
|
|
@@ -271,9 +271,25 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
271
271
|
if (t === undefined) break;
|
|
272
272
|
if (t === '--') { headIdx += 1; break; }
|
|
273
273
|
if (isAssignToken(t)) { headIdx += 1; continue; }
|
|
274
|
+
if (wrapperPositionalPending > 0 && !isVerbToken(t) && !isFlagToken(t)) {
|
|
275
|
+
// The wrapper's own operand (user/host/duration/image/service), then
|
|
276
|
+
// the next token is the real command. Consumed BEFORE the wrapper
|
|
277
|
+
// branch so a 0-positional wrapper token sitting in an operand slot
|
|
278
|
+
// (`exec` after `docker`/`kubectl`) cannot clobber the pending count.
|
|
279
|
+
// Flags are never operands — they fall through to the flag branch.
|
|
280
|
+
wrapperPositionalPending -= 1;
|
|
281
|
+
headIdx += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
274
284
|
const positionalCount = wrapperPositionals(t);
|
|
275
285
|
if (positionalCount !== undefined) {
|
|
276
286
|
wrapperPositionalPending = positionalCount;
|
|
287
|
+
// `docker compose <sub> <svc> <cmd>` carries one extra operand (the
|
|
288
|
+
// service) beyond the flat two — `compose` is a sub-CLI, not a
|
|
289
|
+
// subcommand like `exec`.
|
|
290
|
+
if (positionalCount === 2 && unquote(tokens[headIdx + 1]) === 'compose') {
|
|
291
|
+
wrapperPositionalPending += 1;
|
|
292
|
+
}
|
|
277
293
|
headIdx += 1;
|
|
278
294
|
continue;
|
|
279
295
|
}
|
|
@@ -291,18 +307,12 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
291
307
|
}
|
|
292
308
|
continue;
|
|
293
309
|
}
|
|
294
|
-
if (wrapperPositionalPending > 0 && !isVerbToken(t)) {
|
|
295
|
-
// The wrapper's own operand (user/host/duration/image), then the
|
|
296
|
-
// next token is the real command.
|
|
297
|
-
wrapperPositionalPending -= 1;
|
|
298
|
-
headIdx += 1;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
310
|
break;
|
|
302
311
|
}
|
|
303
312
|
const effectiveHead = unquote(tokens[headIdx] ?? '');
|
|
313
|
+
const headBase = effectiveHead.replace(/^.*\//, '');
|
|
304
314
|
|
|
305
|
-
if (DUMP_VERBS.has(
|
|
315
|
+
if (DUMP_VERBS.has(headBase)) {
|
|
306
316
|
for (const token of tokens.slice(headIdx + 1)) {
|
|
307
317
|
const cleaned = token.replace(/^["']+|["',:]+$/g, '');
|
|
308
318
|
const kind = classifySecretFile(cleaned);
|
|
@@ -312,7 +322,6 @@ async function evaluate({ rawInput, projectRoot, env }) {
|
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
const headBase = effectiveHead.replace(/^.*\//, '');
|
|
316
325
|
const CRED_USER_TOOLS = new Set(['curl', 'wget', 'ftp', 'lftp', 'aria2c', 'http', 'https']);
|
|
317
326
|
// `-uuser:pass`, `-u user:pass`, `--user=user:pass`, `--user user:pass` —
|
|
318
327
|
// the old `\s+` separator missed the joined and `=` forms entirely.
|
|
@@ -967,6 +967,7 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
967
967
|
const previousCodePaths = new Set(
|
|
968
968
|
filesArtifact.items.filter((item) => CODE_EXTENSIONS.has(item?.ext)).map((item) => item.filePath),
|
|
969
969
|
);
|
|
970
|
+
const currentCodePaths = new Set(codeFiles.map((file) => file.filePath));
|
|
970
971
|
const canReuseArchetypes = parsedFiles.length === 0
|
|
971
972
|
&& !recordsChanged
|
|
972
973
|
&& archetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
@@ -1358,11 +1359,10 @@ const TASK_TYPE_BUDGETS = {
|
|
|
1358
1359
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
1359
1360
|
};
|
|
1360
1361
|
|
|
1361
|
-
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
1362
1362
|
const RISKY_SIGNALS = [
|
|
1363
|
-
'auth', '
|
|
1364
|
-
'
|
|
1365
|
-
'core', 'shared', 'runtime',
|
|
1363
|
+
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
1364
|
+
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
1365
|
+
'flakiness', 'intermittent', 'timeout', 'deadlock', 'core', 'shared', 'runtime',
|
|
1366
1366
|
];
|
|
1367
1367
|
const EXPANSION_SIGNALS = [
|
|
1368
1368
|
'similar',
|
|
@@ -1389,12 +1389,15 @@ const EXPANSION_SIGNALS = [
|
|
|
1389
1389
|
|
|
1390
1390
|
// Signals must match whole words: bare `includes` let 'score' fire 'core',
|
|
1391
1391
|
// 'author' fire 'auth', 'context' fire 'text', and 'install ' fire 'all '.
|
|
1392
|
+
// Common inflections (plural/past/-ing) still count — 'timeouts', 'migrations',
|
|
1393
|
+
// 'deadlocked' — but 'authentication'/'authorization'/'flakiness' need explicit
|
|
1394
|
+
// entries since their stems diverge from the base signal.
|
|
1392
1395
|
function escapeSignalRegExp(signal) {
|
|
1393
1396
|
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1394
1397
|
}
|
|
1395
1398
|
|
|
1396
1399
|
function containsSignalWord(lower, signals) {
|
|
1397
|
-
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}
|
|
1400
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}(?:s|es|ed|d|ing)?\\b`).test(lower));
|
|
1398
1401
|
}
|
|
1399
1402
|
|
|
1400
1403
|
export async function resolveContext({
|