arkgate 2.12.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +55 -31
  3. package/bin/ark-check.mjs +95 -36
  4. package/bin/ark-mcp.mjs +11 -5
  5. package/bin/ark-shared.mjs +88 -56
  6. package/bin/ark.mjs +45 -10
  7. package/bin/lib/agent-gates.mjs +12 -0
  8. package/bin/lib/architecture-scan.mjs +8 -0
  9. package/bin/lib/ci-and-commands.mjs +9 -3
  10. package/bin/lib/codex-home.mjs +7 -0
  11. package/bin/lib/config-contract.mjs +331 -0
  12. package/bin/lib/doctor-plan.mjs +43 -16
  13. package/bin/lib/enforcement-profiles.mjs +97 -0
  14. package/bin/lib/host-support-matrix.mjs +77 -0
  15. package/bin/lib/install-migrate.mjs +45 -14
  16. package/bin/lib/mcp-adoption.mjs +35 -3
  17. package/bin/lib/open-html.mjs +75 -0
  18. package/bin/lib/presets.mjs +3 -2
  19. package/bin/lib/safety-diagnostics.mjs +31 -11
  20. package/bin/lib/skill-install.mjs +64 -0
  21. package/bin/lib/ts-resolve.mjs +2 -1
  22. package/bin/lib/weakest-link.mjs +417 -0
  23. package/bin/lib/write-path-capabilities.mjs +182 -0
  24. package/bin/lib/write-path-detect.mjs +62 -99
  25. package/dist/configContract-iBLxx5Tz.d.cts +53 -0
  26. package/dist/configContract-iBLxx5Tz.d.ts +53 -0
  27. package/dist/eslint/index.cjs +375 -13
  28. package/dist/eslint/index.cjs.map +1 -1
  29. package/dist/eslint/index.d.cts +30 -20
  30. package/dist/eslint/index.d.ts +30 -20
  31. package/dist/eslint/index.js +375 -13
  32. package/dist/eslint/index.js.map +1 -1
  33. package/dist/index.cjs +723 -61
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +95 -5
  36. package/dist/index.d.ts +95 -5
  37. package/dist/index.js +716 -61
  38. package/dist/index.js.map +1 -1
  39. package/dist/nestjs/index.cjs +150 -42
  40. package/dist/nestjs/index.cjs.map +1 -1
  41. package/dist/nestjs/index.d.cts +2 -1
  42. package/dist/nestjs/index.d.ts +2 -1
  43. package/dist/nestjs/index.js +150 -42
  44. package/dist/nestjs/index.js.map +1 -1
  45. package/dist/runtime/index.cjs +723 -61
  46. package/dist/runtime/index.cjs.map +1 -1
  47. package/dist/runtime/index.d.cts +3 -2
  48. package/dist/runtime/index.d.ts +3 -2
  49. package/dist/runtime/index.js +716 -61
  50. package/dist/runtime/index.js.map +1 -1
  51. package/dist/{types-BZ17b9i5.d.cts → types-BxBwnBpC.d.cts} +9 -36
  52. package/dist/{types-BZ17b9i5.d.ts → types-Wcs_l1_J.d.ts} +9 -36
  53. package/docs/agent-guide.md +32 -20
  54. package/docs/ai-gates.md +53 -18
  55. package/docs/configuration.md +97 -0
  56. package/docs/enthusiast/README.md +3 -3
  57. package/docs/enthusiast/how-to-agent-gates.md +7 -3
  58. package/docs/migrate-from-ark-runtime-kernel.md +3 -0
  59. package/docs/package-surface.md +14 -9
  60. package/docs/production-hardening.md +15 -2
  61. package/docs/threat-model.md +65 -0
  62. package/docs/typescript-support.md +3 -3
  63. package/package.json +15 -2
  64. package/schemas/ark.config.schema.json +750 -0
  65. package/server.json +2 -2
  66. package/templates/hooks/pre-commit-ark +37 -0
  67. package/templates/skills/ark-coverage.md +2 -2
  68. package/templates/skills/ark-runtime.md +8 -5
  69. package/templates/skills/ark-upgrade.md +36 -16
  70. package/tests/fixtures/ts-consumer/ark.config.json +2 -0
@@ -170,6 +170,311 @@ function isEdgeDenied(rules2, from, to, options) {
170
170
  return findDeniedEdgeRule(rules2, from, to, options) !== void 0;
171
171
  }
172
172
 
173
+ // src/domain/configContract.ts
174
+ var ARK_CONFIG_SCHEMA_VERSION = "1.0";
175
+ var ARK_CONFIG_SCHEMA_URL = "https://unpkg.com/arkgate@2/schemas/ark.config.schema.json";
176
+ var DEFAULT_LAYER_NAMES = [
177
+ "DomainModel",
178
+ "ApplicationOrchestration",
179
+ "PersistenceAdapters",
180
+ "IntegrationAdapters",
181
+ "WorkflowSagaEngine",
182
+ "BackgroundJobsScheduling",
183
+ "PresentationAdapters",
184
+ "ReportingReadModels",
185
+ "ExtensibilityMetadata",
186
+ "SecurityAuditObservability",
187
+ "Kernel"
188
+ ];
189
+ var DEFAULT_ALLOWED_FLOWS = /* @__PURE__ */ new Set([
190
+ "PresentationAdapters->ApplicationOrchestration",
191
+ "ApplicationOrchestration->DomainModel",
192
+ "WorkflowSagaEngine->ApplicationOrchestration",
193
+ "WorkflowSagaEngine->DomainModel",
194
+ "BackgroundJobsScheduling->ApplicationOrchestration"
195
+ ]);
196
+ function createDefaultRules() {
197
+ const rules2 = [];
198
+ for (const from of DEFAULT_LAYER_NAMES) {
199
+ for (const to of DEFAULT_LAYER_NAMES) {
200
+ if (from === to || DEFAULT_ALLOWED_FLOWS.has(`${from}->${to}`)) continue;
201
+ rules2.push({ from, to, allowed: false });
202
+ }
203
+ }
204
+ return rules2;
205
+ }
206
+ var DEFAULT_ARK_CONFIG_RULES = createDefaultRules();
207
+ var stringArraySchema = {
208
+ type: "array",
209
+ items: { type: "string", minLength: 1 },
210
+ uniqueItems: true
211
+ };
212
+ var ARK_CONFIG_SCHEMA = {
213
+ $schema: "https://json-schema.org/draft/2020-12/schema",
214
+ $id: ARK_CONFIG_SCHEMA_URL,
215
+ title: "ArkGate architecture contract",
216
+ description: "Versioned contract consumed identically by ArkGate CLI, MCP, and ESLint surfaces.",
217
+ type: "object",
218
+ additionalProperties: false,
219
+ required: ["$schema", "schemaVersion", "include", "layers", "rules"],
220
+ properties: {
221
+ $schema: {
222
+ type: "string",
223
+ minLength: 1,
224
+ default: ARK_CONFIG_SCHEMA_URL,
225
+ description: "Editor-facing URL or local path for this JSON Schema."
226
+ },
227
+ schemaVersion: {
228
+ type: "string",
229
+ const: ARK_CONFIG_SCHEMA_VERSION,
230
+ default: ARK_CONFIG_SCHEMA_VERSION
231
+ },
232
+ name: { type: "string", minLength: 1 },
233
+ include: { ...stringArraySchema, minItems: 1, default: ["src"] },
234
+ exclude: { ...stringArraySchema, default: [] },
235
+ excludeGenerated: { type: "boolean", default: true },
236
+ frameworkOverlay: { type: "string", minLength: 1 },
237
+ layers: {
238
+ type: "array",
239
+ default: [],
240
+ items: { $ref: "#/$defs/layer" }
241
+ },
242
+ rules: {
243
+ type: "array",
244
+ default: DEFAULT_ARK_CONFIG_RULES,
245
+ items: { $ref: "#/$defs/rule" }
246
+ },
247
+ cyclePolicy: {
248
+ type: "string",
249
+ enum: ["strict", "soft", "framework-soft", "off"],
250
+ default: "strict"
251
+ },
252
+ dynamicImportAllowlist: { ...stringArraySchema, default: [] },
253
+ safety: {
254
+ $ref: "#/$defs/safety",
255
+ default: {
256
+ maxTsSuppressions: 0,
257
+ maxAnyCasts: 0,
258
+ allowInMemory: false,
259
+ allowDisabledPeerIsolation: false
260
+ }
261
+ }
262
+ },
263
+ $defs: {
264
+ layer: {
265
+ type: "object",
266
+ additionalProperties: false,
267
+ required: ["name", "patterns"],
268
+ properties: {
269
+ name: { type: "string", minLength: 1 },
270
+ patterns: { ...stringArraySchema, minItems: 1 },
271
+ exclude: stringArraySchema,
272
+ intentPrefixes: stringArraySchema,
273
+ description: { type: "string", minLength: 1 },
274
+ forbiddenGlobals: stringArraySchema,
275
+ mayImportInfrastructure: { type: "boolean" },
276
+ optional: { type: "boolean" }
277
+ }
278
+ },
279
+ rule: {
280
+ type: "object",
281
+ additionalProperties: false,
282
+ required: ["from", "to", "allowed"],
283
+ properties: {
284
+ from: { type: "string", minLength: 1 },
285
+ to: { type: "string", minLength: 1 },
286
+ allowed: { type: "boolean" },
287
+ message: { type: "string", minLength: 1 },
288
+ peerIsolation: { type: "boolean" },
289
+ sliceFolders: { ...stringArraySchema, minItems: 1 }
290
+ }
291
+ },
292
+ safety: {
293
+ type: "object",
294
+ additionalProperties: false,
295
+ properties: {
296
+ maxTsSuppressions: { type: "integer", minimum: 0, default: 0 },
297
+ maxAnyCasts: { type: "integer", minimum: 0, default: 0 },
298
+ allowInMemory: { type: "boolean", default: false },
299
+ allowDisabledPeerIsolation: { type: "boolean", default: false }
300
+ }
301
+ }
302
+ }
303
+ };
304
+ var ArkConfigValidationError = class extends Error {
305
+ issues;
306
+ source;
307
+ constructor(source, issues) {
308
+ super(
309
+ `Invalid ArkGate config (${source}):
310
+ ${issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")}`
311
+ );
312
+ this.name = "ArkConfigValidationError";
313
+ this.source = source;
314
+ this.issues = issues;
315
+ }
316
+ };
317
+ function isObject(value) {
318
+ return value !== null && typeof value === "object" && !Array.isArray(value);
319
+ }
320
+ function propertyPath(parent, key) {
321
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${parent}.${key}` : `${parent}[${JSON.stringify(key)}]`;
322
+ }
323
+ function valueType(value) {
324
+ if (value === null) return "null";
325
+ if (Array.isArray(value)) return "array";
326
+ return typeof value;
327
+ }
328
+ function resolveSchemaRef(ref, root) {
329
+ const prefix = "#/$defs/";
330
+ if (!ref.startsWith(prefix)) return void 0;
331
+ return root.$defs[ref.slice(prefix.length)];
332
+ }
333
+ function validateNode(value, schema, path2, root, issues) {
334
+ if (schema.$ref) {
335
+ const referenced = resolveSchemaRef(schema.$ref, root);
336
+ if (!referenced) {
337
+ issues.push({ path: path2, message: `schema reference ${schema.$ref} cannot be resolved` });
338
+ return;
339
+ }
340
+ validateNode(value, referenced, path2, root, issues);
341
+ return;
342
+ }
343
+ if (schema.const !== void 0 && !Object.is(value, schema.const)) {
344
+ issues.push({ path: path2, message: `must equal ${JSON.stringify(schema.const)}` });
345
+ return;
346
+ }
347
+ if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
348
+ issues.push({ path: path2, message: `must be one of ${schema.enum.map(String).join(", ")}` });
349
+ return;
350
+ }
351
+ if (schema.type === "object") {
352
+ if (!isObject(value)) {
353
+ issues.push({ path: path2, message: `must be an object; received ${valueType(value)}` });
354
+ return;
355
+ }
356
+ const properties = schema.properties ?? {};
357
+ for (const key of schema.required ?? []) {
358
+ if (value[key] === void 0) {
359
+ issues.push({ path: propertyPath(path2, key), message: "is required" });
360
+ }
361
+ }
362
+ if (schema.additionalProperties === false) {
363
+ for (const key of Object.keys(value)) {
364
+ if (!(key in properties)) {
365
+ issues.push({ path: propertyPath(path2, key), message: "unknown field" });
366
+ }
367
+ }
368
+ }
369
+ for (const [key, childSchema] of Object.entries(properties)) {
370
+ if (value[key] !== void 0) {
371
+ validateNode(value[key], childSchema, propertyPath(path2, key), root, issues);
372
+ }
373
+ }
374
+ return;
375
+ }
376
+ if (schema.type === "array") {
377
+ if (!Array.isArray(value)) {
378
+ issues.push({ path: path2, message: `must be an array; received ${valueType(value)}` });
379
+ return;
380
+ }
381
+ if (schema.minItems !== void 0 && value.length < schema.minItems) {
382
+ issues.push({ path: path2, message: `must contain at least ${schema.minItems} item(s)` });
383
+ }
384
+ if (schema.uniqueItems) {
385
+ const serialized = value.map((entry) => JSON.stringify(entry));
386
+ if (new Set(serialized).size !== serialized.length) {
387
+ issues.push({ path: path2, message: "must not contain duplicate items" });
388
+ }
389
+ }
390
+ if (schema.items) {
391
+ value.forEach(
392
+ (entry, index) => validateNode(entry, schema.items, `${path2}[${index}]`, root, issues)
393
+ );
394
+ }
395
+ return;
396
+ }
397
+ if (schema.type === "string") {
398
+ if (typeof value !== "string") {
399
+ issues.push({ path: path2, message: `must be a string; received ${valueType(value)}` });
400
+ return;
401
+ }
402
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
403
+ issues.push({ path: path2, message: `must contain at least ${schema.minLength} character(s)` });
404
+ }
405
+ return;
406
+ }
407
+ if (schema.type === "boolean") {
408
+ if (typeof value !== "boolean") {
409
+ issues.push({ path: path2, message: `must be a boolean; received ${valueType(value)}` });
410
+ }
411
+ return;
412
+ }
413
+ if (schema.type === "integer") {
414
+ if (!Number.isInteger(value)) {
415
+ issues.push({ path: path2, message: `must be an integer; received ${valueType(value)}` });
416
+ return;
417
+ }
418
+ if (schema.minimum !== void 0 && value < schema.minimum) {
419
+ issues.push({ path: path2, message: `must be at least ${schema.minimum}` });
420
+ }
421
+ }
422
+ }
423
+ function defaultedConfig(input) {
424
+ return {
425
+ ...input,
426
+ $schema: input.$schema === void 0 ? ARK_CONFIG_SCHEMA_URL : input.$schema,
427
+ schemaVersion: input.schemaVersion === void 0 ? ARK_CONFIG_SCHEMA_VERSION : input.schemaVersion,
428
+ include: input.include === void 0 ? ["src"] : input.include,
429
+ layers: input.layers === void 0 ? [] : input.layers,
430
+ rules: input.rules === void 0 ? DEFAULT_ARK_CONFIG_RULES.map((rule) => ({ ...rule })) : input.rules
431
+ };
432
+ }
433
+ function migrateArkConfig(input, source = "ark.config.json") {
434
+ if (!isObject(input)) {
435
+ throw new ArkConfigValidationError(source, [
436
+ { path: "$", message: `must be an object; received ${valueType(input)}` }
437
+ ]);
438
+ }
439
+ const migratedFrom = input.schemaVersion === void 0 ? "unversioned" : null;
440
+ if (input.schemaVersion !== void 0 && input.schemaVersion !== ARK_CONFIG_SCHEMA_VERSION) {
441
+ throw new ArkConfigValidationError(source, [
442
+ {
443
+ path: "$.schemaVersion",
444
+ message: `unsupported version ${JSON.stringify(input.schemaVersion)}; expected ${ARK_CONFIG_SCHEMA_VERSION}`
445
+ }
446
+ ]);
447
+ }
448
+ return { candidate: defaultedConfig(input), migratedFrom };
449
+ }
450
+ function loadArkConfigContract(input, source = "ark.config.json") {
451
+ const { candidate, migratedFrom } = migrateArkConfig(input, source);
452
+ const issues = [];
453
+ validateNode(
454
+ candidate,
455
+ ARK_CONFIG_SCHEMA,
456
+ "$",
457
+ ARK_CONFIG_SCHEMA,
458
+ issues
459
+ );
460
+ if (issues.length > 0) throw new ArkConfigValidationError(source, issues);
461
+ return { config: candidate, migratedFrom };
462
+ }
463
+ function parseArkConfigJson(json, source = "ark.config.json") {
464
+ let input;
465
+ try {
466
+ input = JSON.parse(json);
467
+ } catch (error) {
468
+ throw new ArkConfigValidationError(source, [
469
+ {
470
+ path: "$",
471
+ message: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`
472
+ }
473
+ ]);
474
+ }
475
+ return loadArkConfigContract(input, source);
476
+ }
477
+
173
478
  // src/eslint/index.ts
174
479
  function lintedFilename(context) {
175
480
  if (typeof context.physicalFilename === "string" && context.physicalFilename.length > 0) {
@@ -201,14 +506,10 @@ function findConfigPath(startFile) {
201
506
  var _configCache = /* @__PURE__ */ new Map();
202
507
  function loadArkConfig(configPath) {
203
508
  if (_configCache.has(configPath)) return _configCache.get(configPath) ?? null;
204
- try {
205
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
206
- _configCache.set(configPath, raw);
207
- return raw;
208
- } catch {
209
- _configCache.set(configPath, null);
210
- return null;
211
- }
509
+ if (!fs.existsSync(configPath)) return null;
510
+ const config = parseArkConfigJson(fs.readFileSync(configPath, "utf8"), configPath).config;
511
+ _configCache.set(configPath, config);
512
+ return config;
212
513
  }
213
514
  function resolveRelativeImport(fromFile, specifier) {
214
515
  if (!specifier.startsWith(".")) return null;
@@ -239,6 +540,46 @@ function stringValue(node) {
239
540
  function propertyName(node) {
240
541
  return node?.name ?? stringValue(node);
241
542
  }
543
+ function sourceCodeFor(context) {
544
+ return context.sourceCode ?? context.getSourceCode?.();
545
+ }
546
+ function referenceFor(context, node) {
547
+ let scope = sourceCodeFor(context)?.getScope?.(node);
548
+ while (scope) {
549
+ const reference = scope.references?.find((candidate) => candidate.identifier === node);
550
+ if (reference) return reference;
551
+ scope = scope.upper ?? void 0;
552
+ }
553
+ return void 0;
554
+ }
555
+ function isLocallyBound(context, node, name) {
556
+ const reference = referenceFor(context, node);
557
+ if (reference?.resolved) return (reference.resolved.defs?.length ?? 0) > 0;
558
+ let scope = sourceCodeFor(context)?.getScope?.(node);
559
+ while (scope) {
560
+ const variable = scope.set?.get(name);
561
+ if (variable) return (variable.defs?.length ?? 0) > 0;
562
+ scope = scope.upper ?? void 0;
563
+ }
564
+ return false;
565
+ }
566
+ function isValueIdentifierReference(context, node) {
567
+ const reference = referenceFor(context, node);
568
+ if (reference) return reference.isValueReference !== false;
569
+ return node.parent?.type === "VariableDeclarator" && node.parent.init === node;
570
+ }
571
+ function memberExpressionPath(node) {
572
+ if (node?.type === "Identifier" && node.name) {
573
+ return { root: node, segments: [node.name] };
574
+ }
575
+ if (!node) return void 0;
576
+ const memberLike = node.type === "MemberExpression" || Boolean(node.object && node.property);
577
+ if (!memberLike || node.computed === true) return void 0;
578
+ const base = memberExpressionPath(node.object);
579
+ const property = propertyName(node.property);
580
+ if (!base || !property) return void 0;
581
+ return { root: base.root, segments: [...base.segments, property] };
582
+ }
242
583
  function calleePropertyName(node) {
243
584
  return propertyName(node.callee?.property);
244
585
  }
@@ -431,6 +772,7 @@ var noForbiddenGlobals = {
431
772
  if (!globals) {
432
773
  return {};
433
774
  }
775
+ const scopeAware = typeof sourceCodeFor(context)?.getScope === "function";
434
776
  const report = (node, name) => context.report({
435
777
  node,
436
778
  messageId: config ? "forbiddenGlobal" : "forbiddenGlobalDefault",
@@ -438,19 +780,39 @@ var noForbiddenGlobals = {
438
780
  });
439
781
  return {
440
782
  MemberExpression(node) {
441
- const base = node.object?.type === "Identifier" ? node.object.name : void 0;
442
- if (!base) return;
443
- const dotted = `${base}.${propertyName(node.property) ?? ""}`;
444
- if (globals.has(dotted)) report(node, dotted);
445
- else if (globals.has(base)) report(node, base);
783
+ if (node.parent?.type === "MemberExpression" && node.parent.object === node) return;
784
+ const path2 = memberExpressionPath(node);
785
+ if (!path2 || isLocallyBound(context, path2.root, path2.segments[0])) return;
786
+ const explicitGlobalThis = path2.segments[0] === "globalThis";
787
+ const normalized = explicitGlobalThis ? path2.segments.slice(1) : path2.segments;
788
+ let match;
789
+ for (let length = normalized.length; length >= (explicitGlobalThis ? 1 : 2); length -= 1) {
790
+ const candidate = normalized.slice(0, length).join(".");
791
+ if (globals.has(candidate)) {
792
+ match = candidate;
793
+ break;
794
+ }
795
+ }
796
+ if (match) report(node, match);
797
+ else if (!scopeAware && globals.has(path2.segments[0])) {
798
+ report(node, path2.segments[0]);
799
+ }
446
800
  },
447
801
  CallExpression(node) {
802
+ if (scopeAware) return;
448
803
  const callee = node.callee?.type === "Identifier" ? node.callee.name : void 0;
449
804
  if (callee && globals.has(callee)) report(node, callee);
450
805
  },
451
806
  NewExpression(node) {
807
+ if (scopeAware) return;
452
808
  const callee = node.callee?.type === "Identifier" ? node.callee.name : void 0;
453
809
  if (callee && globals.has(callee)) report(node, callee);
810
+ },
811
+ Identifier(node) {
812
+ if (!scopeAware || !node.name || !globals.has(node.name) || !isValueIdentifierReference(context, node) || isLocallyBound(context, node, node.name)) {
813
+ return;
814
+ }
815
+ report(node, node.name);
454
816
  }
455
817
  };
456
818
  }