arkgate 4.8.23 → 4.8.24
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 +68 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +24 -77
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +596 -112
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -493
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +16 -9
- package/bin/lib/doctor-advisories.mjs +25 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +77 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +130 -156
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +35 -34
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3312 -299
- package/dist/index.js +35 -34
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +84 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +196 -26
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +96 -19
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +47 -10
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -10,6 +10,35 @@ import {
|
|
|
10
10
|
HOST_SUPPORT_MATRIX,
|
|
11
11
|
} from './host-support-matrix.mjs';
|
|
12
12
|
import { KNOWN_TOOLS, normalizeToolsList } from './skill-install.mjs';
|
|
13
|
+
import {
|
|
14
|
+
antigravityHooks,
|
|
15
|
+
claudeSettings,
|
|
16
|
+
codexHooks,
|
|
17
|
+
cursorHooks,
|
|
18
|
+
mergeAntigravityArkHook,
|
|
19
|
+
mergeClaudeStyleArkHooks,
|
|
20
|
+
mergeCursorArkHook,
|
|
21
|
+
} from './hook-templates.mjs';
|
|
22
|
+
|
|
23
|
+
/** Hosts whose hook file the installer merges (keeping user keys) instead of replacing. */
|
|
24
|
+
const MERGEABLE_HOOK_FILES = {
|
|
25
|
+
claude: [claudeSettings, mergeClaudeStyleArkHooks],
|
|
26
|
+
codex: [codexHooks, mergeClaudeStyleArkHooks],
|
|
27
|
+
cursor: [cursorHooks, mergeCursorArkHook],
|
|
28
|
+
antigravity: [antigravityHooks, mergeAntigravityArkHook],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True when install will upsert Ark's hook into the existing host file without --force. */
|
|
32
|
+
export function hostHookFileMergeable(root, host, hookFile) {
|
|
33
|
+
const entry = MERGEABLE_HOOK_FILES[host];
|
|
34
|
+
if (!entry) return false;
|
|
35
|
+
try {
|
|
36
|
+
const [template, merge] = entry;
|
|
37
|
+
return merge(fs.readFileSync(hookFile, 'utf8'), template(root)) != null;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
13
42
|
|
|
14
43
|
export const HOST_ENFORCEMENT_SUPPORT = Object.freeze(
|
|
15
44
|
Object.fromEntries(
|
|
@@ -79,12 +108,19 @@ export function validateHardWriteRequest({ root, host, tools, force = false }) {
|
|
|
79
108
|
}
|
|
80
109
|
|
|
81
110
|
const hookFile = path.join(root, support.hookPath);
|
|
82
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
fs.existsSync(hookFile) &&
|
|
113
|
+
!force &&
|
|
114
|
+
!hasHardWriteHook(root, normalizedHost) &&
|
|
115
|
+
!hostHookFileMergeable(root, normalizedHost, hookFile)
|
|
116
|
+
) {
|
|
83
117
|
return {
|
|
84
118
|
ok: false,
|
|
85
|
-
error:
|
|
86
|
-
`${support.hookPath} already exists
|
|
87
|
-
|
|
119
|
+
error: MERGEABLE_HOOK_FILES[normalizedHost]
|
|
120
|
+
? `${support.hookPath} already exists but is not a JSON object Ark can merge into, so it would be preserved. ` +
|
|
121
|
+
'Fix that file (or use --force to replace it), or omit --require-write-hook for merge-only enforcement.'
|
|
122
|
+
: `${support.hookPath} already exists without an Ark hard-write hook and would be preserved. ` +
|
|
123
|
+
'Use --force to replace that host file, or omit --require-write-hook for merge-only enforcement.',
|
|
88
124
|
};
|
|
89
125
|
}
|
|
90
126
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { NORTH_STAR_ONE_LINE } from './product-copy.mjs';
|
|
6
|
+
import { KNOWN_TOOLS } from './skill-install.mjs';
|
|
6
7
|
|
|
7
8
|
export function setupUsage() {
|
|
8
9
|
return `arkgate (alias ark) — Write. Check. Ship.
|
|
@@ -96,8 +97,8 @@ Options:
|
|
|
96
97
|
Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
|
|
97
98
|
worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike,
|
|
98
99
|
vertical-slice-product, ddd-bounded-contexts.
|
|
99
|
-
--tools One active agent host for start (
|
|
100
|
-
Omit to use the active host; an unknown host
|
|
100
|
+
--tools One active agent host for start (${KNOWN_TOOLS.join(',')}).
|
|
101
|
+
Omit to use the active host; a list or an unknown host is rejected (exit 2).
|
|
101
102
|
--remove-host <host>
|
|
102
103
|
Preview or apply removal of that compact host integration; re-add it with --tools <host>.
|
|
103
104
|
--require-write-hook <host>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importer graph for the flat-parent doctor suggestion.
|
|
3
|
+
*
|
|
4
|
+
* Prefers the resolved dependency facts ark-check already built (tsconfig
|
|
5
|
+
* paths included). Without facts it scans the text and resolves relative
|
|
6
|
+
* specifiers plus tsconfig path aliases, so an `@/…` importer is never
|
|
7
|
+
* invisible. Advisory input only.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { readTsconfigAliases, resolveSpecifierToRel } from './import-resolve.mjs';
|
|
12
|
+
|
|
13
|
+
const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
|
|
14
|
+
const EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
15
|
+
|
|
16
|
+
function withoutComments(text) {
|
|
17
|
+
let out = '';
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < text.length) {
|
|
20
|
+
const c = text[i];
|
|
21
|
+
const n = text[i + 1];
|
|
22
|
+
if (c === '/' && n === '/') {
|
|
23
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (c === '/' && n === '*') {
|
|
27
|
+
i += 2;
|
|
28
|
+
while (i + 1 < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
29
|
+
i = Math.min(text.length, i + 2);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
out += c;
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every string module specifier in `text` (relative and bare). */
|
|
39
|
+
export function moduleSpecifiers(text) {
|
|
40
|
+
const specs = [];
|
|
41
|
+
const source = withoutComments(String(text));
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < source.length) {
|
|
44
|
+
let at = -1;
|
|
45
|
+
let mark = '';
|
|
46
|
+
for (const candidate of SPEC_MARKS) {
|
|
47
|
+
const found = source.indexOf(candidate, i);
|
|
48
|
+
if (found !== -1 && (at === -1 || found < at)) {
|
|
49
|
+
at = found;
|
|
50
|
+
mark = candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (at === -1) break;
|
|
54
|
+
const quote = mark[mark.length - 1];
|
|
55
|
+
const start = at + mark.length;
|
|
56
|
+
const end = source.indexOf(quote, start);
|
|
57
|
+
if (end === -1) break;
|
|
58
|
+
const spec = source.slice(start, end);
|
|
59
|
+
if (spec.length > 0) specs.push(spec);
|
|
60
|
+
i = end + 1;
|
|
61
|
+
}
|
|
62
|
+
return specs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveRelative(fromFile, spec) {
|
|
66
|
+
const base = fromFile.split('/');
|
|
67
|
+
base.pop();
|
|
68
|
+
for (const part of spec.split('/')) {
|
|
69
|
+
if (part === '' || part === '.') continue;
|
|
70
|
+
if (part === '..') {
|
|
71
|
+
if (base.length === 0) return null;
|
|
72
|
+
base.pop();
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
base.push(part);
|
|
76
|
+
}
|
|
77
|
+
return base.join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function probe(joined, fileSet) {
|
|
81
|
+
if (!joined) return null;
|
|
82
|
+
const names = [joined];
|
|
83
|
+
for (const ext of EXT) names.push(`${joined}${ext}`, `${joined}/index${ext}`);
|
|
84
|
+
const stem = joined.replace(/\.(?:m|c)?js$/, '');
|
|
85
|
+
if (stem !== joined) for (const ext of EXT) names.push(`${stem}${ext}`);
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
if (fileSet.has(name)) return name;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readText(root, rel) {
|
|
93
|
+
try {
|
|
94
|
+
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addEdge(graph, target, importer) {
|
|
101
|
+
let set = graph.get(target);
|
|
102
|
+
if (!set) {
|
|
103
|
+
set = new Set();
|
|
104
|
+
graph.set(target, set);
|
|
105
|
+
}
|
|
106
|
+
set.add(importer);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* target file → every file that imports it (value and type-only edges; a
|
|
111
|
+
* peerIsolation wall judges both).
|
|
112
|
+
* @param {{ root: string, files: string[], facts?: object, ts?: object }} input
|
|
113
|
+
*/
|
|
114
|
+
export function flatParentImporterGraph({ root, files, facts, ts }) {
|
|
115
|
+
const graph = new Map();
|
|
116
|
+
const fileSet = new Set(files);
|
|
117
|
+
const dependencies = Array.isArray(facts?.dependencies) ? facts.dependencies : null;
|
|
118
|
+
if (dependencies && dependencies.length > 0) {
|
|
119
|
+
for (const dep of dependencies) {
|
|
120
|
+
if (dep?.resolution !== 'resolved-project' || typeof dep.target !== 'string') continue;
|
|
121
|
+
if (typeof dep.from !== 'string' || dep.from === dep.target) continue;
|
|
122
|
+
addEdge(graph, dep.target, dep.from);
|
|
123
|
+
}
|
|
124
|
+
return graph;
|
|
125
|
+
}
|
|
126
|
+
const aliases = readTsconfigAliases(ts, root);
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
for (const spec of moduleSpecifiers(readText(root, file))) {
|
|
129
|
+
const joined = spec.startsWith('.')
|
|
130
|
+
? resolveRelative(file, spec)
|
|
131
|
+
: resolveSpecifierToRel(spec, file, root, aliases);
|
|
132
|
+
const target = probe(joined, fileSet);
|
|
133
|
+
if (target && target !== file) addEdge(graph, target, file);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return graph;
|
|
137
|
+
}
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
* Doctor suggestion: a universe-level flat file that one child slice imports.
|
|
3
3
|
* Advisory only. Not a finding, not a verdict, not a config field.
|
|
4
4
|
*/
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
5
|
import {
|
|
6
|
+
findDeniedEdgeDecision,
|
|
8
7
|
layerForRelativePath,
|
|
9
8
|
resolveGovernedSlice,
|
|
10
9
|
sliceAliasDestination,
|
|
11
10
|
sliceIdForPath,
|
|
12
11
|
} from '../ark-layer-match.mjs';
|
|
13
12
|
import { destinationKeepsLayerAndSlice } from './physical-cohesion.mjs';
|
|
14
|
-
|
|
15
|
-
const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
|
|
16
|
-
const EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
13
|
+
import { flatParentImporterGraph } from './flat-parent-importers.mjs';
|
|
17
14
|
|
|
18
15
|
function ruleKey(rule) {
|
|
19
16
|
const child = rule?.childSlices;
|
|
@@ -45,102 +42,41 @@ function isFlatParent(rel, rule) {
|
|
|
45
42
|
return Boolean(universe && raw && raw.toLowerCase() === universe.toLowerCase());
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Importers of each flat file, split by child. A file any non-child imports
|
|
47
|
+
* (universe common, a shared root, another universe, another layer) is marked:
|
|
48
|
+
* moving it into one child would turn that edge into a wall deny.
|
|
49
|
+
*/
|
|
50
|
+
function childImporters(flatFiles, graph, rule) {
|
|
51
|
+
const out = new Map();
|
|
52
|
+
for (const target of flatFiles) {
|
|
53
|
+
const universe = resolveGovernedSlice(target, rule).universeId ?? '';
|
|
54
|
+
const ids = new Set();
|
|
55
|
+
let nonChild = false;
|
|
56
|
+
for (const importer of graph.get(target) ?? []) {
|
|
57
|
+
const place = resolveGovernedSlice(importer, rule);
|
|
58
|
+
if (!place.childId || (place.universeId ?? '') !== universe) nonChild = true;
|
|
59
|
+
else ids.add(place.childId);
|
|
63
60
|
}
|
|
64
|
-
out
|
|
65
|
-
i += 1;
|
|
61
|
+
out.set(target, { ids, nonChild, importers: [...(graph.get(target) ?? [])] });
|
|
66
62
|
}
|
|
67
63
|
return out;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (at === -1) break;
|
|
85
|
-
const quote = mark[mark.length - 1];
|
|
86
|
-
const start = at + mark.length;
|
|
87
|
-
const end = source.indexOf(quote, start);
|
|
88
|
-
if (end === -1) break;
|
|
89
|
-
const spec = source.slice(start, end);
|
|
90
|
-
if (spec.startsWith('.')) specs.push(spec);
|
|
91
|
-
i = end + 1;
|
|
92
|
-
}
|
|
93
|
-
return specs;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function resolveRelative(fromFile, spec, fileSet) {
|
|
97
|
-
const base = fromFile.split('/');
|
|
98
|
-
base.pop();
|
|
99
|
-
for (const part of spec.split('/')) {
|
|
100
|
-
if (part === '' || part === '.') continue;
|
|
101
|
-
if (part === '..') {
|
|
102
|
-
if (base.length === 0) return null;
|
|
103
|
-
base.pop();
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
base.push(part);
|
|
107
|
-
}
|
|
108
|
-
const joined = base.join('/');
|
|
109
|
-
const names = [joined];
|
|
110
|
-
for (const ext of EXT) names.push(`${joined}${ext}`, `${joined}/index${ext}`);
|
|
111
|
-
for (const name of names) {
|
|
112
|
-
if (fileSet.has(name)) return name;
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function readText(root, rel) {
|
|
118
|
-
try {
|
|
119
|
-
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
120
|
-
} catch {
|
|
121
|
-
return '';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function childImporters(flatFiles, files, rule, root) {
|
|
126
|
-
const flat = new Set(flatFiles);
|
|
127
|
-
const known = new Set(files);
|
|
128
|
-
const importers = new Map();
|
|
129
|
-
for (const file of files) {
|
|
130
|
-
const place = resolveGovernedSlice(file, rule);
|
|
131
|
-
if (!place.childId) continue;
|
|
132
|
-
for (const spec of relativeSpecifiers(readText(root, file))) {
|
|
133
|
-
const target = resolveRelative(file, spec, known);
|
|
134
|
-
if (!target || !flat.has(target) || target === file) continue;
|
|
135
|
-
let ids = importers.get(target);
|
|
136
|
-
if (!ids) {
|
|
137
|
-
ids = new Set();
|
|
138
|
-
importers.set(target, ids);
|
|
139
|
-
}
|
|
140
|
-
ids.add(place.childId);
|
|
141
|
-
}
|
|
66
|
+
/** Would any recorded importer be denied by the rules after the move? */
|
|
67
|
+
function moveBreaksWall(importers, destFile, rules, layers) {
|
|
68
|
+
const toLayer = layerForRelativePath(destFile, layers);
|
|
69
|
+
for (const importer of importers) {
|
|
70
|
+
const fromLayer = layerForRelativePath(importer, layers);
|
|
71
|
+
if (!fromLayer || !toLayer) return true;
|
|
72
|
+
const denied = findDeniedEdgeDecision(rules, fromLayer, toLayer, {
|
|
73
|
+
fromPath: importer,
|
|
74
|
+
toPath: destFile,
|
|
75
|
+
layers,
|
|
76
|
+
});
|
|
77
|
+
if (denied) return true;
|
|
142
78
|
}
|
|
143
|
-
return
|
|
79
|
+
return false;
|
|
144
80
|
}
|
|
145
81
|
|
|
146
82
|
function basename(rel) {
|
|
@@ -149,7 +85,7 @@ function basename(rel) {
|
|
|
149
85
|
}
|
|
150
86
|
|
|
151
87
|
/**
|
|
152
|
-
* @param {{ root: string, files: string[], rules: object[] | undefined, layers: object[] | undefined }} input
|
|
88
|
+
* @param {{ root: string, files: string[], rules: object[] | undefined, layers: object[] | undefined, facts?: object, ts?: object }} input
|
|
153
89
|
* @returns {object | null}
|
|
154
90
|
*/
|
|
155
91
|
export function collectFlatParentPilot(input) {
|
|
@@ -161,15 +97,18 @@ export function collectFlatParentPilot(input) {
|
|
|
161
97
|
const layers = input.layers;
|
|
162
98
|
const moves = [];
|
|
163
99
|
const seen = new Set();
|
|
100
|
+
let graph = null;
|
|
164
101
|
for (const rule of rules) {
|
|
165
102
|
const flat = files.filter((file) => isFlatParent(file, rule));
|
|
166
103
|
if (flat.length === 0) continue;
|
|
167
|
-
|
|
104
|
+
graph ??= flatParentImporterGraph({ root: input.root, files, facts: input.facts, ts: input.ts });
|
|
105
|
+
const importers = childImporters(flat, graph, rule);
|
|
168
106
|
for (const file of flat) {
|
|
169
107
|
if (seen.has(file)) continue;
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
108
|
+
const entry = importers.get(file);
|
|
109
|
+
// Exactly one child and nobody else: two children, or any non-child importer, keeps it in place.
|
|
110
|
+
if (!entry || entry.nonChild || entry.ids.size !== 1) continue;
|
|
111
|
+
const importer = [...entry.ids][0];
|
|
173
112
|
const destination = sliceAliasDestination(file, importer, rule.childSlices);
|
|
174
113
|
if (!destination) continue;
|
|
175
114
|
const destFile = `${destination}/${basename(file)}`;
|
|
@@ -177,6 +116,7 @@ export function collectFlatParentPilot(input) {
|
|
|
177
116
|
if (!layer || layer !== layerForRelativePath(destFile, layers)) continue;
|
|
178
117
|
const toPlace = resolveGovernedSlice(destFile, rule);
|
|
179
118
|
if ((toPlace.childId ?? '') !== importer) continue;
|
|
119
|
+
if (moveBreaksWall(entry.importers, destFile, input.rules, layers)) continue;
|
|
180
120
|
const fromPlace = resolveGovernedSlice(file, rule);
|
|
181
121
|
const keepsSlice = destinationKeepsLayerAndSlice(file, destFile, layers, [rule]);
|
|
182
122
|
const wallSame =
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { codexProjectMcpIsValid } from './codex-home.mjs';
|
|
8
9
|
import { enforcingArkRunText, runsArkCheck } from './github-enforcement.mjs';
|
|
10
|
+
import { npxArkgatePrefixLength } from './package-manager.mjs';
|
|
9
11
|
|
|
10
12
|
export const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
13
|
export const __arkCheckCli = path.join(__packageRoot, 'bin', 'ark-check.mjs');
|
|
@@ -86,10 +88,27 @@ export function treeHasTypecheckScript(root) {
|
|
|
86
88
|
* @param {{ write?: boolean }} [opts]
|
|
87
89
|
* @returns {{
|
|
88
90
|
* changed: boolean,
|
|
89
|
-
* reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json',
|
|
91
|
+
* reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json' | 'no-typescript',
|
|
90
92
|
* script?: string,
|
|
91
93
|
* }}
|
|
92
94
|
*/
|
|
95
|
+
/**
|
|
96
|
+
* True when `typescript` is declared by the project (deps/devDeps/peerDeps) or resolves
|
|
97
|
+
* from its root (e.g. hoisted in a workspace), so a generated `tsc --noEmit` can run.
|
|
98
|
+
*/
|
|
99
|
+
export function projectHasTypeScript(root, pkg = readPackageJson(root) || {}) {
|
|
100
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
101
|
+
const deps = pkg?.[field];
|
|
102
|
+
if (deps && typeof deps === 'object' && Object.hasOwn(deps, 'typescript')) return true;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
createRequire(path.join(root, 'package.json')).resolve('typescript/package.json');
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
export function ensureTypecheckScript(root, opts = {}) {
|
|
94
113
|
const write = opts.write !== false;
|
|
95
114
|
const hasTsconfig =
|
|
@@ -105,6 +124,8 @@ export function ensureTypecheckScript(root, opts = {}) {
|
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
const pkg = readPackageJson(root) || {};
|
|
127
|
+
// `tsc` only exists when typescript resolves — otherwise CI fails with `tsc: not found`.
|
|
128
|
+
if (!projectHasTypeScript(root, pkg)) return { changed: false, reason: 'no-typescript' };
|
|
108
129
|
const scripts =
|
|
109
130
|
pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : {};
|
|
110
131
|
const script = 'tsc --noEmit';
|
|
@@ -194,7 +215,10 @@ function arkMcpArgs(server) {
|
|
|
194
215
|
const isArkBin = (value) => /^(?:arkgate-mcp|ark-mcp)(?:\.mjs)?$/.test(executableName(value));
|
|
195
216
|
if ([server.command, ...args].filter(isArkBin).length !== 1) return null;
|
|
196
217
|
if (isArkBin(server.command)) return args;
|
|
197
|
-
|
|
218
|
+
const npxPrefix = command === 'npx' ? npxArkgatePrefixLength(args) : 0;
|
|
219
|
+
if ((command === 'npx' || command === 'yarn') && isArkBin(args[npxPrefix])) {
|
|
220
|
+
return args.slice(npxPrefix + 1);
|
|
221
|
+
}
|
|
198
222
|
if (command === 'pnpm') {
|
|
199
223
|
const binIndex =
|
|
200
224
|
args[0] === 'exec'
|
|
@@ -285,6 +309,9 @@ export function hasArkMcpRegistration(root, relativePath = '.mcp.json') {
|
|
|
285
309
|
*/
|
|
286
310
|
export const MCP_JSON_GATE_FILES = ['.mcp.json', '.cursor/mcp.json', '.agents/mcp_config.json'];
|
|
287
311
|
|
|
312
|
+
/** Claude-style hook files: Ark SessionStart/PreToolUse groups are merged, never whole-file replaced. */
|
|
313
|
+
export const CLAUDE_STYLE_HOOK_FILES = ['.claude/settings.json', '.codex/hooks.json'];
|
|
314
|
+
|
|
288
315
|
/**
|
|
289
316
|
* Upsert mcpServers.ark into an existing MCP JSON document.
|
|
290
317
|
* Preserves sibling servers and unknown top-level keys. Returns null if unreadable.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git plumbing for diff-scoped checks (`--changed`, `--local`, `--against`, `--persona`).
|
|
3
|
+
*
|
|
4
|
+
* Every path this module returns or reads is relative to the Ark ROOT (the `-C` directory),
|
|
5
|
+
* never to the repository top level, so a package inside a monorepo sees its own files:
|
|
6
|
+
* - `git diff --relative` strips the package prefix and drops paths outside the root;
|
|
7
|
+
* - `git ls-files --others` is already cwd-relative;
|
|
8
|
+
* - `<ref>:./<path>` makes `git show` resolve from the root instead of the top level.
|
|
9
|
+
* Output is NUL-separated (`-z`) so core.quotePath never C-quotes a non-ASCII name.
|
|
10
|
+
* Every listing is checked: a failed or truncated call is an error, never an empty diff.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/** Kill hung git instead of stalling CI. */
|
|
16
|
+
export const SPAWN_TIMEOUT_MS = 8000;
|
|
17
|
+
/** Same ceiling as design-delta: a large untracked tree must not overflow the default 1 MiB. */
|
|
18
|
+
export const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
/** Refs tried, in order, when no `--base` / `--against` is given. */
|
|
21
|
+
export const TEAM_BASE_CANDIDATES = Object.freeze([
|
|
22
|
+
'origin/dev',
|
|
23
|
+
'origin/main',
|
|
24
|
+
'origin/master',
|
|
25
|
+
'dev',
|
|
26
|
+
'main',
|
|
27
|
+
'master',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function runGit(cwd, args) {
|
|
31
|
+
return spawnSync('git', ['-C', cwd, '-c', 'core.quotePath=false', ...args], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
35
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function safeGitRef(value) {
|
|
40
|
+
return (
|
|
41
|
+
typeof value === 'string' &&
|
|
42
|
+
/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/.test(value) &&
|
|
43
|
+
!value.includes('..')
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function discoverTeamBaseRef(root, preferred) {
|
|
48
|
+
if (safeGitRef(preferred)) return preferred;
|
|
49
|
+
for (const candidate of TEAM_BASE_CANDIDATES) {
|
|
50
|
+
const exists = runGit(root, ['rev-parse', '--verify', `${candidate}^{commit}`]);
|
|
51
|
+
if (exists.status === 0) return candidate;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function gitFailure(label, result) {
|
|
57
|
+
if (result.error) return `git ${label} failed: ${result.error.code || result.error.message}`;
|
|
58
|
+
if (result.status !== 0) {
|
|
59
|
+
const detail = String(result.stderr || '').trim();
|
|
60
|
+
return `git ${label} failed${detail ? `: ${detail}` : ` (status ${result.status})`}`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Root-relative paths changed vs `baseRef` (committed since the merge base, staged,
|
|
67
|
+
* unstaged, untracked). Fails closed: any git failure returns `ok: false`.
|
|
68
|
+
*/
|
|
69
|
+
export function listChangedPaths(root, baseRef) {
|
|
70
|
+
if (!safeGitRef(baseRef)) {
|
|
71
|
+
return { ok: false, paths: [], error: 'Invalid or missing git base ref.' };
|
|
72
|
+
}
|
|
73
|
+
const verify = runGit(root, ['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
74
|
+
if (verify.status !== 0) {
|
|
75
|
+
return { ok: false, paths: [], error: `Cannot resolve git ref ${baseRef}.` };
|
|
76
|
+
}
|
|
77
|
+
const calls = [
|
|
78
|
+
[`diff ${baseRef}...HEAD`, ['diff', '--relative', '--name-only', '-z', `${baseRef}...HEAD`]],
|
|
79
|
+
['diff (unstaged)', ['diff', '--relative', '--name-only', '-z']],
|
|
80
|
+
['diff --cached', ['diff', '--relative', '--name-only', '-z', '--cached']],
|
|
81
|
+
['ls-files --others', ['ls-files', '-z', '--others', '--exclude-standard']],
|
|
82
|
+
];
|
|
83
|
+
const paths = [];
|
|
84
|
+
for (const [label, gitArgs] of calls) {
|
|
85
|
+
const result = runGit(root, gitArgs);
|
|
86
|
+
const failure = gitFailure(label, result);
|
|
87
|
+
if (failure) return { ok: false, paths: [], error: failure };
|
|
88
|
+
paths.push(...String(result.stdout).split('\0'));
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, paths: [...new Set(paths.filter(Boolean))].sort(), error: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Root-relative form of `relPath` for a `<ref>:./<path>` spec; null when outside the root. */
|
|
94
|
+
export function rootRelativeGitPath(root, relPath) {
|
|
95
|
+
if (typeof relPath !== 'string' || !relPath) return null;
|
|
96
|
+
const rel = path.isAbsolute(relPath) ? path.relative(path.resolve(root), relPath) : relPath;
|
|
97
|
+
const normalized = path.posix.normalize(rel.split(path.sep).join('/'));
|
|
98
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) return null;
|
|
99
|
+
return normalized.replace(/^\.\//, '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** File text at `baseRef`, resolved relative to the Ark root (not the repo top level). */
|
|
103
|
+
export function gitShowText(root, baseRef, relPath) {
|
|
104
|
+
const rel = rootRelativeGitPath(root, relPath);
|
|
105
|
+
if (!safeGitRef(baseRef) || !rel) return null;
|
|
106
|
+
const shown = runGit(root, ['show', `${baseRef}:./${rel}`]);
|
|
107
|
+
if (shown.error || shown.status !== 0) return null;
|
|
108
|
+
return shown.stdout;
|
|
109
|
+
}
|
|
@@ -22,7 +22,7 @@ const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]
|
|
|
22
22
|
const SAFE_NEEDS_IF = /^['"]?\$\{\{\s*always\(\)\s*\}\}['"]?$/i;
|
|
23
23
|
const SAFE_CONTINUE = /^['"]?false['"]?$/i;
|
|
24
24
|
const ENV_PREFIX = /^(?:(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)/;
|
|
25
|
-
const DIRECT_ARK = /^(?:(?:npx
|
|
25
|
+
const DIRECT_ARK = /^(?:(?:npx(?:\s+(?:-y|--yes))?(?:\s+(?:-p\s+|--package[\s=])arkgate(?:@\S+)?)?|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
|
|
26
26
|
const CHECK_SCRIPT = /^(?:(?:npm|pnpm)\s+run\s+check:architecture(?=\s|$)|yarn(?:\s+run)?\s+check:architecture(?=\s|$))/;
|
|
27
27
|
const FAIL_CLOSED = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
|
|
28
28
|
|