@ngockhoale/ukit 3.0.2 → 3.0.4
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 +21 -0
- package/bin/ukit +12 -4
- package/manifests/engineConformance.yaml +29 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +2 -1
- package/scripts/audit/decision-coverage.mjs +295 -0
- package/scripts/bench/outline-savings.mjs +19 -4
- package/scripts/bench/parallel-agents.mjs +15 -4
- package/scripts/bench/runGold.mjs +22 -4
- package/scripts/bench/v3-ceremony.mjs +7 -1
- package/scripts/bug/triage.mjs +56 -17
- package/scripts/index/build-index.mjs +94 -28
- package/scripts/index/query-index.mjs +48 -14
- package/scripts/index/refresh-index.mjs +142 -62
- package/scripts/perf/audit-perf.mjs +8 -2
- package/scripts/skill/audit-skill.mjs +54 -25
- package/src/bug/triageBug.js +9 -6
- package/src/cli/adapters.js +6 -0
- package/src/cli/commands/code.js +7 -1
- package/src/cli/commands/indexArgs.js +4 -2
- package/src/cli/commands/indexTools.js +8 -1
- package/src/cli/commands/install.js +13 -0
- package/src/cli/commands/memory.js +10 -3
- package/src/cli/commands/status.js +17 -1
- package/src/cli/commands/update.js +7 -0
- package/src/context/detectProjectContext.js +3 -1
- package/src/core/codeintel/analogy.js +1 -1
- package/src/core/codeintel/diagnostics.js +9 -6
- package/src/core/codeintel/graph.js +14 -8
- package/src/core/codeintel/impact.js +0 -1
- package/src/core/codeintel/invalidation.js +5 -3
- package/src/core/codeintel/packet.js +11 -0
- package/src/core/codeintel/router.js +17 -3
- package/src/core/codeintel/semanticProvider.js +1 -1
- package/src/core/codeintel/summaries.js +11 -7
- package/src/core/compact/contextBudget.js +26 -12
- package/src/core/compact/index.js +15 -8
- package/src/core/docContracts.js +10 -2
- package/src/core/experiments/deliberation.js +321 -0
- package/src/core/experiments/dynamicWorkflow.js +492 -0
- package/src/core/fileOps.js +8 -1
- package/src/core/gatewayProbe.js +29 -1
- package/src/core/gatewayResilienceEnv.js +44 -3
- package/src/core/handoffDocValidator.js +3 -1
- package/src/core/hookChainDoctor.js +16 -1
- package/src/core/memory/deltaOverlays.js +448 -0
- package/src/core/memory/learningCandidates.js +302 -0
- package/src/core/memory/migrate.js +59 -32
- package/src/core/memory/recordStore.js +24 -1
- package/src/core/memory/store.js +44 -8
- package/src/core/memory/storeV2.js +48 -33
- package/src/core/memory/userMemory.js +11 -10
- package/src/core/output/index.js +15 -10
- package/src/core/permissionPolicy.js +8 -0
- package/src/core/runtimeConfig.js +224 -4
- package/src/core/sensitiveValueScanner.js +10 -2
- package/src/core/taskBudgetValidator.js +12 -17
- package/src/core/taskProgressGuard.js +59 -9
- package/src/core/unattendedDoctor.js +5 -2
- package/src/core/uninstall.js +37 -8
- package/src/decision/client.js +371 -0
- package/src/decision/lease.js +198 -0
- package/src/decision/preflight.js +492 -0
- package/src/decision/protocol.js +308 -0
- package/src/decision/registry.js +384 -0
- package/src/decision/shadow.js +281 -0
- package/src/decision/statePacket.js +165 -0
- package/src/diagnostics/failurePatterns.js +2 -1
- package/src/diagnostics/ledgerFiles.js +3 -1
- package/src/diagnostics/routeOutcomes.js +1 -29
- package/src/index/buildIndex.js +23 -11
- package/src/index/impactContext.js +21 -2
- package/src/index/importResolution.js +13 -7
- package/src/index/queryIndex.js +11 -5
- package/src/index/resolveContext.js +16 -6
- package/src/index/taskRouting.js +63 -2
- package/src/index/verificationPlan.js +12 -1
- package/src/learning/patternProposals.js +6 -0
- package/src/render/renderTemplate.js +1 -1
- package/src/skill/auditSkill.js +3 -1
- package/src/stack/detectStack.js +3 -1
- package/template_project/.claude/agents/handoff-planner.md +2 -5
- package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
- package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
- package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
- package/template_project/.claude/hooks/handoff-resume.sh +10 -1
- package/template_project/.claude/hooks/protect-files.sh +0 -1
- package/template_project/.claude/hooks/record-execution.mjs +13 -1
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
- package/template_project/.claude/hooks/session-episode.sh +9 -2
- package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
- package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
- package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
- package/template_project/.claude/ukit/index/route-task.mjs +444 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
- package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
- package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
- package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
- package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
- package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
- package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
- package/template_project/ukit/storage/config.json +34 -1
- package/template_project/.claude/ukit/skill-router-state.json +0 -1
- package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
|
@@ -36,8 +36,10 @@ export function parseIndexArgs(argv, flagDefinitions) {
|
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
const [flag, valueFromEquals = null] =
|
|
39
|
+
const equalsIndex = arg.indexOf('=');
|
|
40
|
+
const [flag, valueFromEquals = null] = equalsIndex >= 0
|
|
41
|
+
? [arg.slice(0, equalsIndex), arg.slice(equalsIndex + 1)]
|
|
42
|
+
: [arg, null];
|
|
41
43
|
const definition = flagDefinitions.get(flag);
|
|
42
44
|
if (!definition) {
|
|
43
45
|
throw new Error(`Unknown option: ${flag}`);
|
|
@@ -192,7 +192,14 @@ export async function runIndexTools({ projectRoot, argv = [] }) {
|
|
|
192
192
|
const taskType = flags.get('--type') ?? null;
|
|
193
193
|
const commandText = flags.get('--tool-command') ?? '';
|
|
194
194
|
const lastExplicitUserPromptText = flags.get('--last-prompt') ?? '';
|
|
195
|
-
const
|
|
195
|
+
const rawAdapter = flags.get('--adapter') ?? 'claude';
|
|
196
|
+
// Same posture as the shipped route-task.mjs helper: unknown adapters warn
|
|
197
|
+
// and fall back to claude instead of silently picking a namespace.
|
|
198
|
+
const KNOWN_ROUTE_ADAPTERS = new Set(['claude', 'codex']);
|
|
199
|
+
const adapter = KNOWN_ROUTE_ADAPTERS.has(rawAdapter) ? rawAdapter : 'claude';
|
|
200
|
+
if (adapter !== rawAdapter) {
|
|
201
|
+
console.error(`[UKit] unknown --adapter "${rawAdapter}" — falling back to claude`);
|
|
202
|
+
}
|
|
196
203
|
const commandNamespace = adapter === 'codex' ? '.codex' : '.claude';
|
|
197
204
|
|
|
198
205
|
if (!promptText && !commandText && !targetFile) {
|
|
@@ -10,6 +10,7 @@ import { removeTrackedPathsFromMetadata } from '../../core/metadata.js';
|
|
|
10
10
|
import {
|
|
11
11
|
ADAPTER_BY_KEY,
|
|
12
12
|
DEFAULT_OPTIONAL_TOOL_KEYS,
|
|
13
|
+
TOOLS_FLAG_HELP,
|
|
13
14
|
parseToolsArg,
|
|
14
15
|
resolveOptionalToolKeys,
|
|
15
16
|
toSelectedAdapterItemIds,
|
|
@@ -245,6 +246,18 @@ export async function pruneDeselectedAdapters({
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
export async function runInstall({ packageRoot, projectRoot, packageVersion, argv = [] }) {
|
|
249
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
250
|
+
console.log('Usage: ukit install [options]');
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log('Install/update UKit assets into the current project (default command).');
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log('Options:');
|
|
255
|
+
console.log(` --tools=<list> Adapters to enable (default: all). Values: ${TOOLS_FLAG_HELP}`);
|
|
256
|
+
console.log(' --with-codegraph Enable CodeGraph MCP integration in CLAUDE.md');
|
|
257
|
+
console.log(' --help, -h Show this help message');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
248
261
|
// parseToolsArg owns only --tools and throws on anything else, so hand it the argv with
|
|
249
262
|
// the flags consumed here (--with-codegraph) already removed — order-dependent parsing
|
|
250
263
|
// used to make the documented `ukit install --with-codegraph` fail 100% of the time.
|
|
@@ -40,8 +40,14 @@ const HELP_FLAGS = new Set(['--help', '-h', 'help']);
|
|
|
40
40
|
function extractFlag(args, flag) {
|
|
41
41
|
const index = args.indexOf(flag);
|
|
42
42
|
if (index < 0) return { value: null, rest: args };
|
|
43
|
+
const value = args[index + 1];
|
|
44
|
+
// A missing value — or the next flag swallowed as the value — used to pass
|
|
45
|
+
// silently (e.g. `--project --category bug` set projectId='--category').
|
|
46
|
+
if (typeof value !== 'string' || value.startsWith('--')) {
|
|
47
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
48
|
+
}
|
|
43
49
|
return {
|
|
44
|
-
value
|
|
50
|
+
value,
|
|
45
51
|
rest: [...args.slice(0, index), ...args.slice(index + 2)],
|
|
46
52
|
};
|
|
47
53
|
}
|
|
@@ -437,8 +443,9 @@ function episodeText(ledger, sessionId) {
|
|
|
437
443
|
? 'not-run'
|
|
438
444
|
: ledger.verificationSucceeded === true ? 'ok' : 'fail';
|
|
439
445
|
const receipts = Array.isArray(ledger.receipts) ? ledger.receipts : [];
|
|
440
|
-
const
|
|
441
|
-
|
|
446
|
+
const lastReceipt = receipts.length > 0 ? receipts[receipts.length - 1] : null;
|
|
447
|
+
const lastCommand = lastReceipt
|
|
448
|
+
? String(lastReceipt?.command ?? '').split('\n')[0].trim() || 'n/a'
|
|
442
449
|
: 'n/a';
|
|
443
450
|
const text = `Session ${sessionId}: ${write}, verify=${verify}, `
|
|
444
451
|
+ `${receipts.length} receipts, last command ${lastCommand}`;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { buildStatusReport, formatStatusReport } from '../../core/status.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const KNOWN_STATUS_FLAGS = new Set(['--help', '-h']);
|
|
4
|
+
|
|
5
|
+
export async function runStatus({ projectRoot, argv = [] }) {
|
|
6
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
7
|
+
console.log('Usage: ukit status');
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log('Show UKit runtime status for the current project.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const unknownArgs = argv.filter((a) => !KNOWN_STATUS_FLAGS.has(a));
|
|
14
|
+
if (unknownArgs.length > 0) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Unknown option(s): ${unknownArgs.join(', ')}. Run \`ukit status --help\` for usage.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
const report = await buildStatusReport(projectRoot);
|
|
5
21
|
for (const line of formatStatusReport(report)) {
|
|
6
22
|
console.log(line);
|
|
@@ -20,6 +20,13 @@ export async function runUpdate({ packageVersion, argv = [], cwd = process.cwd()
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const positionalArgs = argv.filter((a) => !a.startsWith('-'));
|
|
24
|
+
if (positionalArgs.length > 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unexpected argument(s): ${positionalArgs.join(', ')}. Run \`ukit update --help\` for usage.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
console.log(`[UKit] Current version: ${packageVersion}`);
|
|
24
31
|
console.log(`[UKit] Running: npm install -g ${UKIT_PACKAGE_NAME}`);
|
|
25
32
|
|
|
@@ -12,7 +12,9 @@ function inferProjectName(projectRoot, packageJson) {
|
|
|
12
12
|
|
|
13
13
|
export async function detectProjectContext(projectRoot) {
|
|
14
14
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
15
|
-
|
|
15
|
+
// A corrupt package.json must degrade to basename naming, not crash the
|
|
16
|
+
// status/memory CLIs with a raw SyntaxError.
|
|
17
|
+
const packageJson = await readJsonIfExists(packageJsonPath).catch(() => null);
|
|
16
18
|
|
|
17
19
|
return {
|
|
18
20
|
project: {
|
|
@@ -190,7 +190,7 @@ export async function findAnalogies(projectRoot, query, { limit = DEFAULT_LIMIT
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
analogies.sort((a, b) => b.score - a.score || a.ref.localeCompare(b.ref));
|
|
193
|
-
if (analogies.length === 0 &&
|
|
193
|
+
if (analogies.length === 0 && (memory.records !== null || filesArtifact || symbolsArtifact)) {
|
|
194
194
|
omitted.push({ what: 'analogy-lane', why: 'no-match' });
|
|
195
195
|
}
|
|
196
196
|
return { analogies: analogies.slice(0, effectiveLimit), omitted };
|
|
@@ -12,15 +12,18 @@ const SYNTAX_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
|
12
12
|
const MAX_DIAGNOSTICS = 50;
|
|
13
13
|
|
|
14
14
|
function parseCheckLine(stderr) {
|
|
15
|
-
const
|
|
15
|
+
const lines = String(stderr ?? '')
|
|
16
16
|
.split('\n')
|
|
17
17
|
.map((l) => l.trim())
|
|
18
|
-
.
|
|
19
|
-
if (
|
|
20
|
-
|
|
18
|
+
.filter((l) => l.length > 0);
|
|
19
|
+
if (lines.length === 0) return { message: 'syntax check failed', line: null };
|
|
20
|
+
// `node --check` stderr opens with `<file>:<line>` then the offending source
|
|
21
|
+
// line and a caret; the real error is the first *Error line below that.
|
|
22
|
+
const errorLine = lines.find((l) => /\b\w*Error\b/.test(l));
|
|
23
|
+
const lineMatch = lines[0].match(/:(\d+)\s*$/);
|
|
21
24
|
return {
|
|
22
|
-
message:
|
|
23
|
-
line:
|
|
25
|
+
message: errorLine ?? lines[0],
|
|
26
|
+
line: lineMatch ? Number.parseInt(lineMatch[1], 10) : null,
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -96,8 +96,8 @@ export async function buildGraphArtifact(projectRoot) {
|
|
|
96
96
|
}
|
|
97
97
|
return id;
|
|
98
98
|
};
|
|
99
|
-
const addEdge = (edge
|
|
100
|
-
const key = `${edge.from}»${edge.to}»${edge.kind}
|
|
99
|
+
const addEdge = (edge) => {
|
|
100
|
+
const key = `${edge.from}»${edge.to}»${edge.kind}`;
|
|
101
101
|
if (!edges.has(key)) edges.set(key, edge);
|
|
102
102
|
};
|
|
103
103
|
|
|
@@ -132,16 +132,18 @@ export async function buildGraphArtifact(projectRoot) {
|
|
|
132
132
|
for (const name of call.calls ?? []) {
|
|
133
133
|
const target = symbolToFile.get(name);
|
|
134
134
|
if (!target || target === call.filePath) continue;
|
|
135
|
+
// The callee file is a graph node even when it has no calls.json row of
|
|
136
|
+
// its own — an edge must never point at a non-existent node.
|
|
137
|
+
addFileNode(target);
|
|
135
138
|
addEdge(createEdge({
|
|
136
139
|
from: call.filePath,
|
|
137
140
|
to: target,
|
|
138
141
|
kind: 'call',
|
|
139
142
|
provider: 'json-graph',
|
|
140
143
|
evidence: INDEX_ARTIFACTS.calls,
|
|
141
|
-
})
|
|
144
|
+
}));
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
|
-
|
|
145
147
|
// co-change edges: pair endpoints become file nodes; count feeds confidence.
|
|
146
148
|
for (const pair of cochangeArtifact?.pairs ?? []) {
|
|
147
149
|
if (!pair?.a || !pair?.b) continue;
|
|
@@ -155,7 +157,7 @@ export async function buildGraphArtifact(projectRoot) {
|
|
|
155
157
|
confidence,
|
|
156
158
|
provider: 'json-graph',
|
|
157
159
|
evidence: INDEX_ARTIFACTS.cochange,
|
|
158
|
-
})
|
|
160
|
+
}));
|
|
159
161
|
addEdge(createEdge({
|
|
160
162
|
from: pair.b,
|
|
161
163
|
to: pair.a,
|
|
@@ -163,7 +165,7 @@ export async function buildGraphArtifact(projectRoot) {
|
|
|
163
165
|
confidence,
|
|
164
166
|
provider: 'json-graph',
|
|
165
167
|
evidence: INDEX_ARTIFACTS.cochange,
|
|
166
|
-
})
|
|
168
|
+
}));
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
const artifact = {
|
|
@@ -205,11 +207,15 @@ export class JsonGraphStore {
|
|
|
205
207
|
capabilities() {
|
|
206
208
|
return { nodes: true, edges: true, queries: true };
|
|
207
209
|
}
|
|
208
|
-
|
|
209
210
|
async _load() {
|
|
210
211
|
if (!this._artifactPromise) {
|
|
211
212
|
this._artifactPromise = (async () => {
|
|
212
|
-
|
|
213
|
+
let artifact = null;
|
|
214
|
+
try {
|
|
215
|
+
artifact = await readJsonIfExists(getArtifactPath(this.rootDir, INDEX_ARTIFACTS.codegraph));
|
|
216
|
+
} catch {
|
|
217
|
+
return null; // corrupt JSON → empty results, never throws
|
|
218
|
+
}
|
|
213
219
|
if (!artifact || typeof artifact !== 'object') return null;
|
|
214
220
|
if (artifact.schemaVersion !== GRAPH_SCHEMA_VERSION) return null;
|
|
215
221
|
if (!Array.isArray(artifact.nodes) || !Array.isArray(artifact.edges)) return null;
|
|
@@ -98,7 +98,6 @@ export async function impactSet(projectRoot, seeds, {
|
|
|
98
98
|
const nodeCap = clampInt(maxNodes ?? defaults.maxNodes, 1, Number.MAX_SAFE_INTEGER);
|
|
99
99
|
const requestedDepth = typeof depth === 'number' ? depth : defaults.defaultDepth;
|
|
100
100
|
const depthLimit = clampInt(requestedDepth, 1, maxDepthCap);
|
|
101
|
-
const depthClamped = requestedDepth > depthLimit;
|
|
102
101
|
|
|
103
102
|
const seedList = Array.isArray(seeds) ? seeds.filter((s) => typeof s === 'string' && s.length > 0) : [];
|
|
104
103
|
if (seedList.length === 0) {
|
|
@@ -140,15 +140,17 @@ export async function readDirty(projectRoot) {
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Return the accumulated dirty set and clear the file (index-refresh lane).
|
|
143
|
-
*
|
|
143
|
+
* `saturated` is surfaced so callers can treat a saturated set as full
|
|
144
|
+
* invalidation — a saturated drain with few/no paths is NOT a clean state.
|
|
145
|
+
* @returns {Promise<{ paths: string[], saturated: boolean }>}
|
|
144
146
|
*/
|
|
145
147
|
export async function drainDirty(projectRoot) {
|
|
146
148
|
const { paths, saturated } = await readDirtyData(projectRoot);
|
|
147
|
-
if (paths.length === 0 && !saturated) return [];
|
|
149
|
+
if (paths.length === 0 && !saturated) return { paths: [], saturated: false };
|
|
148
150
|
try {
|
|
149
151
|
await writeDirtyData(projectRoot, [], false);
|
|
150
152
|
} catch {
|
|
151
153
|
// never throw — drained set is still returned
|
|
152
154
|
}
|
|
153
|
-
return [...paths].sort();
|
|
155
|
+
return { paths: [...paths].sort(), saturated };
|
|
154
156
|
}
|
|
@@ -125,6 +125,17 @@ export function packetToText(packet) {
|
|
|
125
125
|
for (const t of packet.tests ?? []) lines.push(`- ${t}`);
|
|
126
126
|
if ((packet.tests ?? []).length === 0) lines.push('(none)');
|
|
127
127
|
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push('## Memory');
|
|
130
|
+
for (const m of packet.memory ?? []) {
|
|
131
|
+
if (m && typeof m === 'object') {
|
|
132
|
+
lines.push(`- ${m.id ?? m.ref ?? ''}${typeof m.score === 'number' ? ` (score ${m.score})` : ''}`);
|
|
133
|
+
} else {
|
|
134
|
+
lines.push(`- ${m}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if ((packet.memory ?? []).length === 0) lines.push('(none)');
|
|
138
|
+
|
|
128
139
|
lines.push('');
|
|
129
140
|
lines.push('## Omitted');
|
|
130
141
|
for (const o of packet.omitted ?? []) lines.push(`- ${o.what ?? ''} — ${o.why ?? ''}`);
|
|
@@ -36,7 +36,21 @@ const ERROR_RE = /\b(error|exception|stack ?trace|bug|crash|failing|fails|failur
|
|
|
36
36
|
const EXPLORE_RE = /\b(where|how does|how do|find|explore|understand|what does|which file|locate|handled)\b/i;
|
|
37
37
|
const DEEP_FLOW_RE = /\b(refactor|migrate|migration|flow|end[- ]?to[- ]?end|trace|across|pipeline|lifecycle)\b/i;
|
|
38
38
|
const ANALOGY_RE = /\b(similar to|analogy|like\b|same as|pattern|equivalent)\b/i;
|
|
39
|
-
const PATH_RE = /(?:[\w.-]+\/)+[\w.-]+|[\w.-]+\.(?:js|ts|mjs|cjs|jsx|tsx|py|rb|go|rs|java|md|json|yml|yaml|css|html|sh)\b/
|
|
39
|
+
const PATH_RE = /(?:[\w.-]+\/)+[\w.-]+|[\w.-]+\.(?:js|ts|mjs|cjs|jsx|tsx|py|rb|go|rs|java|md|json|yml|yaml|css|html|sh)\b/gi;
|
|
40
|
+
|
|
41
|
+
// English slash-words that look like paths to PATH_RE but never are —
|
|
42
|
+
// "and/or", "n/a" etc. must not trigger the explicit-target rule.
|
|
43
|
+
const SLASH_WORD_STOPLIST = new Set([
|
|
44
|
+
'and/or', 'n/a', 'yes/no', 'true/false', 'on/off', 'either/or', 'w/o', 'w/', 'b/c',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function hasPathMention(prompt) {
|
|
48
|
+
const text = String(prompt ?? '');
|
|
49
|
+
for (const match of text.matchAll(PATH_RE)) {
|
|
50
|
+
if (!SLASH_WORD_STOPLIST.has(match[0].toLowerCase())) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
40
54
|
|
|
41
55
|
function resolveBudgets(options) {
|
|
42
56
|
const configured = options?.config?.codeIntel?.budgets ?? options?.budgets;
|
|
@@ -63,7 +77,7 @@ function decide(prompt, { hasError = false, filesHinted } = {}) {
|
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
// Rule 3 — explicit path/symbol mention, clear scope, no error.
|
|
66
|
-
const hasExplicitTarget = hintedFiles.length > 0 ||
|
|
80
|
+
const hasExplicitTarget = hintedFiles.length > 0 || hasPathMention(prompt);
|
|
67
81
|
const errorSignalled = hasError || ERROR_RE.test(prompt);
|
|
68
82
|
if (!errorSignalled && hasExplicitTarget) {
|
|
69
83
|
reasons.push('explicit path or symbol mention with clear scope');
|
|
@@ -95,7 +109,7 @@ function decide(prompt, { hasError = false, filesHinted } = {}) {
|
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
// Rule 8 — fallback: single-file verb (single hinted file or lone path mention).
|
|
98
|
-
if (hintedFiles.length === 1 ||
|
|
112
|
+
if (hintedFiles.length === 1 || hasPathMention(prompt)) {
|
|
99
113
|
reasons.push('fallback: single-file hint');
|
|
100
114
|
return { mode: 'peek', reasons };
|
|
101
115
|
}
|
|
@@ -87,7 +87,7 @@ export class TypeScriptSemanticProvider {
|
|
|
87
87
|
try {
|
|
88
88
|
const filesPath = getArtifactPath(this.projectRoot, INDEX_ARTIFACTS.files);
|
|
89
89
|
const raw = JSON.parse(fs.readFileSync(filesPath, 'utf8'));
|
|
90
|
-
const list = Array.isArray(raw) ? raw : (raw.files ?? []);
|
|
90
|
+
const list = Array.isArray(raw) ? raw : (raw.items ?? raw.files ?? []);
|
|
91
91
|
return list
|
|
92
92
|
.map((entry) => (typeof entry === 'string' ? entry : entry?.filePath ?? entry?.path))
|
|
93
93
|
.filter(isSemanticFile)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { getSyntaxProvider } from './providers.js';
|
|
4
|
+
import { getSyntaxProvider, IndexFileSyntaxProvider } from './providers.js';
|
|
5
5
|
import { resolveProjectRelativePath } from '../fileOps.js';
|
|
6
6
|
|
|
7
7
|
// Deterministic extractive summaries (SPEC §7/§8, CI-304) — the L3 detail tier.
|
|
@@ -118,12 +118,16 @@ function scanDeclarations(lines, maxSymbols) {
|
|
|
118
118
|
|
|
119
119
|
async function symbolNames(rootDir, relPath, lines, maxSymbols) {
|
|
120
120
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
121
|
+
let provider = getSyntaxProvider(relPath);
|
|
122
|
+
// The registry's built-in 'index-file' provider is bound to process.cwd()
|
|
123
|
+
// at ensureBuiltins() time — rebind it to this call's rootDir so summaries
|
|
124
|
+
// read the project index even when cwd differs (or supports() failed).
|
|
125
|
+
if (!provider || provider.name === 'index-file') {
|
|
126
|
+
provider = new IndexFileSyntaxProvider({ rootDir });
|
|
127
|
+
}
|
|
128
|
+
const syms = await provider.symbols(relPath);
|
|
129
|
+
if (Array.isArray(syms) && syms.length > 0) {
|
|
130
|
+
return syms.slice(0, maxSymbols).map((s) => s.name).filter(Boolean);
|
|
127
131
|
}
|
|
128
132
|
} catch {
|
|
129
133
|
// provider failure falls through to the regex lane — never throws
|
|
@@ -27,6 +27,31 @@ export function deriveAutoCompactWindow(hardCapTokens, ratio) {
|
|
|
27
27
|
return Math.round(hardCapTokens * safeRatio);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// A malformed hard cap must fall back to a safe ceiling, never collapse to a
|
|
31
|
+
// non-positive or fractional cap that would brick every Edit/Write/Bash call or
|
|
32
|
+
// invert the autoCompactWindow < hardCapTokens invariant — same operator
|
|
33
|
+
// contract as positiveInteger() in compact/threshold.js.
|
|
34
|
+
function positiveInteger(value, fallback) {
|
|
35
|
+
return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0
|
|
36
|
+
? value
|
|
37
|
+
: fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pure normalization of a raw `compact` config object into the budget triple.
|
|
41
|
+
// Exported so the fallback rules are testable without touching the shipped file.
|
|
42
|
+
export function deriveCompactBudget(compact = {}) {
|
|
43
|
+
const source = compact && typeof compact === 'object' ? compact : {};
|
|
44
|
+
const tokenThreshold = Number.isFinite(source.tokenThreshold)
|
|
45
|
+
? source.tokenThreshold
|
|
46
|
+
: EMERGENCY_BUDGET.tokenThreshold;
|
|
47
|
+
const hardCapTokens = positiveInteger(source.hardCapTokens, EMERGENCY_BUDGET.hardCapTokens);
|
|
48
|
+
return {
|
|
49
|
+
tokenThreshold,
|
|
50
|
+
hardCapTokens,
|
|
51
|
+
autoCompactWindow: deriveAutoCompactWindow(hardCapTokens, source.autoCompactWindowRatio),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
let cachedBudget = null;
|
|
31
56
|
|
|
32
57
|
export function loadShippedCompactBudget() {
|
|
@@ -41,17 +66,6 @@ export function loadShippedCompactBudget() {
|
|
|
41
66
|
compact = EMERGENCY_BUDGET;
|
|
42
67
|
}
|
|
43
68
|
|
|
44
|
-
|
|
45
|
-
? compact.tokenThreshold
|
|
46
|
-
: EMERGENCY_BUDGET.tokenThreshold;
|
|
47
|
-
const hardCapTokens = Number.isFinite(compact.hardCapTokens)
|
|
48
|
-
? compact.hardCapTokens
|
|
49
|
-
: EMERGENCY_BUDGET.hardCapTokens;
|
|
50
|
-
|
|
51
|
-
cachedBudget = {
|
|
52
|
-
tokenThreshold,
|
|
53
|
-
hardCapTokens,
|
|
54
|
-
autoCompactWindow: deriveAutoCompactWindow(hardCapTokens, compact.autoCompactWindowRatio),
|
|
55
|
-
};
|
|
69
|
+
cachedBudget = deriveCompactBudget(compact);
|
|
56
70
|
return cachedBudget;
|
|
57
71
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
1
|
+
import { readJsonIfExists, writeJson, withFileLock } from '../fileOps.js';
|
|
2
2
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
3
3
|
import { compressLine, compressMarkdownLines, estimateTokenCount, normalizeLineForDedupe, uniqueLines } from '../token/index.js';
|
|
4
4
|
|
|
@@ -183,7 +183,10 @@ function buildCompactedContextLines(
|
|
|
183
183
|
const forceLine = nonEmptyCount < forceFirstCount;
|
|
184
184
|
const wouldExceedLines = nonEmptyCount >= maxLines;
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
// Forced lines (header + anchors) are guaranteed placement: they bypass the
|
|
187
|
+
// line cap — dropping a forced anchor would silently break the anchor
|
|
188
|
+
// contract this function exists to enforce.
|
|
189
|
+
if ((!forceLine && wouldExceedLines) || (!forceLine && selected.length > 0 && (usedTokens + tokens) > maxTokens)) {
|
|
187
190
|
break;
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -247,12 +250,16 @@ export async function appendCompactHistory(
|
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// Same read-modify-write race as compact-pressure.json: concurrent hook
|
|
254
|
+
// processes appending history must not drop each other's entries.
|
|
255
|
+
return withFileLock(runtimePaths.compactHistoryPath, async () => {
|
|
256
|
+
const history = await readCompactHistory(projectRoot, { maxEntries });
|
|
257
|
+
const nextDocument = normalizeCompactHistoryDocument({
|
|
258
|
+
entries: [normalizedEntry, ...history.entries],
|
|
259
|
+
}, { maxEntries });
|
|
260
|
+
await writeJson(runtimePaths.compactHistoryPath, nextDocument);
|
|
261
|
+
return nextDocument;
|
|
262
|
+
});
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
export async function recordCompaction(
|
package/src/core/docContracts.js
CHANGED
|
@@ -367,6 +367,14 @@ async function checkCommands(rootDir, registry) {
|
|
|
367
367
|
return makeCheck(id, label, 'error', { passed: true });
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// Logical line count: a trailing newline terminates the last line, it does not
|
|
371
|
+
// start a phantom extra one — 'a\n' is 1 line, not 2.
|
|
372
|
+
function countLines(content) {
|
|
373
|
+
if (!content) return 0;
|
|
374
|
+
const n = content.split('\n').length;
|
|
375
|
+
return content.endsWith('\n') ? n - 1 : n;
|
|
376
|
+
}
|
|
377
|
+
|
|
370
378
|
async function checkBudgets(rootDir, registry) {
|
|
371
379
|
const id = 'budgets';
|
|
372
380
|
const label = 'registry line budgets obeyed (error enforcement blocks)';
|
|
@@ -377,7 +385,7 @@ async function checkBudgets(rootDir, registry) {
|
|
|
377
385
|
if (!Number.isInteger(e.budget.max_lines)) continue;
|
|
378
386
|
const content = await readText(path.join(rootDir, e.path));
|
|
379
387
|
if (content === null) continue;
|
|
380
|
-
const lines = content
|
|
388
|
+
const lines = countLines(content);
|
|
381
389
|
if (lines <= e.budget.max_lines) continue;
|
|
382
390
|
const msg = `${e.path}: ${lines} lines > max_lines ${e.budget.max_lines}`;
|
|
383
391
|
if (e.budget.enforcement === 'error') errorViolations.push(msg);
|
|
@@ -531,7 +539,7 @@ async function checkArchiveSuggestions(rootDir, registry, nowMs) {
|
|
|
531
539
|
const signals = [];
|
|
532
540
|
if ((inbound.get(e.path)?.size ?? 0) === 0) signals.push('unreferenced');
|
|
533
541
|
if (e.budget && Number.isInteger(e.budget.max_lines)) {
|
|
534
|
-
const lines = content
|
|
542
|
+
const lines = countLines(content);
|
|
535
543
|
if (lines > e.budget.max_lines) signals.push('over-budget');
|
|
536
544
|
}
|
|
537
545
|
let st = null;
|