arkgate 2.1.1 → 2.3.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,33 @@
2
2
 
3
3
  All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are documented here.
4
4
 
5
+ ## 2.3.0 — 2026-07-08
6
+
7
+ ### Added — P0 complete (mechanical-safe depth + release-trust)
8
+
9
+ - **Third `mechanical-safe` remediation:** pure-type **file** relocate when the whole source
10
+ file is type-surface only (`sourcePureTypeModule` + type-only edge) —
11
+ `remediationKind: pure-type-file-relocate`.
12
+ - Keeps 2.2.0 classes: type-only import move; static import of pure-type target modules.
13
+ - **Deferred:** verbatim infra relocation of value modules (cannot prove behavior-preserving).
14
+ - **Release-trust:** `verify-release-tag` defaults to **fail-closed** on unsigned tags;
15
+ override only via `ARK_ALLOW_UNSIGNED_RELEASE_TAG=true` (publish workflow sets this
16
+ explicitly until GPG signing is wired). Unit tests cover policy + real script path.
17
+ - Corpus: pure-type file, pure-type target, side-effect type file, require/dynamic, value
18
+ import, forbidden global, cycles.
19
+
20
+ ## 2.2.0 — 2026-07-08
21
+
22
+ ### Added — co-pilot P0 depth (mechanical-safe expansion)
23
+
24
+ - **Second `mechanical-safe` class:** static value-syntax imports of **pure type-only modules**
25
+ (only `export type` / `interface` + type-only imports; **no** top-level runtime statements).
26
+ Flagged `targetTypeOnlyExports` → convert to `import type`. Mixed modules, side-effecting
27
+ type files, `require()` / dynamic `import()` stay **judgment** (zero false-safe).
28
+ - **Scan cache v3** carries per-file `exportsOnlyTypes` (two-pass scan so targets resolve).
29
+ - **Classifier corpus** extended: type-only + pure-type static import = 2 auto steps; value
30
+ import, side-effect target, require/dynamic, forbidden global, cycles remain judgment.
31
+
5
32
  ## 2.1.1 — 2026-07-08
6
33
 
7
34
  ### 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,9 @@ 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 } : {}),
4330
+ ...(v.sourcePureTypeModule ? { sourcePureTypeModule: true } : {}),
4331
+ ...(verdict.remediationKind ? { remediationKind: verdict.remediationKind } : {}),
4278
4332
  };
4279
4333
  });
4280
4334
  // Order: auto-applicable first (quick, safe wins), then human decisions, then deferred.
@@ -4836,10 +4890,17 @@ async function main() {
4836
4890
  ts.forEachChild(node, visit);
4837
4891
  };
4838
4892
  visit(sourceFile);
4839
- return { contentViolations: violations, edges };
4893
+ return {
4894
+ contentViolations: violations,
4895
+ edges,
4896
+ exportsOnlyTypes: sourceFileExportsOnlyTypes(ts, sourceFile),
4897
+ };
4840
4898
  }
4841
4899
 
4900
+ // Pass 1: scan every governed file into nextCacheFiles (needs complete map before
4901
+ // targetTypeOnlyExports can be resolved for import edges).
4842
4902
  const importGraph = new Map();
4903
+ const scanned = []; // { file, sourceLayer, relFile, entry }
4843
4904
  for (const file of files) {
4844
4905
  const sourceLayer = layerForFile(root, file, config.layers);
4845
4906
  if (!sourceLayer) continue;
@@ -4853,7 +4914,11 @@ async function main() {
4853
4914
  ? cached
4854
4915
  : { fileKey, ...scanSourceFile(file, sourceLayer) };
4855
4916
  nextCacheFiles[relFile] = entry;
4917
+ scanned.push({ file, sourceLayer, relFile, entry });
4918
+ }
4856
4919
 
4920
+ // Pass 2: content violations + layer edges (with target type-export surface).
4921
+ for (const { file, sourceLayer, relFile, entry } of scanned) {
4857
4922
  violations.push(...entry.contentViolations);
4858
4923
  for (const edge of entry.edges) {
4859
4924
  const target = resolveImport(ts, edge.specifier, file, compilerOptionsFor(file), moduleHost, root);
@@ -4864,14 +4929,28 @@ async function main() {
4864
4929
  }
4865
4930
  const rule = targetLayer ? isBlocked(rules, sourceLayer, targetLayer) : undefined;
4866
4931
  if (rule) {
4932
+ const relTarget = normalize(path.relative(root, target));
4933
+ // After pass 1 every in-scope target is in nextCacheFiles. Missing → not type-only.
4934
+ // targetTypeOnlyExports only for static import/export declarations — never require()
4935
+ // or dynamic import(), which always load the module at runtime (side effects matter).
4936
+ const targetCached = nextCacheFiles[relTarget];
4937
+ const staticEdge = edge.kind === 'import' || edge.kind === 'export';
4938
+ const targetTypeOnlyExports =
4939
+ staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
4940
+ // Importer is itself a pure type-surface file (no runtime body) — enables
4941
+ // pure-type-file-relocate classification when the edge is type-only.
4942
+ const sourcePureTypeModule = Boolean(entry.exportsOnlyTypes);
4867
4943
  violations.push({
4868
4944
  ruleId: 'LAYER_IMPORT_VIOLATION',
4869
4945
  file: relFile,
4870
4946
  line: edge.line,
4871
4947
  fromLayer: sourceLayer,
4872
4948
  toLayer: targetLayer,
4873
- target: normalize(path.relative(root, target)),
4949
+ target: relTarget,
4874
4950
  ...(edge.typeOnly ? { typeOnly: true } : {}),
4951
+ ...(targetTypeOnlyExports ? { targetTypeOnlyExports: true } : {}),
4952
+ ...(sourcePureTypeModule ? { sourcePureTypeModule: true } : {}),
4953
+ ...(edge.kind ? { edgeKind: edge.kind } : {}),
4875
4954
  message: rule.message ?? `${sourceLayer} must not ${edge.kind} ${targetLayer}.`,
4876
4955
  });
4877
4956
  }
@@ -541,23 +541,59 @@ 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) whole source file is pure type-surface + type-only edge relocate the file
546
+ * (2) import/export already marked type-only move/re-export the type
547
+ * (3) static value-syntax import of a pure type-only *target* module → import type
548
+ * Pure function of one violation object so CLI, MCP, and apply-loop classify identically.
549
+ * Returns { class, confidence, rationale, remediationKind? }.
547
550
  */
548
551
  export const REMEDIATION_CLASSES = ['mechanical-safe', 'judgment', 'deferred'];
549
552
 
550
553
  export function classifyRemediation(violation) {
551
554
  const ruleId = violation?.ruleId;
552
555
  if (ruleId === 'LAYER_IMPORT_VIOLATION') {
556
+ // Pure type-only *source file* with a type-only edge: relocating the whole file is
557
+ // behavior-preserving (no runtime body). Distinct from a single import-type move.
558
+ if (violation.typeOnly && violation.sourcePureTypeModule) {
559
+ return {
560
+ class: 'mechanical-safe',
561
+ confidence: 0.88,
562
+ remediationKind: 'pure-type-file-relocate',
563
+ rationale:
564
+ 'Whole source file is type-only surface (no runtime statements) with a type-only cross-layer edge: relocate the file to the owning layer (or extract the type there). Behavior-preserving; gate verifies.',
565
+ };
566
+ }
553
567
  if (violation.typeOnly) {
554
568
  return {
555
569
  class: 'mechanical-safe',
556
570
  confidence: 0.9,
571
+ remediationKind: 'type-only-import-move',
557
572
  rationale:
558
573
  '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
574
  };
560
575
  }
576
+ // Target module is a pure type-surface file AND the edge is a static import/export
577
+ // (flag only set on those edges). Value-syntax `import { T }` → convert to import type.
578
+ // require()/import() never get this flag (runtime load). Mixed modules stay judgment.
579
+ if (violation.targetTypeOnlyExports) {
580
+ const kind = violation.edgeKind;
581
+ if (kind === 'require' || kind === 'dynamic-import') {
582
+ return {
583
+ class: 'judgment',
584
+ confidence: 0.75,
585
+ rationale:
586
+ '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.',
587
+ };
588
+ }
589
+ return {
590
+ class: 'mechanical-safe',
591
+ confidence: 0.85,
592
+ remediationKind: 'import-type-from-pure-type-module',
593
+ rationale:
594
+ '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.',
595
+ };
596
+ }
561
597
  return {
562
598
  class: 'judgment',
563
599
  confidence: 0.7,
@@ -1306,11 +1342,12 @@ export function enrichViolationWithFixClass(violation) {
1306
1342
  const enriched = { ...violation };
1307
1343
  switch (violation.ruleId) {
1308
1344
  case 'LAYER_IMPORT_VIOLATION':
1309
- if (violation.typeOnly) {
1345
+ if (violation.typeOnly || violation.targetTypeOnlyExports) {
1310
1346
  enriched.fixClass = 'file-move';
1311
1347
  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.';
1348
+ enriched.enthusiastHint = violation.targetTypeOnlyExports
1349
+ ? 'The imported module only exports types use `import type` and place the type in a layer both sides may share.'
1350
+ : '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
1351
  } else {
1315
1352
  enriched.fixClass = 'port-inversion';
1316
1353
  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.3.0";
84
84
 
85
85
  // src/kernel/intent/IntentRegistry.ts
86
86
  var IntentRegistry = class {