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 +12 -0
- package/bin/ark-check.mjs +79 -6
- package/bin/ark-shared.mjs +29 -6
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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:
|
|
2465
|
-
//
|
|
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-
|
|
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 {
|
|
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:
|
|
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
|
}
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -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
|
|
545
|
-
*
|
|
546
|
-
*
|
|
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
|
-
'
|
|
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';
|