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 +27 -0
- package/bin/ark-check.mjs +85 -6
- package/bin/ark-shared.mjs +43 -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,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:
|
|
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,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 {
|
|
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:
|
|
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
|
}
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -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
|
|
545
|
-
*
|
|
546
|
-
*
|
|
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
|
-
'
|
|
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';
|