arkgate 2.1.1 → 2.2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are documented here.
4
4
 
5
+ ## 2.2.0 — 2026-07-08
6
+
7
+ ### Added — co-pilot P0 depth (mechanical-safe expansion)
8
+
9
+ - **Second `mechanical-safe` class:** static value-syntax imports of **pure type-only modules**
10
+ (only `export type` / `interface` + type-only imports; **no** top-level runtime statements).
11
+ Flagged `targetTypeOnlyExports` → convert to `import type`. Mixed modules, side-effecting
12
+ type files, `require()` / dynamic `import()` stay **judgment** (zero false-safe).
13
+ - **Scan cache v3** carries per-file `exportsOnlyTypes` (two-pass scan so targets resolve).
14
+ - **Classifier corpus** extended: type-only + pure-type static import = 2 auto steps; value
15
+ import, side-effect target, require/dynamic, forbidden global, cycles remain judgment.
16
+
5
17
  ## 2.1.1 — 2026-07-08
6
18
 
7
19
  ### Documentation
package/bin/ark-check.mjs CHANGED
@@ -2461,12 +2461,11 @@ function scanCacheKey(root, args) {
2461
2461
  : path.join(root, args.manifest)
2462
2462
  : undefined;
2463
2463
  // Bump this schema tag whenever the cached scan shape changes, so a warm cache from an
2464
- // older Ark can't feed stale entries to new logic. v2: violation/edge records gained the
2465
- // `typeOnly` field a v1 cache would otherwise report every violation as a value edge
2466
- // after upgrade until files changed. The tag invalidates every existing cache exactly once.
2464
+ // older Ark can't feed stale entries to new logic. v2: typeOnly on edges. v3: per-file
2465
+ // exportsOnlyTypes (target-module type-only export detection for plan classifier).
2467
2466
  return crypto
2468
2467
  .createHash('sha1')
2469
- .update(`ark-check-cache-v2\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
2468
+ .update(`ark-check-cache-v3\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
2470
2469
  .digest('hex');
2471
2470
  }
2472
2471
 
@@ -2588,6 +2587,58 @@ function isTypeOnlyModuleReference(ts, node) {
2588
2587
  return false;
2589
2588
  }
2590
2589
 
2590
+ /**
2591
+ * True when a module is a pure type-surface file: only type/interface exports and
2592
+ * type-only imports. Conservative false (→ judgment) when:
2593
+ * - any top-level runtime statement (value decls, expression stmts, side-effect imports)
2594
+ * - ambiguous `export { X }` without type keyword, export *, default/export=
2595
+ * Used so static value-syntax `import { T }` of a pure-type module can be mechanical-safe
2596
+ * (convert to `import type`). Never trust this for require()/import() edges.
2597
+ */
2598
+ function sourceFileExportsOnlyTypes(ts, sourceFile) {
2599
+ let sawTypeExport = false;
2600
+ const hasExportModifier = (node) =>
2601
+ Array.isArray(node.modifiers) &&
2602
+ node.modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
2603
+
2604
+ for (const stmt of sourceFile.statements) {
2605
+ // Type-only imports OK; value or side-effect imports mean runtime load of deps.
2606
+ if (ts.isImportDeclaration(stmt)) {
2607
+ if (!isTypeOnlyModuleReference(ts, stmt)) return false;
2608
+ continue;
2609
+ }
2610
+ if (typeof ts.isImportEqualsDeclaration === 'function' && ts.isImportEqualsDeclaration(stmt)) {
2611
+ return false;
2612
+ }
2613
+ if (ts.isExportDeclaration(stmt)) {
2614
+ if (stmt.isTypeOnly) {
2615
+ sawTypeExport = true;
2616
+ continue;
2617
+ }
2618
+ // export * from '…' can re-export values — not provably type-only.
2619
+ if (!stmt.exportClause) return false;
2620
+ if (ts.isNamespaceExport(stmt.exportClause)) return false;
2621
+ if (ts.isNamedExports(stmt.exportClause)) {
2622
+ if (stmt.exportClause.elements.length === 0) return false;
2623
+ for (const el of stmt.exportClause.elements) {
2624
+ if (!el.isTypeOnly) return false; // bare `export { X }` — ambiguous without checker
2625
+ }
2626
+ sawTypeExport = true;
2627
+ continue;
2628
+ }
2629
+ return false;
2630
+ }
2631
+ if (ts.isExportAssignment(stmt)) return false; // export = / export default expr
2632
+ if (ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) {
2633
+ if (hasExportModifier(stmt)) sawTypeExport = true;
2634
+ continue;
2635
+ }
2636
+ // Any other top-level statement (const/fn/class/enum, console.log, if, …) is runtime.
2637
+ return false;
2638
+ }
2639
+ return sawTypeExport;
2640
+ }
2641
+
2591
2642
  function propertyName(ts, node) {
2592
2643
  if (!node) return undefined;
2593
2644
  if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) return node.text;
@@ -4275,6 +4326,7 @@ function buildRemediationPlan(root, activeViolations, governedPercent = null, to
4275
4326
  ...(v.line ? { line: v.line } : {}),
4276
4327
  ...(v.target ? { target: v.target } : {}),
4277
4328
  ...(v.typeOnly ? { typeOnly: true } : {}),
4329
+ ...(v.targetTypeOnlyExports ? { targetTypeOnlyExports: true } : {}),
4278
4330
  };
4279
4331
  });
4280
4332
  // Order: auto-applicable first (quick, safe wins), then human decisions, then deferred.
@@ -4836,10 +4888,17 @@ async function main() {
4836
4888
  ts.forEachChild(node, visit);
4837
4889
  };
4838
4890
  visit(sourceFile);
4839
- return { contentViolations: violations, edges };
4891
+ return {
4892
+ contentViolations: violations,
4893
+ edges,
4894
+ exportsOnlyTypes: sourceFileExportsOnlyTypes(ts, sourceFile),
4895
+ };
4840
4896
  }
4841
4897
 
4898
+ // Pass 1: scan every governed file into nextCacheFiles (needs complete map before
4899
+ // targetTypeOnlyExports can be resolved for import edges).
4842
4900
  const importGraph = new Map();
4901
+ const scanned = []; // { file, sourceLayer, relFile, entry }
4843
4902
  for (const file of files) {
4844
4903
  const sourceLayer = layerForFile(root, file, config.layers);
4845
4904
  if (!sourceLayer) continue;
@@ -4853,7 +4912,11 @@ async function main() {
4853
4912
  ? cached
4854
4913
  : { fileKey, ...scanSourceFile(file, sourceLayer) };
4855
4914
  nextCacheFiles[relFile] = entry;
4915
+ scanned.push({ file, sourceLayer, relFile, entry });
4916
+ }
4856
4917
 
4918
+ // Pass 2: content violations + layer edges (with target type-export surface).
4919
+ for (const { file, sourceLayer, relFile, entry } of scanned) {
4857
4920
  violations.push(...entry.contentViolations);
4858
4921
  for (const edge of entry.edges) {
4859
4922
  const target = resolveImport(ts, edge.specifier, file, compilerOptionsFor(file), moduleHost, root);
@@ -4864,14 +4927,24 @@ async function main() {
4864
4927
  }
4865
4928
  const rule = targetLayer ? isBlocked(rules, sourceLayer, targetLayer) : undefined;
4866
4929
  if (rule) {
4930
+ const relTarget = normalize(path.relative(root, target));
4931
+ // After pass 1 every in-scope target is in nextCacheFiles. Missing → not type-only.
4932
+ // targetTypeOnlyExports only for static import/export declarations — never require()
4933
+ // or dynamic import(), which always load the module at runtime (side effects matter).
4934
+ const targetCached = nextCacheFiles[relTarget];
4935
+ const staticEdge = edge.kind === 'import' || edge.kind === 'export';
4936
+ const targetTypeOnlyExports =
4937
+ staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
4867
4938
  violations.push({
4868
4939
  ruleId: 'LAYER_IMPORT_VIOLATION',
4869
4940
  file: relFile,
4870
4941
  line: edge.line,
4871
4942
  fromLayer: sourceLayer,
4872
4943
  toLayer: targetLayer,
4873
- target: normalize(path.relative(root, target)),
4944
+ target: relTarget,
4874
4945
  ...(edge.typeOnly ? { typeOnly: true } : {}),
4946
+ ...(targetTypeOnlyExports ? { targetTypeOnlyExports: true } : {}),
4947
+ ...(edge.kind ? { edgeKind: edge.kind } : {}),
4875
4948
  message: rule.message ?? `${sourceLayer} must not ${edge.kind} ${targetLayer}.`,
4876
4949
  });
4877
4950
  }
@@ -541,9 +541,11 @@ export function looksLikeIntent(value) {
541
541
  * - 'deferred' : not enough signal to place it → a human should look first.
542
542
  *
543
543
  * Deliberately biased toward 'judgment': a false 'mechanical-safe' that auto-lands a bad edit
544
- * is the failure mode that sinks trust, so only the provably-safe type-only move earns 'auto'.
545
- * Pure function of one violation object ({ ruleId, typeOnly, ... }) so the CLI, the MCP gate,
546
- * and (later) the apply-loop all classify identically. Returns { class, confidence, rationale }.
544
+ * is the failure mode that sinks trust. Only statically-provable type-surface fixes earn 'auto':
545
+ * (1) import/export already marked type-only
546
+ * (2) value-syntax import of a module that *only* exports types (convert to import type)
547
+ * Pure function of one violation object so CLI, MCP, and apply-loop classify identically.
548
+ * Returns { class, confidence, rationale }.
547
549
  */
548
550
  export const REMEDIATION_CLASSES = ['mechanical-safe', 'judgment', 'deferred'];
549
551
 
@@ -558,6 +560,26 @@ export function classifyRemediation(violation) {
558
560
  'Type-only import (erased at runtime): move the type to the layer that owns it and re-export for back-compat. Behavior-preserving, and the gate verifies it.',
559
561
  };
560
562
  }
563
+ // Target module is a pure type-surface file AND the edge is a static import/export
564
+ // (flag only set on those edges). Value-syntax `import { T }` → convert to import type.
565
+ // require()/import() never get this flag (runtime load). Mixed modules stay judgment.
566
+ if (violation.targetTypeOnlyExports) {
567
+ const kind = violation.edgeKind;
568
+ if (kind === 'require' || kind === 'dynamic-import') {
569
+ return {
570
+ class: 'judgment',
571
+ confidence: 0.75,
572
+ rationale:
573
+ 'Runtime module load (require/import()) of a type-only module still executes the target file — not auto-safe; rewrite to a static import type if appropriate.',
574
+ };
575
+ }
576
+ return {
577
+ class: 'mechanical-safe',
578
+ confidence: 0.85,
579
+ rationale:
580
+ 'Static import targets a pure type-only module: convert to `import type` (erased at runtime) and place the type in a shared/owning layer. No runtime coupling; gate verifies.',
581
+ };
582
+ }
561
583
  return {
562
584
  class: 'judgment',
563
585
  confidence: 0.7,
@@ -1306,11 +1328,12 @@ export function enrichViolationWithFixClass(violation) {
1306
1328
  const enriched = { ...violation };
1307
1329
  switch (violation.ruleId) {
1308
1330
  case 'LAYER_IMPORT_VIOLATION':
1309
- if (violation.typeOnly) {
1331
+ if (violation.typeOnly || violation.targetTypeOnlyExports) {
1310
1332
  enriched.fixClass = 'file-move';
1311
1333
  enriched.effort = 'small';
1312
- enriched.enthusiastHint =
1313
- 'This is a type-only importmove the type to a layer both sides may share, or relocate the file to match its role.';
1334
+ enriched.enthusiastHint = violation.targetTypeOnlyExports
1335
+ ? 'The imported module only exports types use `import type` and place the type in a layer both sides may share.'
1336
+ : 'This is a type-only import — move the type to a layer both sides may share, or relocate the file to match its role.';
1314
1337
  } else {
1315
1338
  enriched.fixClass = 'port-inversion';
1316
1339
  enriched.effort = 'medium';
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.1.1";
83
+ var version = "2.2.0";
84
84
 
85
85
  // src/kernel/intent/IntentRegistry.ts
86
86
  var IntentRegistry = class {