@ngockhoale/ukit 3.0.3 → 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 +17 -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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadow.js (TASK-004 / M07)
|
|
3
|
+
*
|
|
4
|
+
* Shadow evaluator for the UNIC Decision Agent (SPEC §5 FR-008/FR-009,
|
|
5
|
+
* UNIC_DECISION_SPEC §15). Runs the atomic preflight batch through the
|
|
6
|
+
* decision client, compares answers against the deterministic baseline
|
|
7
|
+
* (decision-table-v2 via buildFallbackPreflight), and emits a REDACTED
|
|
8
|
+
* receipt — decision keys, probability bands, outcome/latency classes,
|
|
9
|
+
* fallback codes, agreement counts. The receipt never carries state text,
|
|
10
|
+
* prompts, source, or secrets.
|
|
11
|
+
*
|
|
12
|
+
* Stage contract (decisionPlane.stage, absence = 'off'):
|
|
13
|
+
* off → returns null before any client/transport work (zero cost)
|
|
14
|
+
* shadow → receipt only; deterministic policy stays authoritative
|
|
15
|
+
* canary/default → receipt + validated preflight bundle; blocked/invalid
|
|
16
|
+
* answers degrade to the deterministic fallback bundle
|
|
17
|
+
* `decisionPlane.enabled === false` is the global emergency disable → null.
|
|
18
|
+
*
|
|
19
|
+
* The evaluator never throws: transport outages, malformed responses, and
|
|
20
|
+
* sensitive-state blocks all surface as typed receipt outcomes so the caller
|
|
21
|
+
* can fall back to the deterministic route with one bounded diagnostic.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { resolveConfigStage } from '../core/runtimeConfig.js';
|
|
25
|
+
import { createDecisionClient, resolveCheckpoint } from './client.js';
|
|
26
|
+
import { classifyLanguage } from './protocol.js';
|
|
27
|
+
import { serializeStatePacket } from './statePacket.js';
|
|
28
|
+
import {
|
|
29
|
+
PREFLIGHT_DECISION_KEYS,
|
|
30
|
+
buildPreflightBatch,
|
|
31
|
+
validatePreflightAnswers,
|
|
32
|
+
applyPreflight,
|
|
33
|
+
buildFallbackPreflight,
|
|
34
|
+
} from './preflight.js';
|
|
35
|
+
|
|
36
|
+
function decisionPlane(config) {
|
|
37
|
+
const dp = config?.decisionPlane ?? config ?? {};
|
|
38
|
+
return dp && typeof dp === 'object' ? dp : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Confidence → band. Receipts carry bands, never raw volatile probabilities.
|
|
42
|
+
function probabilityBand(confidence) {
|
|
43
|
+
if (!Number.isFinite(confidence)) return 'unknown';
|
|
44
|
+
if (confidence < 0.25) return 'low';
|
|
45
|
+
if (confidence < 0.5) return 'moderate';
|
|
46
|
+
if (confidence < 0.75) return 'high';
|
|
47
|
+
return 'very-high';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize an agent answer into the same units as the deterministic baseline
|
|
51
|
+
// bundle field it is compared against (score → legend label, capability set
|
|
52
|
+
// label → member list, lease label → integer).
|
|
53
|
+
function answerBaselineValue(answer, candidateMap) {
|
|
54
|
+
switch (answer.decisionKey) {
|
|
55
|
+
case 'preflight.rigor.v1': {
|
|
56
|
+
const legend = candidateMap?.rigor ?? [];
|
|
57
|
+
return legend[answer.value - 1] ?? null;
|
|
58
|
+
}
|
|
59
|
+
case 'preflight.capabilities.v1':
|
|
60
|
+
return candidateMap?.capabilities?.[answer.value] ?? null;
|
|
61
|
+
case 'preflight.lease.v1':
|
|
62
|
+
return Number(answer.value);
|
|
63
|
+
default:
|
|
64
|
+
return answer.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baselineValue(decisionKey, baseline) {
|
|
69
|
+
switch (decisionKey) {
|
|
70
|
+
case 'preflight.execution-lane.v1': return baseline.executionLane;
|
|
71
|
+
case 'preflight.model-role.v1': return baseline.executionModelRole;
|
|
72
|
+
case 'preflight.reasoning-effort.v1': return baseline.reasoningEffort;
|
|
73
|
+
case 'preflight.rigor.v1':
|
|
74
|
+
return typeof baseline.rigor === 'string' ? baseline.rigor.toLowerCase() : baseline.rigor;
|
|
75
|
+
case 'preflight.capabilities.v1': return baseline.activeCapabilities ?? [];
|
|
76
|
+
case 'preflight.verification-depth.v1': return baseline.verificationDepth;
|
|
77
|
+
case 'preflight.lease.v1': return baseline.lease?.granted ?? null;
|
|
78
|
+
default: return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function valuesEqual(a, b) {
|
|
83
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
84
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
85
|
+
const sa = [...a].map(String).sort();
|
|
86
|
+
const sb = [...b].map(String).sort();
|
|
87
|
+
return sa.every((v, i) => v === sb[i]);
|
|
88
|
+
}
|
|
89
|
+
return a === b;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Per-key agreement of the agent answers with the deterministic baseline.
|
|
93
|
+
// Invalid/missing answers count as 'no-answer', never as disagreement.
|
|
94
|
+
// Only keys actually asked in the batch are scored — buildPreflightBatch drops
|
|
95
|
+
// undecidable questions, and counting them as no-answer would skew the
|
|
96
|
+
// agreement signal with questions the agent never saw.
|
|
97
|
+
function computeAgreement(answers, baseline, candidateMap, askedKeys = PREFLIGHT_DECISION_KEYS) {
|
|
98
|
+
const agreement = { agree: 0, differ: 0, noAnswer: 0, differingKeys: [] };
|
|
99
|
+
const byKey = new Map(
|
|
100
|
+
(Array.isArray(answers) ? answers : [])
|
|
101
|
+
.filter((a) => a && typeof a.decisionKey === 'string')
|
|
102
|
+
.map((a) => [a.decisionKey, a]),
|
|
103
|
+
);
|
|
104
|
+
for (const key of askedKeys) {
|
|
105
|
+
const answer = byKey.get(key);
|
|
106
|
+
if (!answer || answer.validationStatus !== 'valid') {
|
|
107
|
+
agreement.noAnswer += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const expected = baselineValue(key, baseline);
|
|
111
|
+
const actual = answerBaselineValue(answer, candidateMap);
|
|
112
|
+
if (valuesEqual(actual, expected)) {
|
|
113
|
+
agreement.agree += 1;
|
|
114
|
+
} else {
|
|
115
|
+
agreement.differ += 1;
|
|
116
|
+
agreement.differingKeys.push(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return agreement;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The evaluator consumes the resolved-route (C01) shape. A legacy summary
|
|
123
|
+
// (routeSchema stage off) lacks the execution/intent groups — normalize the
|
|
124
|
+
// minimal fields so the batch still builds deterministic candidates.
|
|
125
|
+
function normalizeRoute(route) {
|
|
126
|
+
if (route?.execution && typeof route.execution === 'object') return route;
|
|
127
|
+
return {
|
|
128
|
+
intent: { kind: route?.intent?.kind ?? null },
|
|
129
|
+
execution: {
|
|
130
|
+
mode: route?.executionMode ?? null,
|
|
131
|
+
rigor: route?.execution?.rigor ?? null,
|
|
132
|
+
riskFloor: route?.riskFloor?.floor ?? null,
|
|
133
|
+
modelTier: route?.execution?.modelTier ?? null,
|
|
134
|
+
},
|
|
135
|
+
capabilityPolicy: route?.capabilityPolicy ?? {},
|
|
136
|
+
evidence: route?.evidence ?? {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run the decision-plane shadow evaluation for one route.
|
|
142
|
+
*
|
|
143
|
+
* @param {{route?: object, config?: object, transport?: Function,
|
|
144
|
+
* hostCapabilities?: object, env?: object, projectRoot?: string,
|
|
145
|
+
* homeDir?: string, now?: Function, deadlineMs?: number}} args
|
|
146
|
+
* transport: fetch-compatible injection point (tests); defaults to fetch.
|
|
147
|
+
* hostCapabilities: host capability map; defaults to
|
|
148
|
+
* config.decisionPlane.hostCapabilities ({} when undeclared).
|
|
149
|
+
* @returns {Promise<null|{receipt: object, preflight: object|null}>}
|
|
150
|
+
* null at stage 'off'/disabled; otherwise the redacted receipt plus the
|
|
151
|
+
* applied/fallback preflight bundle for canary+ stages (null at shadow).
|
|
152
|
+
*/
|
|
153
|
+
export async function runShadowDecisions({
|
|
154
|
+
route = {},
|
|
155
|
+
config = {},
|
|
156
|
+
transport,
|
|
157
|
+
hostCapabilities = null,
|
|
158
|
+
env,
|
|
159
|
+
projectRoot,
|
|
160
|
+
homeDir,
|
|
161
|
+
now,
|
|
162
|
+
deadlineMs,
|
|
163
|
+
} = {}) {
|
|
164
|
+
const dp = decisionPlane(config);
|
|
165
|
+
const stage = resolveConfigStage({ decisionPlane: dp }, 'decisionPlane.stage');
|
|
166
|
+
// Zero-cost exits first: no client construction, no transport, no batch.
|
|
167
|
+
if (stage === 'off' || dp.enabled === false) return null;
|
|
168
|
+
|
|
169
|
+
const hosts = hostCapabilities ?? dp.hostCapabilities ?? {};
|
|
170
|
+
const normalizedRoute = normalizeRoute(route);
|
|
171
|
+
|
|
172
|
+
const { batch, candidateMap } = buildPreflightBatch({
|
|
173
|
+
route: normalizedRoute,
|
|
174
|
+
hostCapabilities: hosts,
|
|
175
|
+
config,
|
|
176
|
+
});
|
|
177
|
+
// Reclassify over the same input the client uses (state + question text) so
|
|
178
|
+
// the batch record's checkpoint matches the one the client resolves —
|
|
179
|
+
// classifying state-only can mislabel a multilingual batch as english.
|
|
180
|
+
const questionText = (batch.questions ?? [])
|
|
181
|
+
.map((q) => `${q.decisionKey} ${q.instruction} ${(q.candidates ?? []).join(' ')}`)
|
|
182
|
+
.join(' ');
|
|
183
|
+
const languageClass = classifyLanguage(`${serializeStatePacket(batch.statePacket)} ${questionText}`);
|
|
184
|
+
batch.languageClass = languageClass;
|
|
185
|
+
batch.checkpoint = resolveCheckpoint(languageClass, config);
|
|
186
|
+
if (Number.isFinite(deadlineMs)) batch.deadlineMs = deadlineMs;
|
|
187
|
+
|
|
188
|
+
const baseline = buildFallbackPreflight({
|
|
189
|
+
route: normalizedRoute,
|
|
190
|
+
hostCapabilities: hosts,
|
|
191
|
+
config,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const client = createDecisionClient({
|
|
195
|
+
config,
|
|
196
|
+
transport,
|
|
197
|
+
now,
|
|
198
|
+
projectRoot,
|
|
199
|
+
homeDir,
|
|
200
|
+
env,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = await client.requestBatch(batch);
|
|
206
|
+
} catch {
|
|
207
|
+
// The client never throws for transport-level failures; anything that
|
|
208
|
+
// still escapes is an internal fault — same bounded fallback shape.
|
|
209
|
+
result = { status: 'unavailable', fallbackCode: 'shadow-error', answers: [] };
|
|
210
|
+
}
|
|
211
|
+
const answers = Array.isArray(result?.answers) ? result.answers : [];
|
|
212
|
+
|
|
213
|
+
const probabilityBands = {};
|
|
214
|
+
for (const answer of answers) {
|
|
215
|
+
// Only canonical keys land in the receipt — model-authored unknown
|
|
216
|
+
// tool-call names must never become receipt keys (they ride
|
|
217
|
+
// routeSummary.decisionPlane into printed diagnostics).
|
|
218
|
+
if (typeof answer?.decisionKey === 'string' && PREFLIGHT_DECISION_KEYS.includes(answer.decisionKey)) {
|
|
219
|
+
probabilityBands[answer.decisionKey] = probabilityBand(answer.confidence);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const key of PREFLIGHT_DECISION_KEYS) {
|
|
223
|
+
if (!(key in probabilityBands)) probabilityBands[key] = 'unknown';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const okOutcomes = new Set(['accepted', 'partial', 'abstained']);
|
|
227
|
+
const outcomeClass = result?.status ?? 'unavailable';
|
|
228
|
+
const receipt = {
|
|
229
|
+
receiptVersion: 1,
|
|
230
|
+
stage,
|
|
231
|
+
batchId: result?.batchId ?? batch.batchId,
|
|
232
|
+
decisionKeys: [...PREFLIGHT_DECISION_KEYS],
|
|
233
|
+
probabilityBands,
|
|
234
|
+
outcomeClass,
|
|
235
|
+
checkpoint: result?.checkpoint ?? batch.checkpoint ?? null,
|
|
236
|
+
latencyClass: result?.latencyClass ?? 'unknown',
|
|
237
|
+
fallbackCode: result?.fallbackCode ?? (okOutcomes.has(outcomeClass) ? null : outcomeClass),
|
|
238
|
+
agreement: computeAgreement(
|
|
239
|
+
answers,
|
|
240
|
+
baseline,
|
|
241
|
+
candidateMap,
|
|
242
|
+
(batch.questions ?? [])
|
|
243
|
+
.map((q) => q?.decisionKey)
|
|
244
|
+
.filter((k) => typeof k === 'string'),
|
|
245
|
+
),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// canary/default: validate the answer set cross-field and apply the bundle
|
|
249
|
+
// atomically. A blocked bundle is never half-applied — the deterministic
|
|
250
|
+
// fallback bundle (decision-table-v2) takes its place.
|
|
251
|
+
let preflight = null;
|
|
252
|
+
if (stage === 'canary' || stage === 'default') {
|
|
253
|
+
if (outcomeClass === 'accepted' || outcomeClass === 'partial') {
|
|
254
|
+
const validated = validatePreflightAnswers(answers, {
|
|
255
|
+
hostCapabilities: hosts,
|
|
256
|
+
riskFloor: normalizedRoute?.execution?.riskFloor ?? null,
|
|
257
|
+
candidateMap,
|
|
258
|
+
config,
|
|
259
|
+
batch,
|
|
260
|
+
});
|
|
261
|
+
if (validated.status === 'validated') {
|
|
262
|
+
// No host binding exists at routing time — applyPreflight records the
|
|
263
|
+
// attempt and keeps the bundle 'validated' without acknowledgement.
|
|
264
|
+
const { preflight: applied } = applyPreflight(validated.bundle, {
|
|
265
|
+
host: hosts?.host ?? null,
|
|
266
|
+
});
|
|
267
|
+
preflight = applied;
|
|
268
|
+
} else {
|
|
269
|
+
preflight = buildFallbackPreflight({
|
|
270
|
+
route: normalizedRoute,
|
|
271
|
+
hostCapabilities: hosts,
|
|
272
|
+
config,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
preflight = baseline;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { receipt, preflight };
|
|
281
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* statePacket.js (TASK-002 / M07)
|
|
3
|
+
*
|
|
4
|
+
* C13 decision state packet (docs/pstack/UNIC_DECISION_SPEC.md §6): a compact,
|
|
5
|
+
* deterministic serialization of approved features — never raw conversation
|
|
6
|
+
* text. The sensitive-value gate runs BEFORE serialization/transport; a
|
|
7
|
+
* positive detection yields `blocked-sensitive`, never a redacted-after-send.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
scanText,
|
|
12
|
+
isSensitiveDataGateEnabled,
|
|
13
|
+
loadSensitiveAllowlist,
|
|
14
|
+
} from '../core/sensitiveValueScanner.js';
|
|
15
|
+
|
|
16
|
+
// Whitelisted C13 fields (UNIC_DECISION_SPEC §6). Anything else — prompts,
|
|
17
|
+
// source excerpts, diffs, command payloads, paths, secrets — is dropped.
|
|
18
|
+
const ALLOWED_FIELDS = new Set([
|
|
19
|
+
'stateVersion',
|
|
20
|
+
'taskClass',
|
|
21
|
+
'intentSignals',
|
|
22
|
+
'targetClass',
|
|
23
|
+
'execution',
|
|
24
|
+
'riskSignals',
|
|
25
|
+
'evidenceCodes',
|
|
26
|
+
'capabilityAvailability',
|
|
27
|
+
'candidateLabels',
|
|
28
|
+
'constraints',
|
|
29
|
+
'priorDecisionRefs',
|
|
30
|
+
'summary',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const MAX_CODE_LENGTH = 64;
|
|
34
|
+
const MAX_SUMMARY_LENGTH = 280;
|
|
35
|
+
const MAX_ARRAY_ITEMS = 24;
|
|
36
|
+
|
|
37
|
+
function sanitizeValue(value, key) {
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
return value.slice(0, key === 'summary' ? MAX_SUMMARY_LENGTH : MAX_CODE_LENGTH);
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'number') {
|
|
42
|
+
return Number.isFinite(value) ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'boolean') return value;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
const items = value
|
|
47
|
+
.slice(0, MAX_ARRAY_ITEMS)
|
|
48
|
+
.map((v) => sanitizeValue(v, key))
|
|
49
|
+
.filter((v) => v !== undefined);
|
|
50
|
+
return items;
|
|
51
|
+
}
|
|
52
|
+
if (value && typeof value === 'object') {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [k, v] of Object.entries(value)) {
|
|
55
|
+
// '__proto__' assignment would mutate out's prototype instead of storing
|
|
56
|
+
// the key — silently dropping the value and polluting the packet shape.
|
|
57
|
+
if (k === '__proto__') continue;
|
|
58
|
+
const clean = sanitizeValue(v, k);
|
|
59
|
+
if (clean !== undefined) out[k] = clean;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a C13 state packet from approved fields. Unknown fields are dropped;
|
|
68
|
+
* values are bounded to short stable codes.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} fields
|
|
71
|
+
* @returns {object} packet with stateVersion:1
|
|
72
|
+
*/
|
|
73
|
+
export function buildStatePacket(fields = {}) {
|
|
74
|
+
const packet = { stateVersion: 1 };
|
|
75
|
+
if (!fields || typeof fields !== 'object') return packet;
|
|
76
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
77
|
+
if (!ALLOWED_FIELDS.has(key) || key === 'stateVersion') continue;
|
|
78
|
+
const clean = sanitizeValue(value, key);
|
|
79
|
+
if (clean !== undefined) packet[key] = clean;
|
|
80
|
+
}
|
|
81
|
+
return packet;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Sorted-key serialization — identical logical packets produce identical bytes
|
|
85
|
+
// (state fingerprints depend on it).
|
|
86
|
+
function stableStringify(value) {
|
|
87
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
88
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
89
|
+
const keys = Object.keys(value).sort();
|
|
90
|
+
return `{${keys
|
|
91
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
92
|
+
.join(',')}}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Serialize a packet deterministically.
|
|
97
|
+
* @param {object|string} packet
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function serializeStatePacket(packet) {
|
|
101
|
+
return typeof packet === 'string' ? packet : stableStringify(packet);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Deterministic token estimator: a ~4-chars/token heuristic with a
|
|
106
|
+
* conservative byte backstop (ceil(bytes/3)) that dominates for dense ASCII —
|
|
107
|
+
* the safe direction is overestimation (UNIC_DECISION_SPEC §6).
|
|
108
|
+
*
|
|
109
|
+
* @param {object|string} packet packet or serialized text
|
|
110
|
+
* @returns {number} estimated token count
|
|
111
|
+
*/
|
|
112
|
+
export function estimateTokens(packet) {
|
|
113
|
+
const text = serializeStatePacket(packet);
|
|
114
|
+
const chars = text.length;
|
|
115
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
116
|
+
return Math.max(Math.ceil(chars / 4), Math.ceil(bytes / 3));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DEFAULT_LIMITS = { multilingual: 1024, english: 512 };
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Assert the packet fits the token budget for its language class.
|
|
123
|
+
* 'english' → 512; 'multilingual'/'unknown' → 1024 (config-overridable).
|
|
124
|
+
* Throws an Error with code 'state-too-large' — never silently truncates.
|
|
125
|
+
*
|
|
126
|
+
* @param {object|string} packet
|
|
127
|
+
* @param {'english'|'multilingual'|'unknown'} languageClass
|
|
128
|
+
* @param {{multilingual?: number, english?: number}} [limits]
|
|
129
|
+
* @returns {number} the estimated token count
|
|
130
|
+
*/
|
|
131
|
+
export function assertStateBudget(packet, languageClass, limits = DEFAULT_LIMITS) {
|
|
132
|
+
const budget =
|
|
133
|
+
languageClass === 'english'
|
|
134
|
+
? (limits?.english ?? DEFAULT_LIMITS.english)
|
|
135
|
+
: (limits?.multilingual ?? DEFAULT_LIMITS.multilingual);
|
|
136
|
+
const tokens = estimateTokens(packet);
|
|
137
|
+
if (tokens > budget) {
|
|
138
|
+
const err = new Error(`state-too-large: ${tokens} tokens exceeds ${budget}`);
|
|
139
|
+
err.code = 'state-too-large';
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
return tokens;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run the sensitive-value gate over the serialized packet. Fail-closed: any
|
|
147
|
+
* positive detection returns `blocked-sensitive` with labels only — the secret
|
|
148
|
+
* value is never carried in the result.
|
|
149
|
+
*
|
|
150
|
+
* @param {object|string} packet
|
|
151
|
+
* @param {{config?: object}} [options] runtime config (gate toggle + allowlist)
|
|
152
|
+
* @returns {{status:'ok', serialized:string, tokens:number}
|
|
153
|
+
* | {status:'blocked-sensitive', labels:string[]}}
|
|
154
|
+
*/
|
|
155
|
+
export function redactStatePacket(packet, { config } = {}) {
|
|
156
|
+
const serialized = serializeStatePacket(packet);
|
|
157
|
+
const scan = scanText(serialized, {
|
|
158
|
+
gateEnabled: isSensitiveDataGateEnabled(config),
|
|
159
|
+
allowlistHashes: loadSensitiveAllowlist(config),
|
|
160
|
+
});
|
|
161
|
+
if (scan.hasSecret) {
|
|
162
|
+
return { status: 'blocked-sensitive', labels: scan.labels };
|
|
163
|
+
}
|
|
164
|
+
return { status: 'ok', serialized, tokens: estimateTokens(serialized) };
|
|
165
|
+
}
|
|
@@ -50,7 +50,8 @@ function normalizeSignature(command) {
|
|
|
50
50
|
|
|
51
51
|
// Extract failure events from one parsed ledger. A failure event is a receipt with
|
|
52
52
|
// `success === false` on a verification-ish kind carrying a `command`, OR (when the
|
|
53
|
-
// ledger declares `verificationFailed === true`) any receipt carrying a
|
|
53
|
+
// ledger declares `verificationFailed === true`) any FAILED receipt carrying a
|
|
54
|
+
// `command` — a succeeded receipt is never a failure event.
|
|
54
55
|
function failureCommands(ledger) {
|
|
55
56
|
if (!isObject(ledger) || !Array.isArray(ledger.receipts)) return [];
|
|
56
57
|
const out = [];
|
|
@@ -43,5 +43,7 @@ export async function listLedgerFiles(dir, limit) {
|
|
|
43
43
|
}
|
|
44
44
|
}));
|
|
45
45
|
withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
46
|
-
|
|
46
|
+
// `Math.max(0, undefined)` is NaN and slice(0, NaN) is [] — an omitted limit
|
|
47
|
+
// silently returned nothing. Omitted limit means "all files".
|
|
48
|
+
return withMtime.slice(0, Math.max(0, limit ?? Infinity)).map((f) => f.name);
|
|
47
49
|
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { listLedgerFiles } from './ledgerFiles.js';
|
|
7
8
|
|
|
8
9
|
const UNCLASSIFIED = '(unclassified)';
|
|
9
|
-
const LEDGER_EXCLUDE = new Set(['gate-crash-counter.json']);
|
|
10
10
|
|
|
11
11
|
function zeroedResult(extra = {}) {
|
|
12
12
|
return {
|
|
@@ -37,34 +37,6 @@ function emptyBucket() {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function listLedgerFiles(ledgerDir, limitLedgers) {
|
|
41
|
-
let dirents;
|
|
42
|
-
try {
|
|
43
|
-
dirents = await fs.readdir(ledgerDir, { withFileTypes: true });
|
|
44
|
-
} catch {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
const candidates = [];
|
|
48
|
-
for (const dirent of dirents) {
|
|
49
|
-
if (!dirent.isFile()) continue;
|
|
50
|
-
const name = dirent.name;
|
|
51
|
-
if (!name.endsWith('.json')) continue;
|
|
52
|
-
if (LEDGER_EXCLUDE.has(name)) continue;
|
|
53
|
-
if (name.includes('.journal') || name.includes('.quarantine')) continue;
|
|
54
|
-
candidates.push(name);
|
|
55
|
-
}
|
|
56
|
-
const withMtime = await Promise.all(candidates.map(async (name) => {
|
|
57
|
-
try {
|
|
58
|
-
const stat = await fs.stat(path.join(ledgerDir, name));
|
|
59
|
-
return { name, mtimeMs: stat.mtimeMs };
|
|
60
|
-
} catch {
|
|
61
|
-
return { name, mtimeMs: 0 };
|
|
62
|
-
}
|
|
63
|
-
}));
|
|
64
|
-
withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
65
|
-
return withMtime.slice(0, Math.max(0, limitLedgers)).map((entry) => entry.name);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
40
|
async function readJson(filePath) {
|
|
69
41
|
try {
|
|
70
42
|
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
package/src/index/buildIndex.js
CHANGED
|
@@ -249,7 +249,10 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
249
249
|
]);
|
|
250
250
|
canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
251
251
|
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
252
|
-
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
252
|
+
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
253
|
+
&& Array.isArray(previousSymbolsArtifact.items)
|
|
254
|
+
&& Array.isArray(previousImportsArtifact.items)
|
|
255
|
+
&& Array.isArray(previousCallsArtifact.items);
|
|
253
256
|
|
|
254
257
|
if (canReuseParsedArtifacts) {
|
|
255
258
|
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
|
|
@@ -306,7 +309,8 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
306
309
|
? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.testsMap)
|
|
307
310
|
: null;
|
|
308
311
|
const canReuseTestsMap = canReuseTestsMapCandidate
|
|
309
|
-
&& previousTestsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
312
|
+
&& previousTestsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
313
|
+
&& Array.isArray(previousTestsMapArtifact.items);
|
|
310
314
|
const testsMap = canReuseTestsMap
|
|
311
315
|
? previousTestsMapArtifact.items
|
|
312
316
|
: buildTestsMap(fileRecords);
|
|
@@ -315,6 +319,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
315
319
|
? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.hotspots)
|
|
316
320
|
: null;
|
|
317
321
|
const canReuseHotspots = previousHotspotsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
322
|
+
&& Array.isArray(previousHotspotsArtifact.items)
|
|
318
323
|
&& areBugIndexSnapshotsEqual(previousHotspotsArtifact?.sourceSnapshot, bugIndexSnapshot);
|
|
319
324
|
const hotspots = canReuseHotspots
|
|
320
325
|
? previousHotspotsArtifact.items
|
|
@@ -329,8 +334,9 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
329
334
|
: null;
|
|
330
335
|
const canReuseArchetypes = canReuseArchetypesCandidate
|
|
331
336
|
&& previousArchetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
332
|
-
&& previousArchetypesArtifact
|
|
333
|
-
&&
|
|
337
|
+
&& Array.isArray(previousArchetypesArtifact.items)
|
|
338
|
+
&& previousArchetypesArtifact.items.length === codeFiles.length
|
|
339
|
+
&& previousArchetypesArtifact.items.every((item) => currentCodePaths.has(item.filePath));
|
|
334
340
|
|
|
335
341
|
// Reuse cached archetypes only when every indexed code file is unchanged
|
|
336
342
|
const archetypes = canReuseArchetypes
|
|
@@ -356,6 +362,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
356
362
|
};
|
|
357
363
|
const canReuseRelations = canReuseGraphArtifactsCandidate
|
|
358
364
|
&& previousRelationsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
365
|
+
&& Array.isArray(previousRelationsArtifact.items)
|
|
359
366
|
&& areStringArraysEqual(previousRelationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
360
367
|
&& arePathSnapshotsEqual(
|
|
361
368
|
previousRelationsArtifact?.sourceSnapshot?.importAliasSnapshot,
|
|
@@ -363,6 +370,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
363
370
|
);
|
|
364
371
|
const canReuseAnalogs = canReuseGraphArtifactsCandidate
|
|
365
372
|
&& previousAnalogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
373
|
+
&& Array.isArray(previousAnalogsArtifact.items)
|
|
366
374
|
&& areStringArraysEqual(previousAnalogsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
367
375
|
&& arePathSnapshotsEqual(
|
|
368
376
|
previousAnalogsArtifact?.sourceSnapshot?.importAliasSnapshot,
|
|
@@ -948,11 +956,13 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
948
956
|
// expensive work being avoided is discovery and re-parsing, not these pure
|
|
949
957
|
// transformations, so their output stays identical to a clean rebuild.
|
|
950
958
|
const canReuseTestsMap = haveStableTrackedFileIdentities(filesArtifact.items, mergedFileRecords)
|
|
951
|
-
&& testsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
959
|
+
&& testsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
960
|
+
&& Array.isArray(testsMapArtifact.items);
|
|
952
961
|
const testsMap = canReuseTestsMap ? testsMapArtifact.items : buildTestsMap(mergedFileRecords);
|
|
953
962
|
|
|
954
963
|
const bugIndexSnapshot = await readBugIndexSnapshot(absoluteRoot);
|
|
955
964
|
const canReuseHotspots = hotspotsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
965
|
+
&& Array.isArray(hotspotsArtifact.items)
|
|
956
966
|
&& areBugIndexSnapshotsEqual(hotspotsArtifact?.sourceSnapshot, bugIndexSnapshot);
|
|
957
967
|
const hotspots = canReuseHotspots ? hotspotsArtifact.items : await buildHotspots(absoluteRoot, bugIndexSnapshot);
|
|
958
968
|
|
|
@@ -964,9 +974,10 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
964
974
|
const canReuseArchetypes = parsedFiles.length === 0
|
|
965
975
|
&& !recordsChanged
|
|
966
976
|
&& archetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
977
|
+
&& Array.isArray(archetypesArtifact.items)
|
|
967
978
|
&& codeFiles.every((file) => previousCodePaths.has(file.filePath))
|
|
968
|
-
&& archetypesArtifact
|
|
969
|
-
&&
|
|
979
|
+
&& archetypesArtifact.items.length === archetypes.length
|
|
980
|
+
&& archetypesArtifact.items.every((item) => currentCodePaths.has(item?.filePath));
|
|
970
981
|
const archetypeItems = canReuseArchetypes ? archetypesArtifact.items : archetypes;
|
|
971
982
|
|
|
972
983
|
const needsAliasContext = importsNeedAliasContext(imports);
|
|
@@ -984,7 +995,8 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
984
995
|
&& areStringArraysEqual(relationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
985
996
|
&& arePathSnapshotsEqual(relationsArtifact?.sourceSnapshot?.importAliasSnapshot, styleSnapshot.importAliasSnapshot);
|
|
986
997
|
const canReuseAnalogs = canReuseRelations
|
|
987
|
-
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
998
|
+
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
999
|
+
&& Array.isArray(analogsArtifact.items);
|
|
988
1000
|
|
|
989
1001
|
let relations = relationsArtifact.items;
|
|
990
1002
|
let analogs = analogsArtifact?.items ?? null;
|
|
@@ -1639,7 +1651,7 @@ function extractImports(filePath, content) {
|
|
|
1639
1651
|
const stripped = stripCommentsStringAware(content);
|
|
1640
1652
|
const imports = [];
|
|
1641
1653
|
|
|
1642
|
-
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s
|
|
1654
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s*(?:[^'";]+?\s*from\s*)?['"]([^'"]+)['"]/g)) {
|
|
1643
1655
|
imports.push({
|
|
1644
1656
|
from: filePath,
|
|
1645
1657
|
to: match[1],
|
|
@@ -1663,7 +1675,7 @@ function extractImports(filePath, content) {
|
|
|
1663
1675
|
});
|
|
1664
1676
|
}
|
|
1665
1677
|
|
|
1666
|
-
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s
|
|
1678
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
1667
1679
|
imports.push({
|
|
1668
1680
|
from: filePath,
|
|
1669
1681
|
to: match[1],
|
|
@@ -1671,7 +1683,7 @@ function extractImports(filePath, content) {
|
|
|
1671
1683
|
});
|
|
1672
1684
|
}
|
|
1673
1685
|
|
|
1674
|
-
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s
|
|
1686
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
1675
1687
|
imports.push({
|
|
1676
1688
|
from: filePath,
|
|
1677
1689
|
to: match[1],
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { getArtifactPath, INDEX_ARTIFACTS } from './paths.js';
|
|
5
5
|
import { classifyImpactRisk, findMirrorCounterparts } from './impactCatalog.js';
|
|
6
6
|
import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from './relatedTests.js';
|
|
7
|
+
import { importsNeedAliasContext, loadImportAliasContext, resolveImportSpecifier } from './importResolution.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_IMPACT_LIMITS = {
|
|
9
10
|
maxCallers: 8,
|
|
@@ -86,9 +87,10 @@ export async function resolveImpactContext({
|
|
|
86
87
|
const normalizedChangedFiles = unique(changedFiles.map(normalizePath));
|
|
87
88
|
const normalizedChangedSymbols = unique(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
88
89
|
|
|
89
|
-
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
90
|
+
const [callsArtifact, importsArtifact, filesArtifact, relatedArtifacts] = await Promise.all([
|
|
90
91
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
91
92
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
93
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.files),
|
|
92
94
|
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
93
95
|
]);
|
|
94
96
|
|
|
@@ -140,9 +142,26 @@ export async function resolveImpactContext({
|
|
|
140
142
|
|
|
141
143
|
const importers = [];
|
|
142
144
|
const dependencies = [];
|
|
145
|
+
// Non-relative specifiers (aliases like `@/x`, `~/x`, tsconfig paths) resolve
|
|
146
|
+
// through the same machinery the index builder uses; relative specifiers keep
|
|
147
|
+
// the candidate-list match so a changed file absent from files.json (e.g. a
|
|
148
|
+
// brand-new file) still counts.
|
|
149
|
+
const indexedFileSet = new Set((filesArtifact.items ?? []).map((item) => item.filePath));
|
|
150
|
+
const importAliasContext = importsNeedAliasContext(importItems)
|
|
151
|
+
? await loadImportAliasContext({ rootDir: absoluteRoot }).catch(() => null)
|
|
152
|
+
: null;
|
|
143
153
|
for (const imp of importItems) {
|
|
144
154
|
const from = normalizePath(imp.from);
|
|
145
|
-
const
|
|
155
|
+
const specifier = String(imp.to ?? '').trim();
|
|
156
|
+
const targets = specifier.startsWith('.')
|
|
157
|
+
? resolveImportTarget(from, specifier)
|
|
158
|
+
: [resolveImportSpecifier({
|
|
159
|
+
rootDir: absoluteRoot,
|
|
160
|
+
fromFilePath: from,
|
|
161
|
+
specifier,
|
|
162
|
+
indexedFileSet,
|
|
163
|
+
aliasContext: importAliasContext,
|
|
164
|
+
})].filter(Boolean);
|
|
146
165
|
const matched = targets.find((candidate) => changedFileSet.has(normalizePath(candidate)));
|
|
147
166
|
|
|
148
167
|
if (matched) {
|