arkgate 2.9.1 → 2.10.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +14 -2
  3. package/bin/ark-mcp.mjs +282 -102
  4. package/bin/lib/agent-gates.mjs +181 -6
  5. package/bin/lib/architecture-scan.mjs +19 -0
  6. package/bin/lib/auto-patch.mjs +264 -0
  7. package/bin/lib/doctor-plan.mjs +54 -0
  8. package/bin/lib/port-proof.mjs +309 -0
  9. package/bin/lib/prepare-write.mjs +130 -0
  10. package/bin/lib/remediation.mjs +21 -0
  11. package/dist/index.cjs +13 -4
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +13 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/nestjs/index.cjs +1 -1
  18. package/dist/nestjs/index.cjs.map +1 -1
  19. package/dist/nestjs/index.js +1 -1
  20. package/dist/nestjs/index.js.map +1 -1
  21. package/dist/runtime/index.cjs +13 -4
  22. package/dist/runtime/index.cjs.map +1 -1
  23. package/dist/runtime/index.js +13 -4
  24. package/dist/runtime/index.js.map +1 -1
  25. package/docs/agent-guide.md +14 -0
  26. package/docs/ai-gates.md +43 -3
  27. package/docs/enthusiast/how-to-agent-gates.md +8 -0
  28. package/docs/enthusiast/reference-commands.md +1 -1
  29. package/package.json +2 -1
  30. package/server.json +2 -2
  31. package/templates/skills/ark-adopt.md +57 -10
  32. package/templates/skills/ark-architect.md +33 -2
  33. package/templates/skills/ark-autopilot.md +75 -20
  34. package/templates/skills/ark-contract.md +36 -0
  35. package/templates/skills/ark-coverage.md +81 -27
  36. package/templates/skills/ark-explain.md +35 -2
  37. package/templates/skills/ark-explore.md +119 -0
  38. package/templates/skills/ark-fix.md +47 -2
  39. package/templates/skills/ark-loop.md +50 -3
  40. package/templates/skills/ark-place.md +36 -0
  41. package/templates/skills/ark-runtime.md +36 -0
  42. package/templates/skills/ark-think.md +63 -12
  43. package/templates/skills/ark-upgrade.md +33 -1
@@ -0,0 +1,309 @@
1
+ /**
2
+ * W6 — Verified structural transform: port-proof inject binding (single narrow kind).
3
+ *
4
+ * Scope (intentionally tiny — false mechanical-safe is worse than extra judgment):
5
+ * - Exactly one static named value import (no default / namespace / side-effect / export-from)
6
+ * - Binding used ONLY as property-access call receiver: `binding.method(...)`
7
+ * - All uses inside `function` declarations (not module-level, not classes, not arrows)
8
+ * - No require / dynamic import
9
+ *
10
+ * Transform (single file):
11
+ * 1. Remove the import
12
+ * 2. Emit `export type <Port> = { method: (...args: unknown[]) => unknown; ... }`
13
+ * 3. Add `binding: Port` as last parameter of each function that uses it
14
+ * 4. Leave call expressions verbatim (`binding.method(...)`)
15
+ *
16
+ * Static proof of behavior preservation (module-local):
17
+ * If the injected parameter equals the value previously bound by the import, every
18
+ * statement in each rewritten function evaluates identically — call expressions are
19
+ * preserved character-for-character after rebinding. No adapter file is invented;
20
+ * the outer layer must pass the implementation (classic port inject).
21
+ *
22
+ * Fail closed: any unmatched use pattern → not eligible (judgment).
23
+ */
24
+ import path from 'node:path';
25
+
26
+ /**
27
+ * @param {object} ts typescript module
28
+ * @param {string} source
29
+ * @param {{ filePath?: string, importLocalName?: string, importSpecifier?: string }} [opts]
30
+ * @returns {{ eligible: boolean, reason?: string, bindingName?: string, methods?: string[], functionNames?: string[], specifier?: string }}
31
+ */
32
+ export function provePortProofInject(ts, source, opts = {}) {
33
+ if (!ts || typeof source !== 'string') {
34
+ return { eligible: false, reason: 'missing-ts-or-source' };
35
+ }
36
+ const sf = ts.createSourceFile(
37
+ opts.filePath || 'file.ts',
38
+ source,
39
+ ts.ScriptTarget.Latest,
40
+ true,
41
+ ts.ScriptKind.TS
42
+ );
43
+
44
+ /** @type {import('typescript').ImportDeclaration | null} */
45
+ let targetImport = null;
46
+ let bindingName = null;
47
+ let specifier = null;
48
+ let namedImportCount = 0;
49
+
50
+ for (const stmt of sf.statements) {
51
+ if (!ts.isImportDeclaration(stmt)) continue;
52
+ if (stmt.importClause?.isTypeOnly) continue;
53
+ const clause = stmt.importClause;
54
+ if (!clause) continue; // side-effect
55
+ if (clause.name) {
56
+ // default import — never port-proof
57
+ return { eligible: false, reason: 'default-import' };
58
+ }
59
+ const named = clause.namedBindings;
60
+ if (!named || !ts.isNamedImports(named)) {
61
+ return { eligible: false, reason: 'namespace-or-missing-named' };
62
+ }
63
+ if (named.elements.some((el) => el.isTypeOnly)) {
64
+ // partial type-only mixed — not this transform
65
+ continue;
66
+ }
67
+ if (named.elements.length !== 1) {
68
+ return { eligible: false, reason: 'multi-named-import' };
69
+ }
70
+ namedImportCount += 1;
71
+ if (namedImportCount > 1) {
72
+ return { eligible: false, reason: 'multiple-value-imports' };
73
+ }
74
+ const el = named.elements[0];
75
+ bindingName = el.name.text;
76
+ specifier = stmt.moduleSpecifier && ts.isStringLiteralLike(stmt.moduleSpecifier)
77
+ ? stmt.moduleSpecifier.text
78
+ : null;
79
+ if (!specifier || (!specifier.startsWith('./') && !specifier.startsWith('../'))) {
80
+ return { eligible: false, reason: 'non-relative-specifier' };
81
+ }
82
+ if (opts.importLocalName && opts.importLocalName !== bindingName) {
83
+ return { eligible: false, reason: 'binding-mismatch' };
84
+ }
85
+ if (opts.importSpecifier && opts.importSpecifier !== specifier) {
86
+ // soft: still allow if only one import
87
+ }
88
+ targetImport = stmt;
89
+ }
90
+
91
+ if (!targetImport || !bindingName) {
92
+ return { eligible: false, reason: 'no-single-named-value-import' };
93
+ }
94
+
95
+ const methods = new Set();
96
+ const functionNames = new Set();
97
+ let freeBindingUse = false;
98
+
99
+ // Walk top-level: only function declarations may use the binding (as method calls).
100
+ function walkTop(node) {
101
+ if (ts.isFunctionDeclaration(node)) {
102
+ if (node.body) {
103
+ visitBody(node.body, node.name?.text);
104
+ }
105
+ return;
106
+ }
107
+ // Any binding use outside function decls is invalid
108
+ if (ts.isIdentifier(node) && node.text === bindingName) {
109
+ if (node.parent && ts.isImportSpecifier(node.parent)) return;
110
+ freeBindingUse = true;
111
+ return;
112
+ }
113
+ ts.forEachChild(node, walkTop);
114
+ }
115
+
116
+ function visitBody(body, fnName) {
117
+ function walk(node) {
118
+ if (ts.isIdentifier(node) && node.text === bindingName) {
119
+ const parent = node.parent;
120
+ if (
121
+ parent &&
122
+ ts.isPropertyAccessExpression(parent) &&
123
+ parent.expression === node &&
124
+ parent.parent &&
125
+ ts.isCallExpression(parent.parent) &&
126
+ parent.parent.expression === parent
127
+ ) {
128
+ methods.add(parent.name.text);
129
+ if (fnName) functionNames.add(fnName);
130
+ return;
131
+ }
132
+ freeBindingUse = true;
133
+ return;
134
+ }
135
+ // Nested functions: still require property-call form
136
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
137
+ // Nested non-declarations: fail closed (narrow transform)
138
+ if (!ts.isFunctionDeclaration(node)) {
139
+ // Check if binding used inside — if yes, ineligible
140
+ let nestedUse = false;
141
+ const check = (n) => {
142
+ if (ts.isIdentifier(n) && n.text === bindingName) nestedUse = true;
143
+ else ts.forEachChild(n, check);
144
+ };
145
+ if (node.body) check(node.body);
146
+ if (nestedUse) freeBindingUse = true;
147
+ return;
148
+ }
149
+ }
150
+ ts.forEachChild(node, walk);
151
+ }
152
+ walk(body);
153
+ }
154
+
155
+ for (const stmt of sf.statements) {
156
+ walkTop(stmt);
157
+ }
158
+
159
+ if (freeBindingUse) {
160
+ return { eligible: false, reason: 'free-or-non-call-use' };
161
+ }
162
+ if (methods.size === 0 || functionNames.size === 0) {
163
+ return { eligible: false, reason: 'no-method-calls-in-functions' };
164
+ }
165
+
166
+ return {
167
+ eligible: true,
168
+ bindingName,
169
+ methods: [...methods].sort(),
170
+ functionNames: [...functionNames].sort(),
171
+ specifier,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Apply port-proof inject when prove succeeds.
177
+ * @returns {{ source: string, remediationKind: string, confidence: number, proof: object } | null}
178
+ */
179
+ export function applyPortProofInject(ts, source, opts = {}) {
180
+ const proof = provePortProofInject(ts, source, opts);
181
+ if (!proof.eligible) return null;
182
+
183
+ const sf = ts.createSourceFile(
184
+ opts.filePath || 'file.ts',
185
+ source,
186
+ ts.ScriptTarget.Latest,
187
+ true,
188
+ ts.ScriptKind.TS
189
+ );
190
+
191
+ const bindingName = proof.bindingName;
192
+ const methods = proof.methods;
193
+ const portTypeName = `${capitalize(bindingName)}Port`;
194
+
195
+ // Find import to remove
196
+ /** @type {{ start: number, end: number } | null} */
197
+ let importSpan = null;
198
+ for (const stmt of sf.statements) {
199
+ if (!ts.isImportDeclaration(stmt)) continue;
200
+ const clause = stmt.importClause;
201
+ if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue;
202
+ if (clause.namedBindings.elements.length !== 1) continue;
203
+ if (clause.namedBindings.elements[0].name.text !== bindingName) continue;
204
+ // getStart skips leading trivia so we don't leave a stray indent before the port type.
205
+ importSpan = { start: stmt.getStart(sf), end: stmt.getEnd() };
206
+ if (source[importSpan.end] === '\r') importSpan.end += 1;
207
+ if (source[importSpan.end] === '\n') importSpan.end += 1;
208
+ break;
209
+ }
210
+ if (!importSpan) return null;
211
+
212
+ // Functions that need the port param
213
+ const fnEdits = [];
214
+ for (const stmt of sf.statements) {
215
+ if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !stmt.body) continue;
216
+ if (!proof.functionNames.includes(stmt.name.text)) continue;
217
+ // Already has a param named bindingName?
218
+ const params = stmt.parameters || [];
219
+ if (params.some((p) => ts.isIdentifier(p.name) && p.name.text === bindingName)) {
220
+ continue;
221
+ }
222
+ // Fail closed: rest params / non-identifier patterns would yield illegal TS
223
+ // (e.g. function f(...args, db: Port)).
224
+ if (
225
+ params.some(
226
+ (p) =>
227
+ p.dotDotDotToken ||
228
+ !ts.isIdentifier(p.name)
229
+ )
230
+ ) {
231
+ return null;
232
+ }
233
+ if (params.length > 0) {
234
+ const at = params[params.length - 1].getEnd();
235
+ fnEdits.push({
236
+ start: at,
237
+ end: at,
238
+ text: `, ${bindingName}: ${portTypeName}`,
239
+ });
240
+ } else {
241
+ const open = source.indexOf('(', stmt.name.getEnd());
242
+ if (open < 0) continue;
243
+ const at = open + 1;
244
+ fnEdits.push({
245
+ start: at,
246
+ end: at,
247
+ text: `${bindingName}: ${portTypeName}`,
248
+ });
249
+ }
250
+ }
251
+ if (fnEdits.length === 0) return null;
252
+
253
+ const portDecl =
254
+ `export type ${portTypeName} = {\n` +
255
+ methods.map((m) => ` ${m}: (...args: unknown[]) => unknown;`).join('\n') +
256
+ `\n};\n\n`;
257
+
258
+ // Apply edits from end to start
259
+ const edits = [
260
+ { start: importSpan.start, end: importSpan.end, text: portDecl },
261
+ ...fnEdits,
262
+ ].sort((a, b) => b.start - a.start);
263
+
264
+ let out = source;
265
+ for (const e of edits) {
266
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
267
+ }
268
+ if (out === source) return null;
269
+
270
+ // Re-prove on result: binding should no longer need the import; methods still present
271
+ // (as params). Eligibility after transform is "no value import" — proof may fail for
272
+ // different reasons. Validate shape: no import of binding's original specifier as value.
273
+ if (new RegExp(`import\\s*\\{[^}]*\\b${escapeRe(bindingName)}\\b`).test(out)) {
274
+ return null;
275
+ }
276
+ if (!out.includes(`export type ${portTypeName}`)) return null;
277
+
278
+ return {
279
+ source: out,
280
+ remediationKind: 'port-proof-inject-binding',
281
+ confidence: 0.8,
282
+ proof: {
283
+ bindingName,
284
+ methods,
285
+ functionNames: proof.functionNames,
286
+ specifier: proof.specifier,
287
+ portTypeName,
288
+ },
289
+ };
290
+ }
291
+
292
+ function capitalize(s) {
293
+ if (!s) return 'Port';
294
+ return s.charAt(0).toUpperCase() + s.slice(1);
295
+ }
296
+
297
+ function escapeRe(s) {
298
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
299
+ }
300
+
301
+ /**
302
+ * True when relative path basename matches a common path segment of the import target.
303
+ * Used only as a soft filter when wiring from ark-check violations.
304
+ */
305
+ export function specifierLooksLikeTarget(specifier, violationTargetRel) {
306
+ if (!specifier || !violationTargetRel) return true;
307
+ const base = path.basename(violationTargetRel, path.extname(violationTargetRel));
308
+ return specifier.includes(base) || violationTargetRel.includes(specifier.replace(/^\.\//, ''));
309
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * W2 — ark_prepare_write composition helpers.
3
+ *
4
+ * Place + constrain + validate + autoPatch + judgmentBrief + content identity.
5
+ * Pure-ish: no second architecture contract — callers pass placement + validate.
6
+ */
7
+ import crypto from 'node:crypto';
8
+ import { validateWithAutoPatch } from './auto-patch.mjs';
9
+ import { classifyRemediation, enrichViolationWithFixClass } from './remediation.mjs';
10
+
11
+ /**
12
+ * Stable content identity for host commit / cache keys.
13
+ * @param {string} source
14
+ * @returns {{ contentHash: string, byteLength: number }}
15
+ */
16
+ export function contentIdentity(source) {
17
+ const text = typeof source === 'string' ? source : '';
18
+ const contentHash = `sha256:${crypto.createHash('sha256').update(text, 'utf8').digest('hex')}`;
19
+ return { contentHash, byteLength: Buffer.byteLength(text, 'utf8') };
20
+ }
21
+
22
+ /**
23
+ * One judgment decision for the agent when autoPatch is absent or insufficient.
24
+ * @param {Array<object>} violations
25
+ * @returns {null | { fixClass: string, decision: string, remediationClass: string, remediationKind?: string }}
26
+ */
27
+ export function buildJudgmentBrief(violations) {
28
+ if (!Array.isArray(violations) || violations.length === 0) return null;
29
+ for (const v of violations) {
30
+ const ruleId = v.ruleId || v.code;
31
+ const shaped = {
32
+ ruleId,
33
+ typeOnly: v.typeOnly ?? v.details?.typeOnly,
34
+ sourcePureTypeModule: v.sourcePureTypeModule,
35
+ targetTypeOnlyExports: v.targetTypeOnlyExports,
36
+ namedBindingsTypeOnly: v.namedBindingsTypeOnly,
37
+ peerIsolation: v.peerIsolation ?? v.details?.peerIsolation,
38
+ edgeKind: v.edgeKind ?? v.details?.importKind,
39
+ fromLayer: v.fromLayer,
40
+ toLayer: v.toLayer,
41
+ target: v.target,
42
+ message: v.message,
43
+ };
44
+ const verdict = classifyRemediation(shaped);
45
+ if (verdict.class === 'mechanical-safe') continue;
46
+ const enriched = enrichViolationWithFixClass(shaped);
47
+ return {
48
+ fixClass: enriched.fixClass,
49
+ decision: enriched.enthusiastHint,
50
+ remediationClass: verdict.class,
51
+ ...(verdict.remediationKind ? { remediationKind: verdict.remediationKind } : {}),
52
+ };
53
+ }
54
+ // All mechanical-safe or unclassifiable — still offer first enriched hint
55
+ const first = violations[0];
56
+ const ruleId = first.ruleId || first.code;
57
+ const shaped = { ...first, ruleId };
58
+ const enriched = enrichViolationWithFixClass(shaped);
59
+ const verdict = classifyRemediation(shaped);
60
+ return {
61
+ fixClass: enriched.fixClass,
62
+ decision: enriched.enthusiastHint,
63
+ remediationClass: verdict.class,
64
+ ...(verdict.remediationKind ? { remediationKind: verdict.remediationKind } : {}),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Compose placement + write-boundary validation into one prepare_write result.
70
+ *
71
+ * @param {{
72
+ * source: string,
73
+ * placement: object,
74
+ * root: string,
75
+ * ts: object,
76
+ * validate: (source: string) => { valid: boolean, violations?: any[] },
77
+ * resolveTargetAbs?: Function,
78
+ * }} opts
79
+ */
80
+ export function composePrepareWrite(opts) {
81
+ const { source, placement, root, ts, validate, resolveTargetAbs } = opts;
82
+ if (typeof source !== 'string') {
83
+ return {
84
+ ok: false,
85
+ error: 'source is required (string)',
86
+ };
87
+ }
88
+ const identity = contentIdentity(source);
89
+ const filePath = placement?.filePath;
90
+ const gate = validateWithAutoPatch({
91
+ source,
92
+ filePath,
93
+ root,
94
+ ts,
95
+ validate,
96
+ resolveTargetAbs,
97
+ });
98
+
99
+ // judgmentBrief when invalid and no mechanical autoPatch (agent must decide).
100
+ // When autoPatch is present, omit brief — host should apply the patch first.
101
+ const judgment =
102
+ !gate.valid && !gate.autoPatch ? buildJudgmentBrief(gate.violations) : null;
103
+
104
+ return {
105
+ ok: true,
106
+ filePath: placement?.filePath ?? null,
107
+ layer: placement?.layer ?? null,
108
+ governed: placement?.governed,
109
+ proposed: placement?.proposed,
110
+ mayImport: placement?.mayImport,
111
+ mustNotImport: placement?.mustNotImport,
112
+ forbiddenGlobals: placement?.forbiddenGlobals ?? [],
113
+ ...(placement?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
114
+ ...(placement?.suggestedLayers ? { suggestedLayers: placement.suggestedLayers } : {}),
115
+ ...(placement?.message ? { placementMessage: placement.message } : {}),
116
+ ...(placement?.note ? { placementNote: placement.note } : {}),
117
+ ...(placement?.description ? { description: placement.description } : {}),
118
+ valid: gate.valid,
119
+ violations: gate.violations,
120
+ ...(gate.autoPatch ? { autoPatch: gate.autoPatch } : {}),
121
+ ...(judgment ? { judgmentBrief: judgment } : {}),
122
+ contentHash: identity.contentHash,
123
+ byteLength: identity.byteLength,
124
+ ...(gate.autoPatch
125
+ ? {
126
+ autoPatchContentHash: contentIdentity(gate.autoPatch.source).contentHash,
127
+ }
128
+ : {}),
129
+ };
130
+ }
@@ -19,6 +19,14 @@ export const MECHANICAL_SAFE_KINDS = [
19
19
  'type-only-import-move',
20
20
  'import-type-from-pure-type-module',
21
21
  'import-type-of-type-exports',
22
+ // port-proof-inject-binding is intentionally NOT mechanical-safe (signature change).
23
+ ];
24
+ /**
25
+ * Judgment-class kinds that still have a named transform / plan label (eval corpus vocabulary).
26
+ * Never auto-apply without multi-file / caller proof.
27
+ */
28
+ export const JUDGMENT_SUGGESTED_KINDS = [
29
+ 'port-proof-inject-binding',
22
30
  ];
23
31
  /** fixClass values from enrichViolationWithFixClass (eval corpus / reports). */
24
32
  export const KNOWN_FIX_CLASSES = [
@@ -91,6 +99,19 @@ export function classifyRemediation(violation) {
91
99
  rationale: 'Named bindings are type-only exports of the target module (even if the file also exports values): convert to `import type` / `export type` (erased at runtime). Gate verifies.',
92
100
  };
93
101
  }
102
+ // W6: port-proof inject is a *suggested* shape when proof holds, but always judgment
103
+ // for auto-apply — adding a required parameter breaks external call sites.
104
+ if (violation?.portProofEligible &&
105
+ edgeKind !== 'require' &&
106
+ edgeKind !== 'dynamic-import' &&
107
+ !violation?.typeOnly) {
108
+ return {
109
+ class: 'judgment',
110
+ confidence: 0.82,
111
+ remediationKind: 'port-proof-inject-binding',
112
+ rationale: 'Port-proof shape: single named value import used only as binding.method(...) in function declarations. Inject as a port parameter (body-local calls preserved) — outer layer must pass the impl. Not mechanical-safe auto-apply: call arity changes. Apply via agent judgment / multi-file plan.',
113
+ };
114
+ }
94
115
  return {
95
116
  class: 'judgment',
96
117
  confidence: 0.7,
package/dist/index.cjs CHANGED
@@ -80,7 +80,7 @@ __export(index_exports, {
80
80
  module.exports = __toCommonJS(index_exports);
81
81
 
82
82
  // src/version.ts
83
- var version = "2.9.1";
83
+ var version = "2.10.0";
84
84
 
85
85
  // src/kernel/intent/IntentRegistry.ts
86
86
  var IntentRegistry = class {
@@ -2255,7 +2255,9 @@ function extractModuleSpecifiers(source) {
2255
2255
  let match;
2256
2256
  while ((match = pattern.re.exec(source)) !== null) {
2257
2257
  const index = match.index + match[0].indexOf(match[1]);
2258
- matches.push({ value: match[1], index, kind: pattern.kind });
2258
+ const raw = match[0];
2259
+ const typeOnly = pattern.kind === "import" && /\bimport\s+type\b/.test(raw) || pattern.kind === "export" && /\bexport\s+type\b/.test(raw);
2260
+ matches.push({ value: match[1], index, kind: pattern.kind, typeOnly });
2259
2261
  }
2260
2262
  }
2261
2263
  return matches.sort((a, b) => a.index - b.index);
@@ -2495,6 +2497,9 @@ function createAICodeGate(options = {}) {
2495
2497
  }
2496
2498
  );
2497
2499
  if (blocked) {
2500
+ if (specifier.typeOnly && !blocked.peerIsolation) {
2501
+ continue;
2502
+ }
2498
2503
  const peer = Boolean(blocked.peerIsolation);
2499
2504
  violations.push(
2500
2505
  violation(
@@ -2508,7 +2513,11 @@ function createAICodeGate(options = {}) {
2508
2513
  fromLayer: contextLayer,
2509
2514
  toLayer: targetLayer,
2510
2515
  suggestion: peer ? "Extract shared code to a shared layer, or coordinate slices via events/ports \u2014 do not import across feature/context slices." : "Depend on a port/interface owned by an inner layer instead, or move this code to a layer allowed to make this import.",
2511
- details: { importKind: specifier.kind, peerIsolation: peer }
2516
+ details: {
2517
+ importKind: specifier.kind,
2518
+ peerIsolation: peer,
2519
+ ...specifier.typeOnly ? { typeOnly: true } : {}
2520
+ }
2512
2521
  }
2513
2522
  )
2514
2523
  );
@@ -2518,7 +2527,7 @@ function createAICodeGate(options = {}) {
2518
2527
  continue;
2519
2528
  }
2520
2529
  }
2521
- if (exemptFromInfraHeuristics) continue;
2530
+ if (exemptFromInfraHeuristics || specifier.typeOnly) continue;
2522
2531
  if (!hasInfrastructureToken(specifier.value) && !isKnownInfrastructurePackage(specifier.value)) {
2523
2532
  continue;
2524
2533
  }