arkgate 2.11.0 → 2.13.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 +147 -0
- package/README.md +70 -41
- package/bin/ark-check.mjs +95 -36
- package/bin/ark-mcp.mjs +11 -5
- package/bin/ark-shared.mjs +88 -56
- package/bin/ark.mjs +97 -29
- package/bin/lib/agent-gates.mjs +79 -2093
- package/bin/lib/architecture-scan.mjs +8 -0
- package/bin/lib/ci-and-commands.mjs +392 -0
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/deploy-path.mjs +205 -0
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- package/bin/lib/gate-files.mjs +223 -0
- package/bin/lib/hook-templates.mjs +99 -0
- package/bin/lib/host-support-matrix.mjs +77 -0
- package/bin/lib/install-migrate.mjs +473 -0
- package/bin/lib/mcp-adoption.mjs +455 -0
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +6 -2
- package/bin/lib/safety-diagnostics.mjs +31 -11
- package/bin/lib/skill-install.mjs +323 -0
- package/bin/lib/ts-resolve.mjs +2 -1
- package/bin/lib/typescript-host.mjs +88 -0
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +182 -0
- package/bin/lib/write-path-detect.mjs +101 -0
- package/dist/configContract-iBLxx5Tz.d.cts +53 -0
- package/dist/configContract-iBLxx5Tz.d.ts +53 -0
- package/dist/eslint/index.cjs +375 -13
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +30 -20
- package/dist/eslint/index.d.ts +30 -20
- package/dist/eslint/index.js +375 -13
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +723 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +716 -61
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +150 -42
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +2 -1
- package/dist/nestjs/index.d.ts +2 -1
- package/dist/nestjs/index.js +150 -42
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +723 -61
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +3 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +716 -61
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-BZ17b9i5.d.cts → types-BxBwnBpC.d.cts} +9 -36
- package/dist/{types-BZ17b9i5.d.ts → types-Wcs_l1_J.d.ts} +9 -36
- package/docs/agent-guide.md +43 -21
- package/docs/ai-gates.md +53 -18
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +3 -0
- package/docs/package-surface.md +22 -10
- package/docs/production-hardening.md +15 -2
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +3 -3
- package/package.json +15 -2
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-autopilot.md +77 -45
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +135 -34
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/configContract.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/config-contract.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ARK_CONFIG_SCHEMA_VERSION = '1.0';
|
|
12
|
+
export const ARK_CONFIG_SCHEMA_URL = 'https://unpkg.com/arkgate@2/schemas/ark.config.schema.json';
|
|
13
|
+
const DEFAULT_LAYER_NAMES = [
|
|
14
|
+
'DomainModel',
|
|
15
|
+
'ApplicationOrchestration',
|
|
16
|
+
'PersistenceAdapters',
|
|
17
|
+
'IntegrationAdapters',
|
|
18
|
+
'WorkflowSagaEngine',
|
|
19
|
+
'BackgroundJobsScheduling',
|
|
20
|
+
'PresentationAdapters',
|
|
21
|
+
'ReportingReadModels',
|
|
22
|
+
'ExtensibilityMetadata',
|
|
23
|
+
'SecurityAuditObservability',
|
|
24
|
+
'Kernel',
|
|
25
|
+
];
|
|
26
|
+
const DEFAULT_ALLOWED_FLOWS = new Set([
|
|
27
|
+
'PresentationAdapters->ApplicationOrchestration',
|
|
28
|
+
'ApplicationOrchestration->DomainModel',
|
|
29
|
+
'WorkflowSagaEngine->ApplicationOrchestration',
|
|
30
|
+
'WorkflowSagaEngine->DomainModel',
|
|
31
|
+
'BackgroundJobsScheduling->ApplicationOrchestration',
|
|
32
|
+
]);
|
|
33
|
+
function createDefaultRules() {
|
|
34
|
+
const rules = [];
|
|
35
|
+
for (const from of DEFAULT_LAYER_NAMES) {
|
|
36
|
+
for (const to of DEFAULT_LAYER_NAMES) {
|
|
37
|
+
if (from === to || DEFAULT_ALLOWED_FLOWS.has(`${from}->${to}`))
|
|
38
|
+
continue;
|
|
39
|
+
rules.push({ from, to, allowed: false });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return rules;
|
|
43
|
+
}
|
|
44
|
+
export const DEFAULT_ARK_CONFIG_RULES = createDefaultRules();
|
|
45
|
+
export const ARK_CONFIG_MIGRATIONS = [
|
|
46
|
+
{ from: 'unversioned', to: ARK_CONFIG_SCHEMA_VERSION },
|
|
47
|
+
];
|
|
48
|
+
const stringArraySchema = {
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: { type: 'string', minLength: 1 },
|
|
51
|
+
uniqueItems: true,
|
|
52
|
+
};
|
|
53
|
+
export const ARK_CONFIG_SCHEMA = {
|
|
54
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
55
|
+
$id: ARK_CONFIG_SCHEMA_URL,
|
|
56
|
+
title: 'ArkGate architecture contract',
|
|
57
|
+
description: 'Versioned contract consumed identically by ArkGate CLI, MCP, and ESLint surfaces.',
|
|
58
|
+
type: 'object',
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
required: ['$schema', 'schemaVersion', 'include', 'layers', 'rules'],
|
|
61
|
+
properties: {
|
|
62
|
+
$schema: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
minLength: 1,
|
|
65
|
+
default: ARK_CONFIG_SCHEMA_URL,
|
|
66
|
+
description: 'Editor-facing URL or local path for this JSON Schema.',
|
|
67
|
+
},
|
|
68
|
+
schemaVersion: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
const: ARK_CONFIG_SCHEMA_VERSION,
|
|
71
|
+
default: ARK_CONFIG_SCHEMA_VERSION,
|
|
72
|
+
},
|
|
73
|
+
name: { type: 'string', minLength: 1 },
|
|
74
|
+
include: { ...stringArraySchema, minItems: 1, default: ['src'] },
|
|
75
|
+
exclude: { ...stringArraySchema, default: [] },
|
|
76
|
+
excludeGenerated: { type: 'boolean', default: true },
|
|
77
|
+
frameworkOverlay: { type: 'string', minLength: 1 },
|
|
78
|
+
layers: {
|
|
79
|
+
type: 'array',
|
|
80
|
+
default: [],
|
|
81
|
+
items: { $ref: '#/$defs/layer' },
|
|
82
|
+
},
|
|
83
|
+
rules: {
|
|
84
|
+
type: 'array',
|
|
85
|
+
default: DEFAULT_ARK_CONFIG_RULES,
|
|
86
|
+
items: { $ref: '#/$defs/rule' },
|
|
87
|
+
},
|
|
88
|
+
cyclePolicy: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
enum: ['strict', 'soft', 'framework-soft', 'off'],
|
|
91
|
+
default: 'strict',
|
|
92
|
+
},
|
|
93
|
+
dynamicImportAllowlist: { ...stringArraySchema, default: [] },
|
|
94
|
+
safety: {
|
|
95
|
+
$ref: '#/$defs/safety',
|
|
96
|
+
default: {
|
|
97
|
+
maxTsSuppressions: 0,
|
|
98
|
+
maxAnyCasts: 0,
|
|
99
|
+
allowInMemory: false,
|
|
100
|
+
allowDisabledPeerIsolation: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
$defs: {
|
|
105
|
+
layer: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
required: ['name', 'patterns'],
|
|
109
|
+
properties: {
|
|
110
|
+
name: { type: 'string', minLength: 1 },
|
|
111
|
+
patterns: { ...stringArraySchema, minItems: 1 },
|
|
112
|
+
exclude: stringArraySchema,
|
|
113
|
+
intentPrefixes: stringArraySchema,
|
|
114
|
+
description: { type: 'string', minLength: 1 },
|
|
115
|
+
forbiddenGlobals: stringArraySchema,
|
|
116
|
+
mayImportInfrastructure: { type: 'boolean' },
|
|
117
|
+
optional: { type: 'boolean' },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
rule: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
required: ['from', 'to', 'allowed'],
|
|
124
|
+
properties: {
|
|
125
|
+
from: { type: 'string', minLength: 1 },
|
|
126
|
+
to: { type: 'string', minLength: 1 },
|
|
127
|
+
allowed: { type: 'boolean' },
|
|
128
|
+
message: { type: 'string', minLength: 1 },
|
|
129
|
+
peerIsolation: { type: 'boolean' },
|
|
130
|
+
sliceFolders: { ...stringArraySchema, minItems: 1 },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
safety: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
properties: {
|
|
137
|
+
maxTsSuppressions: { type: 'integer', minimum: 0, default: 0 },
|
|
138
|
+
maxAnyCasts: { type: 'integer', minimum: 0, default: 0 },
|
|
139
|
+
allowInMemory: { type: 'boolean', default: false },
|
|
140
|
+
allowDisabledPeerIsolation: { type: 'boolean', default: false },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
export class ArkConfigValidationError extends Error {
|
|
146
|
+
issues;
|
|
147
|
+
source;
|
|
148
|
+
constructor(source, issues) {
|
|
149
|
+
super(`Invalid ArkGate config (${source}):\n${issues
|
|
150
|
+
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
151
|
+
.join('\n')}`);
|
|
152
|
+
this.name = 'ArkConfigValidationError';
|
|
153
|
+
this.source = source;
|
|
154
|
+
this.issues = issues;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function isObject(value) {
|
|
158
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
159
|
+
}
|
|
160
|
+
function propertyPath(parent, key) {
|
|
161
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
162
|
+
? `${parent}.${key}`
|
|
163
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
164
|
+
}
|
|
165
|
+
function valueType(value) {
|
|
166
|
+
if (value === null)
|
|
167
|
+
return 'null';
|
|
168
|
+
if (Array.isArray(value))
|
|
169
|
+
return 'array';
|
|
170
|
+
return typeof value;
|
|
171
|
+
}
|
|
172
|
+
function resolveSchemaRef(ref, root) {
|
|
173
|
+
const prefix = '#/$defs/';
|
|
174
|
+
if (!ref.startsWith(prefix))
|
|
175
|
+
return undefined;
|
|
176
|
+
return root.$defs[ref.slice(prefix.length)];
|
|
177
|
+
}
|
|
178
|
+
function validateNode(value, schema, path, root, issues) {
|
|
179
|
+
if (schema.$ref) {
|
|
180
|
+
const referenced = resolveSchemaRef(schema.$ref, root);
|
|
181
|
+
if (!referenced) {
|
|
182
|
+
issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
validateNode(value, referenced, path, root, issues);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (schema.const !== undefined && !Object.is(value, schema.const)) {
|
|
189
|
+
issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
193
|
+
issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (schema.type === 'object') {
|
|
197
|
+
if (!isObject(value)) {
|
|
198
|
+
issues.push({ path, message: `must be an object; received ${valueType(value)}` });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const properties = schema.properties ?? {};
|
|
202
|
+
for (const key of schema.required ?? []) {
|
|
203
|
+
if (value[key] === undefined) {
|
|
204
|
+
issues.push({ path: propertyPath(path, key), message: 'is required' });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (schema.additionalProperties === false) {
|
|
208
|
+
for (const key of Object.keys(value)) {
|
|
209
|
+
if (!(key in properties)) {
|
|
210
|
+
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
215
|
+
if (value[key] !== undefined) {
|
|
216
|
+
validateNode(value[key], childSchema, propertyPath(path, key), root, issues);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (schema.type === 'array') {
|
|
222
|
+
if (!Array.isArray(value)) {
|
|
223
|
+
issues.push({ path, message: `must be an array; received ${valueType(value)}` });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
227
|
+
issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
|
|
228
|
+
}
|
|
229
|
+
if (schema.uniqueItems) {
|
|
230
|
+
const serialized = value.map((entry) => JSON.stringify(entry));
|
|
231
|
+
if (new Set(serialized).size !== serialized.length) {
|
|
232
|
+
issues.push({ path, message: 'must not contain duplicate items' });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (schema.items) {
|
|
236
|
+
value.forEach((entry, index) => validateNode(entry, schema.items, `${path}[${index}]`, root, issues));
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (schema.type === 'string') {
|
|
241
|
+
if (typeof value !== 'string') {
|
|
242
|
+
issues.push({ path, message: `must be a string; received ${valueType(value)}` });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
246
|
+
issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (schema.type === 'boolean') {
|
|
251
|
+
if (typeof value !== 'boolean') {
|
|
252
|
+
issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (schema.type === 'integer') {
|
|
257
|
+
if (!Number.isInteger(value)) {
|
|
258
|
+
issues.push({ path, message: `must be an integer; received ${valueType(value)}` });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
262
|
+
issues.push({ path, message: `must be at least ${schema.minimum}` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function defaultedConfig(input) {
|
|
267
|
+
return {
|
|
268
|
+
...input,
|
|
269
|
+
$schema: input.$schema === undefined ? ARK_CONFIG_SCHEMA_URL : input.$schema,
|
|
270
|
+
schemaVersion: input.schemaVersion === undefined ? ARK_CONFIG_SCHEMA_VERSION : input.schemaVersion,
|
|
271
|
+
include: input.include === undefined ? ['src'] : input.include,
|
|
272
|
+
layers: input.layers === undefined ? [] : input.layers,
|
|
273
|
+
rules: input.rules === undefined
|
|
274
|
+
? DEFAULT_ARK_CONFIG_RULES.map((rule) => ({ ...rule }))
|
|
275
|
+
: input.rules,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export function migrateArkConfig(input, source = 'ark.config.json') {
|
|
279
|
+
if (!isObject(input)) {
|
|
280
|
+
throw new ArkConfigValidationError(source, [
|
|
281
|
+
{ path: '$', message: `must be an object; received ${valueType(input)}` },
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
const migratedFrom = input.schemaVersion === undefined ? 'unversioned' : null;
|
|
285
|
+
if (input.schemaVersion !== undefined &&
|
|
286
|
+
input.schemaVersion !== ARK_CONFIG_SCHEMA_VERSION) {
|
|
287
|
+
throw new ArkConfigValidationError(source, [
|
|
288
|
+
{
|
|
289
|
+
path: '$.schemaVersion',
|
|
290
|
+
message: `unsupported version ${JSON.stringify(input.schemaVersion)}; expected ${ARK_CONFIG_SCHEMA_VERSION}`,
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
return { candidate: defaultedConfig(input), migratedFrom };
|
|
295
|
+
}
|
|
296
|
+
export function loadArkConfigContract(input, source = 'ark.config.json') {
|
|
297
|
+
const { candidate, migratedFrom } = migrateArkConfig(input, source);
|
|
298
|
+
const issues = [];
|
|
299
|
+
validateNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
|
|
300
|
+
if (issues.length > 0)
|
|
301
|
+
throw new ArkConfigValidationError(source, issues);
|
|
302
|
+
return { config: candidate, migratedFrom };
|
|
303
|
+
}
|
|
304
|
+
export function parseArkConfigJson(json, source = 'ark.config.json') {
|
|
305
|
+
let input;
|
|
306
|
+
try {
|
|
307
|
+
input = JSON.parse(json);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw new ArkConfigValidationError(source, [
|
|
311
|
+
{
|
|
312
|
+
path: '$',
|
|
313
|
+
message: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
return loadArkConfigContract(input, source);
|
|
318
|
+
}
|
|
319
|
+
export function withArkConfigMetadata(config) {
|
|
320
|
+
const result = {
|
|
321
|
+
$schema: typeof config.$schema === 'string' && config.$schema.length > 0
|
|
322
|
+
? config.$schema
|
|
323
|
+
: ARK_CONFIG_SCHEMA_URL,
|
|
324
|
+
schemaVersion: ARK_CONFIG_SCHEMA_VERSION,
|
|
325
|
+
};
|
|
326
|
+
for (const [key, value] of Object.entries(config)) {
|
|
327
|
+
if (key !== '$schema' && key !== 'schemaVersion')
|
|
328
|
+
result[key] = value;
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production deploy-path quality signals (install modularization).
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { readPackageJson, packageScriptsHaveTypecheck } from './gate-files.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Production deploy path quality (universal — any consumer repo).
|
|
10
|
+
* Detects when the production build host runs ESLint / typecheck as part of
|
|
11
|
+
* `build` (e.g. Next.js "Linting and checking validity of types") so failures
|
|
12
|
+
* surface first on Vercel/Netlify/etc. unless CI/pre-merge runs the same checks.
|
|
13
|
+
* Framework signals only (deps + scripts + config) — never project-specific.
|
|
14
|
+
*
|
|
15
|
+
* @returns {{
|
|
16
|
+
* embedsLintInBuild: boolean,
|
|
17
|
+
* embedsTypecheckInBuild: boolean,
|
|
18
|
+
* engines: string[],
|
|
19
|
+
* hasLintScript: boolean,
|
|
20
|
+
* hasTypecheckScript: boolean,
|
|
21
|
+
* ciRunsLint: boolean,
|
|
22
|
+
* ciRunsTypecheck: boolean,
|
|
23
|
+
* eslintIgnoreDuringBuilds: boolean,
|
|
24
|
+
* }}
|
|
25
|
+
*/
|
|
26
|
+
export function detectDeployPathQuality(root) {
|
|
27
|
+
const pkg = readPackageJson(root) || {};
|
|
28
|
+
const deps = {
|
|
29
|
+
...(pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {}),
|
|
30
|
+
...(pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {}),
|
|
31
|
+
...(pkg.peerDependencies && typeof pkg.peerDependencies === 'object' ? pkg.peerDependencies : {}),
|
|
32
|
+
};
|
|
33
|
+
const scripts =
|
|
34
|
+
pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
35
|
+
const buildScript = typeof scripts.build === 'string' ? scripts.build : '';
|
|
36
|
+
|
|
37
|
+
const engines = [];
|
|
38
|
+
// Next.js production build runs ESLint + typecheck by default (unless opted out).
|
|
39
|
+
if (deps.next || /\bnext\s+build\b/.test(buildScript)) engines.push('next');
|
|
40
|
+
// Nuxt 3+ can lint via modules; only flag when build clearly invokes nuxt build + eslint tooling present.
|
|
41
|
+
if ((deps.nuxt || deps['nuxt3'] || /\bnuxt\s+build\b/.test(buildScript)) && (deps.eslint || hasEslintConfig(root))) {
|
|
42
|
+
engines.push('nuxt');
|
|
43
|
+
}
|
|
44
|
+
// Create React App historically failed build on ESLint errors.
|
|
45
|
+
if (deps['react-scripts'] || /\breact-scripts\s+build\b/.test(buildScript)) engines.push('cra');
|
|
46
|
+
|
|
47
|
+
const eslintIgnoreDuringBuilds = engines.includes('next') && nextIgnoresEslintDuringBuilds(root);
|
|
48
|
+
const embedsLintInBuild = engines.length > 0 && !eslintIgnoreDuringBuilds;
|
|
49
|
+
// Next still typechecks during build even when eslint.ignoreDuringBuilds is true.
|
|
50
|
+
const embedsTypecheckInBuild = engines.includes('next') || engines.includes('nuxt');
|
|
51
|
+
|
|
52
|
+
const scriptHasLint = (s) =>
|
|
53
|
+
Boolean(
|
|
54
|
+
s &&
|
|
55
|
+
((typeof s.lint === 'string' && s.lint.trim()) ||
|
|
56
|
+
(typeof s.eslint === 'string' && s.eslint.trim()) ||
|
|
57
|
+
(typeof s['lint:ci'] === 'string' && s['lint:ci'].trim()) ||
|
|
58
|
+
(typeof s['check:lint'] === 'string' && s['check:lint'].trim()))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let hasLintScript = scriptHasLint(scripts);
|
|
62
|
+
let hasTypecheckScript = packageScriptsHaveTypecheck(scripts);
|
|
63
|
+
const packageLintScripts = [];
|
|
64
|
+
// Monorepo: package-level scripts count (apps/web, packages/ui, …).
|
|
65
|
+
try {
|
|
66
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
67
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
68
|
+
const candidates = [path.join(root, entry.name)];
|
|
69
|
+
// one more level: packages/foo
|
|
70
|
+
try {
|
|
71
|
+
for (const child of fs.readdirSync(path.join(root, entry.name), { withFileTypes: true })) {
|
|
72
|
+
if (child.isDirectory() && !child.name.startsWith('.')) {
|
|
73
|
+
candidates.push(path.join(root, entry.name, child.name));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
for (const dir of candidates) {
|
|
80
|
+
const pj = path.join(dir, 'package.json');
|
|
81
|
+
if (!fs.existsSync(pj)) continue;
|
|
82
|
+
try {
|
|
83
|
+
const nested = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
84
|
+
const ns = nested.scripts && typeof nested.scripts === 'object' ? nested.scripts : {};
|
|
85
|
+
if (scriptHasLint(ns)) {
|
|
86
|
+
hasLintScript = true;
|
|
87
|
+
packageLintScripts.push(path.relative(root, dir).split(path.sep).join('/'));
|
|
88
|
+
}
|
|
89
|
+
if (packageScriptsHaveTypecheck(ns)) hasTypecheckScript = true;
|
|
90
|
+
const nd = {
|
|
91
|
+
...(nested.dependencies || {}),
|
|
92
|
+
...(nested.devDependencies || {}),
|
|
93
|
+
};
|
|
94
|
+
if (nd.next && !engines.includes('next')) engines.push('next');
|
|
95
|
+
} catch {
|
|
96
|
+
/* ignore */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
/* ignore */
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ciTexts = collectCiWorkflowTexts(root);
|
|
105
|
+
const ciJoined = ciTexts.join('\n');
|
|
106
|
+
const ciRunsLint =
|
|
107
|
+
ciTexts.length > 0 &&
|
|
108
|
+
(/\bnpm\s+run\s+lint\b/i.test(ciJoined) ||
|
|
109
|
+
/\bpnpm\s+(?:run\s+)?lint\b/i.test(ciJoined) ||
|
|
110
|
+
/\byarn\s+(?:run\s+)?lint\b/i.test(ciJoined) ||
|
|
111
|
+
/\bbun\s+run\s+lint\b/i.test(ciJoined) ||
|
|
112
|
+
/\beslint\b/i.test(ciJoined) ||
|
|
113
|
+
/\blint:ci\b/i.test(ciJoined) ||
|
|
114
|
+
/\bcheck:lint\b/i.test(ciJoined) ||
|
|
115
|
+
// package-level: working-directory + lint, or path/filter lint
|
|
116
|
+
(packageLintScripts.length > 0 &&
|
|
117
|
+
packageLintScripts.some((p) => ciJoined.includes(p) && /lint/i.test(ciJoined))));
|
|
118
|
+
const ciRunsTypecheck =
|
|
119
|
+
ciTexts.length > 0 &&
|
|
120
|
+
(/\btypecheck\b/i.test(ciJoined) ||
|
|
121
|
+
/\btype-check\b/i.test(ciJoined) ||
|
|
122
|
+
/\bcheck:types\b/i.test(ciJoined) ||
|
|
123
|
+
/\btsc\s+--noEmit\b/i.test(ciJoined));
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
embedsLintInBuild,
|
|
127
|
+
embedsTypecheckInBuild,
|
|
128
|
+
engines,
|
|
129
|
+
hasLintScript,
|
|
130
|
+
hasTypecheckScript,
|
|
131
|
+
ciRunsLint,
|
|
132
|
+
ciRunsTypecheck,
|
|
133
|
+
eslintIgnoreDuringBuilds,
|
|
134
|
+
hasCiWorkflows: ciTexts.length > 0,
|
|
135
|
+
packageLintScripts,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasEslintConfig(root) {
|
|
140
|
+
return [
|
|
141
|
+
'eslint.config.mjs',
|
|
142
|
+
'eslint.config.js',
|
|
143
|
+
'eslint.config.cjs',
|
|
144
|
+
'eslint.config.ts',
|
|
145
|
+
'.eslintrc.json',
|
|
146
|
+
'.eslintrc.cjs',
|
|
147
|
+
'.eslintrc.js',
|
|
148
|
+
'.eslintrc.yml',
|
|
149
|
+
'.eslintrc.yaml',
|
|
150
|
+
].some((f) => fs.existsSync(path.join(root, f)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** next.config.* eslint.ignoreDuringBuilds: true → production build will not fail on ESLint. */
|
|
154
|
+
function nextIgnoresEslintDuringBuilds(root) {
|
|
155
|
+
const names = [
|
|
156
|
+
'next.config.ts',
|
|
157
|
+
'next.config.mts',
|
|
158
|
+
'next.config.js',
|
|
159
|
+
'next.config.mjs',
|
|
160
|
+
'next.config.cjs',
|
|
161
|
+
];
|
|
162
|
+
for (const name of names) {
|
|
163
|
+
const file = path.join(root, name);
|
|
164
|
+
if (!fs.existsSync(file)) continue;
|
|
165
|
+
try {
|
|
166
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
167
|
+
// Common patterns: ignoreDuringBuilds: true | ignoreDuringBuilds: true,
|
|
168
|
+
if (/ignoreDuringBuilds\s*:\s*true/.test(text)) return true;
|
|
169
|
+
} catch {
|
|
170
|
+
/* ignore */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectCiWorkflowTexts(root) {
|
|
177
|
+
const texts = [];
|
|
178
|
+
const pushFile = (rel) => {
|
|
179
|
+
try {
|
|
180
|
+
const full = path.join(root, rel);
|
|
181
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
182
|
+
texts.push(fs.readFileSync(full, 'utf8'));
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
/* ignore */
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
pushFile('.gitlab-ci.yml');
|
|
189
|
+
pushFile('bitbucket-pipelines.yml');
|
|
190
|
+
pushFile('azure-pipelines.yml');
|
|
191
|
+
pushFile('.circleci/config.yml');
|
|
192
|
+
const wfDir = path.join(root, '.github', 'workflows');
|
|
193
|
+
try {
|
|
194
|
+
if (fs.existsSync(wfDir)) {
|
|
195
|
+
for (const f of fs.readdirSync(wfDir)) {
|
|
196
|
+
if (!/\.ya?ml$/i.test(f)) continue;
|
|
197
|
+
pushFile(path.join('.github', 'workflows', f));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
/* ignore */
|
|
202
|
+
}
|
|
203
|
+
return texts;
|
|
204
|
+
}
|
|
205
|
+
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -327,8 +327,14 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
327
327
|
gatesMissing,
|
|
328
328
|
skillGaps,
|
|
329
329
|
staleRunnerFiles: staleRunners,
|
|
330
|
-
//
|
|
330
|
+
// Active-host guarantees plus separate repo-wide inventory.
|
|
331
331
|
writePath: {
|
|
332
|
+
activeHost: writePath.activeHost,
|
|
333
|
+
support: writePath.support,
|
|
334
|
+
supportSummary: writePath.supportSummary,
|
|
335
|
+
capabilities: writePath.capabilities,
|
|
336
|
+
capabilityEvidence: writePath.capabilityEvidence,
|
|
337
|
+
inventory: writePath.inventory,
|
|
332
338
|
mode: writePath.mode,
|
|
333
339
|
prepareWrite: writePath.prepareWrite,
|
|
334
340
|
autoPatch: writePath.autoPatch,
|
|
@@ -404,7 +410,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
404
410
|
adapt:
|
|
405
411
|
'Align — contract and folders still disagree, or coverage is weak / debt is open. You do not pick this mode. Next: classify ungoverned dirs (/ark-contract, /ark-adopt), run the plan (/ark-autopilot or /ark-loop). Gates do not fully protect you yet.',
|
|
406
412
|
enforce:
|
|
407
|
-
'Guard — contract
|
|
413
|
+
'Guard — contract coverage is honest and checked edges are clean. You do not pick this mode; you arrived here. Next: keep the host-appropriate write path and CI check on; only NEW violations should fail.',
|
|
408
414
|
};
|
|
409
415
|
line(modeMark, `${mode.toUpperCase()} — ${modeHelp[mode]}`);
|
|
410
416
|
if (emptyScope) {
|
|
@@ -496,26 +502,37 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
496
502
|
|
|
497
503
|
console.log('');
|
|
498
504
|
console.log(color.bold('Write path (agent)'));
|
|
505
|
+
const capabilities = writePath.capabilities;
|
|
499
506
|
const writePathLabels = {
|
|
500
507
|
repair: 'repair-capable — hard block + machine-readable autoPatch / ARK_REPAIR_JSON',
|
|
501
508
|
'reject-only': 'reject-only — hard block with prose; no repair payload',
|
|
502
509
|
'mcp-only': 'MCP tools only — prepare-write/autoPatch available; no PreToolUse hook',
|
|
503
|
-
none: '
|
|
510
|
+
none: 'no write gate hook and no Ark MCP',
|
|
504
511
|
};
|
|
505
512
|
const wpMark =
|
|
506
|
-
|
|
513
|
+
capabilities['hard-write']
|
|
507
514
|
? ok
|
|
508
|
-
:
|
|
509
|
-
?
|
|
510
|
-
:
|
|
515
|
+
: capabilities['advisory-write'] || capabilities['merge-gate']
|
|
516
|
+
? warn
|
|
517
|
+
: bad;
|
|
518
|
+
line(' ', `Active host: ${writePath.activeHost}`);
|
|
519
|
+
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
511
520
|
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
512
521
|
line(
|
|
513
|
-
|
|
514
|
-
`
|
|
522
|
+
capabilities['hard-write'] ? ok : warn,
|
|
523
|
+
`Hard write boundary: ${capabilities['hard-write'] ? 'yes' : 'no'}`
|
|
524
|
+
);
|
|
525
|
+
line(
|
|
526
|
+
warn,
|
|
527
|
+
`Advisory write tools (MCP): ${capabilities['advisory-write'] ? 'yes' : 'no'}`
|
|
515
528
|
);
|
|
516
529
|
line(
|
|
517
|
-
|
|
518
|
-
`
|
|
530
|
+
capabilities['merge-gate'] ? ok : bad,
|
|
531
|
+
`CI check (--strict-merge): ${capabilities['merge-gate'] ? 'yes' : 'no'} (merge blocking requires a required status)`
|
|
532
|
+
);
|
|
533
|
+
line(
|
|
534
|
+
capabilities['repair-payload'] ? ok : warn,
|
|
535
|
+
`Repair payload at hard boundary: ${capabilities['repair-payload'] ? 'yes' : 'no'}`
|
|
519
536
|
);
|
|
520
537
|
if (writePath.gap) {
|
|
521
538
|
line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
|
|
@@ -527,7 +544,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
527
544
|
|
|
528
545
|
console.log('');
|
|
529
546
|
console.log(color.bold('Gates & skills'));
|
|
530
|
-
if (gatesMissing.length === 0) line(ok, '
|
|
547
|
+
if (gatesMissing.length === 0) line(ok, 'Shared gate files present (AGENTS.md, .mcp.json, CI)');
|
|
531
548
|
else {
|
|
532
549
|
line(bad, `Missing gates: ${gatesMissing.join(', ')}`);
|
|
533
550
|
actions.push(`install gates (${arkCommand(root, 'ark-check', '--install-agent-gates')})`);
|
|
@@ -570,10 +587,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
570
587
|
);
|
|
571
588
|
} else {
|
|
572
589
|
for (const gap of adoption.gaps) {
|
|
573
|
-
|
|
590
|
+
// Deferred Codex-home debt (non-temp) is annotated, not a top action, when the
|
|
591
|
+
// session host is not Codex — fix when that host is used.
|
|
592
|
+
const mark = gap.deferred
|
|
593
|
+
? color.dim('·')
|
|
594
|
+
: gap.severity === 'warn'
|
|
595
|
+
? warn
|
|
596
|
+
: gap.severity === 'info'
|
|
597
|
+
? warn
|
|
598
|
+
: bad;
|
|
574
599
|
line(mark, gap.message);
|
|
575
|
-
if (gap.fix)
|
|
576
|
-
|
|
600
|
+
if (gap.fix) {
|
|
601
|
+
line(' ', color.dim(gap.deferred ? `When using Codex: ${gap.fix}` : `Fix: ${gap.fix}`));
|
|
602
|
+
}
|
|
603
|
+
if (!gap.deferred) actions.push(gap.fix || gap.message);
|
|
577
604
|
}
|
|
578
605
|
if (adoption.layerBalance) {
|
|
579
606
|
line(warn, color.dim(adoption.layerBalance.educational));
|
|
@@ -601,7 +628,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
601
628
|
line(warn, 'Safety diagnostics unavailable');
|
|
602
629
|
} else {
|
|
603
630
|
const rows = [
|
|
604
|
-
['Non-literal dynamic
|
|
631
|
+
['Non-literal dynamic dependencies', safety.nonLiteralDynamicImports],
|
|
605
632
|
['@ts-ignore / @ts-nocheck', safety.tsSuppressions],
|
|
606
633
|
['Explicit any casts', safety.anyCasts],
|
|
607
634
|
['InMemory stores in production source', safety.inMemoryProductionStores],
|