arkgate 4.6.6 → 4.7.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 (66) hide show
  1. package/CHANGELOG.md +139 -2
  2. package/README.md +22 -10
  3. package/SECURITY.md +1 -1
  4. package/bin/ark-check-runtime.mjs +21 -341
  5. package/bin/ark-mcp-runtime.mjs +71 -325
  6. package/bin/ark-shared.mjs +24 -158
  7. package/bin/lib/adapter-contract.mjs +17 -36
  8. package/bin/lib/analysis-engine.mjs +6 -6
  9. package/bin/lib/ark-run-doctor.mjs +144 -0
  10. package/bin/lib/ark-run-facts.mjs +472 -0
  11. package/bin/lib/ark-run-report.mjs +57 -0
  12. package/bin/lib/ark-run-sensors.mjs +309 -0
  13. package/bin/lib/check-args.mjs +173 -0
  14. package/bin/lib/check-config-detect.mjs +101 -0
  15. package/bin/lib/check-watch.mjs +80 -0
  16. package/bin/lib/config-contract.mjs +86 -11
  17. package/bin/lib/deep-module-coach.mjs +3 -0
  18. package/bin/lib/diagnostic-catalog.mjs +8 -0
  19. package/bin/lib/doctor-advisories.mjs +45 -8
  20. package/bin/lib/doctor-human.mjs +519 -0
  21. package/bin/lib/doctor-plan.mjs +62 -445
  22. package/bin/lib/extra-merge-teeth.mjs +187 -0
  23. package/bin/lib/github-enforcement.mjs +22 -9
  24. package/bin/lib/html-report-advisories.mjs +2 -0
  25. package/bin/lib/html-report-depth.mjs +22 -2
  26. package/bin/lib/html-report.mjs +40 -7
  27. package/bin/lib/mcp-hook-payload.mjs +328 -0
  28. package/bin/lib/package-manager.mjs +174 -0
  29. package/bin/lib/policy-delta-io.mjs +4 -0
  30. package/bin/lib/remediation.mjs +132 -0
  31. package/bin/lib/resolved-candidate-facts.mjs +67 -2
  32. package/bin/lib/rules-under-contract.mjs +37 -89
  33. package/bin/lib/snippet-analysis.mjs +43 -2
  34. package/bin/lib/status-command.mjs +28 -0
  35. package/bin/lib/status-manifest.mjs +23 -0
  36. package/bin/lib/team-parliament-io.mjs +4 -0
  37. package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
  38. package/dist/eslint/index.cjs +6 -2
  39. package/dist/eslint/index.d.ts +70 -2
  40. package/dist/eslint/index.js +6 -2
  41. package/dist/index.cjs +35 -35
  42. package/dist/index.d.ts +787 -272
  43. package/dist/index.js +35 -35
  44. package/docs/README.md +4 -3
  45. package/docs/agent-guide.md +21 -15
  46. package/docs/ai-gates.md +13 -0
  47. package/docs/configuration.md +24 -11
  48. package/docs/develop.md +12 -3
  49. package/docs/diagnostics.md +75 -0
  50. package/docs/enthusiast/README.md +4 -3
  51. package/docs/package-surface.md +17 -13
  52. package/docs/product-voice.md +6 -3
  53. package/docs/threat-model.md +1 -1
  54. package/docs/use.md +5 -4
  55. package/package.json +1 -1
  56. package/schemas/ark.config.schema.json +41 -2
  57. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  58. package/schemas/ark.status-manifest.schema.json +47 -0
  59. package/server.json +2 -2
  60. package/templates/agent-skills/README.md +1 -1
  61. package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
  62. package/templates/agent-skills/ark-place/SKILL.md +26 -2
  63. package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
  64. package/templates/skills/ark-adopt.md +23 -2
  65. package/templates/skills/ark-place.md +26 -2
  66. package/templates/skills/ark-runtime.md +66 -24
@@ -8,8 +8,8 @@
8
8
  * Pure CLI helper (bin/lib/config-contract.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- /** Current published ark.config.json schema version (ADR 0012: 1.1 adds optional arkRules). */
12
- export const ARK_CONFIG_SCHEMA_VERSION = '1.1';
11
+ /** Current published ark.config.json schema version (ADR 0020: 1.2 adds optional arkRun). */
12
+ export const ARK_CONFIG_SCHEMA_VERSION = '1.2';
13
13
  export const ARK_CONFIG_SCHEMA_URL = 'https://unpkg.com/arkgate@2/schemas/ark.config.schema.json';
14
14
  const DEFAULT_LAYER_NAMES = [
15
15
  'DomainModel',
@@ -50,6 +50,7 @@ export const DEFAULT_ARK_CONFIG_RULES = createDefaultRules();
50
50
  export const ARK_CONFIG_MIGRATIONS = [
51
51
  { from: 'unversioned', to: '1.0' },
52
52
  { from: '1.0', to: '1.1' },
53
+ { from: '1.1', to: '1.2' },
53
54
  ];
54
55
  const stringArraySchema = {
55
56
  type: 'array',
@@ -112,6 +113,8 @@ export const ARK_CONFIG_SCHEMA = {
112
113
  additionalProperties: { type: 'string', minLength: 1 },
113
114
  default: {},
114
115
  },
116
+ /** ADR 0020 — optional ArkRun extra. Absence is silent; unknown keys fail closed. */
117
+ arkRun: { $ref: '#/$defs/arkRun' },
115
118
  /** Team parliament — GitHub handles or emails who may loosen the law (not part of policy hash). */
116
119
  stewards: { ...stringArraySchema, default: [] },
117
120
  },
@@ -181,6 +184,20 @@ export const ARK_CONFIG_SCHEMA = {
181
184
  allowDisabledPeerIsolation: { type: 'boolean', default: false },
182
185
  },
183
186
  },
187
+ arkRun: {
188
+ type: 'object',
189
+ additionalProperties: false,
190
+ properties: {
191
+ mode: {
192
+ type: 'string',
193
+ enum: ['advisory', 'enforced'],
194
+ default: 'advisory',
195
+ },
196
+ compositionRoots: { ...stringArraySchema, default: [] },
197
+ managedLayers: { ...stringArraySchema, default: [] },
198
+ requireDeclarations: { type: 'boolean', default: true },
199
+ },
200
+ },
184
201
  },
185
202
  };
186
203
  export class ArkConfigValidationError extends Error {
@@ -314,8 +331,19 @@ function validateNode(value, schema, path, root, issues) {
314
331
  }
315
332
  }
316
333
  }
317
- function defaultedConfig(input) {
334
+ function defaultedArkRun(value) {
335
+ if (!isObject(value))
336
+ return value;
318
337
  return {
338
+ ...value,
339
+ mode: value.mode === undefined ? 'advisory' : value.mode,
340
+ compositionRoots: value.compositionRoots === undefined ? [] : value.compositionRoots,
341
+ managedLayers: value.managedLayers === undefined ? [] : value.managedLayers,
342
+ requireDeclarations: value.requireDeclarations === undefined ? true : value.requireDeclarations,
343
+ };
344
+ }
345
+ function defaultedConfig(input) {
346
+ const result = {
319
347
  ...input,
320
348
  $schema: input.$schema === undefined ? ARK_CONFIG_SCHEMA_URL : input.$schema,
321
349
  schemaVersion: input.schemaVersion === undefined ? ARK_CONFIG_SCHEMA_VERSION : input.schemaVersion,
@@ -325,6 +353,57 @@ function defaultedConfig(input) {
325
353
  ? DEFAULT_ARK_CONFIG_RULES.map((rule) => ({ ...rule }))
326
354
  : input.rules,
327
355
  };
356
+ if (input.arkRun !== undefined)
357
+ result.arkRun = defaultedArkRun(input.arkRun);
358
+ return result;
359
+ }
360
+ function validateArkRunExtra(config, issues) {
361
+ const extra = config.arkRun;
362
+ if (extra === undefined || !isObject(extra))
363
+ return;
364
+ const layerNames = new Set();
365
+ if (Array.isArray(config.layers)) {
366
+ for (const layer of config.layers) {
367
+ if (isObject(layer) && typeof layer.name === 'string' && layer.name.length > 0) {
368
+ layerNames.add(layer.name);
369
+ }
370
+ }
371
+ }
372
+ const managed = extra.managedLayers;
373
+ if (Array.isArray(managed)) {
374
+ managed.forEach((name, index) => {
375
+ if (typeof name === 'string' && name.length > 0 && !layerNames.has(name)) {
376
+ issues.push({
377
+ path: `$.arkRun.managedLayers[${index}]`,
378
+ message: `layer ${JSON.stringify(name)} is not declared in layers[]`,
379
+ });
380
+ }
381
+ });
382
+ }
383
+ if (extra.mode === 'enforced') {
384
+ const roots = extra.compositionRoots;
385
+ if (!Array.isArray(roots) || roots.length === 0) {
386
+ issues.push({
387
+ path: '$.arkRun.compositionRoots',
388
+ message: 'ARKRUN_MISSING_ROOT: enforced mode requires at least one composition root',
389
+ });
390
+ }
391
+ if (!Array.isArray(managed) || managed.length === 0) {
392
+ issues.push({
393
+ path: '$.arkRun.managedLayers',
394
+ message: 'enforced mode requires at least one managed layer',
395
+ });
396
+ }
397
+ }
398
+ }
399
+ function migratedFromOf(originalVersion) {
400
+ if (originalVersion === ARK_CONFIG_SCHEMA_VERSION)
401
+ return null;
402
+ if (originalVersion === 'unversioned')
403
+ return 'unversioned';
404
+ if (originalVersion === '1.0' || originalVersion === '1.1')
405
+ return originalVersion;
406
+ return null;
328
407
  }
329
408
  function knownInputVersions() {
330
409
  const versions = new Set([ARK_CONFIG_SCHEMA_VERSION]);
@@ -369,8 +448,8 @@ export function migrateArkConfig(input, source = 'ark.config.json') {
369
448
  }
370
449
  let version = originalVersion;
371
450
  const working = { ...input };
372
- // Walk the migration table. Each step is a pure version stamp for 1.0→1.1
373
- // (arkRules is optional; absence needs no field rewrite).
451
+ // Walk the migration table. Each step is a pure version stamp (optional extras
452
+ // like arkRules / arkRun need no field rewrite when absent).
374
453
  let guard = 0;
375
454
  while (version !== ARK_CONFIG_SCHEMA_VERSION && guard < ARK_CONFIG_MIGRATIONS.length + 1) {
376
455
  guard += 1;
@@ -394,17 +473,13 @@ export function migrateArkConfig(input, source = 'ark.config.json') {
394
473
  },
395
474
  ]);
396
475
  }
397
- const migratedFrom = originalVersion === 'unversioned'
398
- ? 'unversioned'
399
- : originalVersion === '1.0'
400
- ? '1.0'
401
- : null;
402
- return { candidate: defaultedConfig(working), migratedFrom };
476
+ return { candidate: defaultedConfig(working), migratedFrom: migratedFromOf(originalVersion) };
403
477
  }
404
478
  export function loadArkConfigContract(input, source = 'ark.config.json') {
405
479
  const { candidate, migratedFrom } = migrateArkConfig(input, source);
406
480
  const issues = [];
407
481
  validateNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
482
+ validateArkRunExtra(candidate, issues);
408
483
  if (issues.length > 0)
409
484
  throw new ArkConfigValidationError(source, issues);
410
485
  return { config: candidate, migratedFrom };
@@ -22,6 +22,8 @@ export const HOT_PATH_COMMIT_LIMIT = 200;
22
22
  export const HOT_PATH_LIST_CAP = 8;
23
23
  /** Minimum change hits before a path is “elevated”. */
24
24
  export const HOT_PATH_MIN_HITS = 3;
25
+ /** Kill hung git instead of stalling CI. */
26
+ export const SPAWN_TIMEOUT_MS = 8000;
25
27
 
26
28
  /**
27
29
  * Best-effort recent-churn paths from git history.
@@ -40,6 +42,7 @@ export function computeHotPathAdvisory(root, opts = {}) {
40
42
  encoding: 'utf8',
41
43
  maxBuffer: 8 * 1024 * 1024,
42
44
  stdio: ['ignore', 'pipe', 'pipe'],
45
+ timeout: SPAWN_TIMEOUT_MS,
43
46
  });
44
47
 
45
48
  const emptyUnavailable = (reason) => ({
@@ -56,6 +56,14 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
56
56
  entry('ARKRULE_INVARIANT', 'arkrules', 'ArkRule invariant failed', 'Reserved / remediation-recognized code for invariant-plane failures bound to an ArkRule id (coverage path also emits INVARIANT_UNCOVERED).', 'Fix the invariant for the ArkRule declared in arkrules/<Layer>.json, then preflight again. Do not demote without acknowledgement.'),
57
57
  entry('ARKRULE_SCOPE_EMPTY', 'arkrules', 'ArkRule appliesTo matched zero files', 'An ArkRule’s appliesTo globs matched no governed files — the rule cannot observe what it claims to protect.', 'Fix appliesTo globs so they match governed files, or remove the rule. Enforced empty scope fails; advisory empty scope warns.', { oftenAdvisory: true }),
58
58
  entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green.'),
59
+ // ── ArkRun (opt-in extra; RN05 dual-depth nextAction) ────────────────────
60
+ entry('ARKRUN_MISSING_ROOT', 'arkrun', 'No kernel factory in composition roots', 'The ArkRun extra is on but no createArkKernel / createStrictArkKernel / createArkKernelFromConfig / createStrictArkKernelFromConfig factory was found in arkRun.compositionRoots, so agents can skip the kernel while the write gate stays green.', 'Import createStrictArkKernel from @arkgate/runtime (never a removed arkgate/runtime shim) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
61
+ entry('ARKRUN_KERNEL_IN_DOMAIN', 'arkrun', 'Domain-role layer imports the kernel', 'A Domain-role layer imports @arkgate/runtime or kernel types. Domain stays kernel-free; composition roots and adapters own the factory.', 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again. Never mechanical-safe.'),
62
+ entry('ARKRUN_DIRECT_NEW', 'arkrun', 'Managed type constructed with new', 'A managed non-Domain file constructs an admitted type with new outside an ArkRun composition-root factory, skipping kernel resolve/registration.', 'Resolve the type from the kernel instead of constructing it with new, then preflight again. Never mechanical-safe — rewiring construction is a design decision.'),
63
+ entry('ARKRUN_UNDECLARED_EMIT', 'arkrun', 'Emit name not in raises/sends', 'A publisher / publish / raise / send call-site literal is not listed in the file’s raises or sends declaration.', 'Add the existing call-site name to raises or sends on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new emit stays judgment.'),
64
+ entry('ARKRUN_UNDECLARED_HANDLE', 'arkrun', 'Handle name not in reactsTo', 'A subscribe / registerHandler call-site literal is not listed in the file’s reactsTo declaration.', 'Add the existing call-site name to reactsTo on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new handle stays judgment.'),
65
+ entry('ARKRUN_UNDECLARED_DEPEND', 'arkrun', 'Depend name not in uses', 'A resolve / resolveSingleton call-site literal is not listed in the file’s uses declaration.', 'Add the existing call-site name to uses on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new depend stays judgment.'),
66
+ entry('ARKRUN_TRANSPORT_BYPASS', 'arkrun', 'Homemade broker or emitter import', 'A managed layer imports a closed broker/queue/emitter specifier (EventEmitter, queue clients, …) instead of the ArkRun kernel transport.', 'Send through the ArkRun kernel transport instead of importing that broker or emitter, then preflight again. Never mechanical-safe — homemade buses stay judgment.'),
59
67
  // ── atomic preflight / change set ────────────────────────────────────────
60
68
  entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
61
69
  entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
@@ -20,8 +20,20 @@ import { printParseHealthSection, summarizeParseHealth } from './parse-health.mj
20
20
  import { detectGraphBlindSpots, printGraphBlindSection } from './graph-blind.mjs';
21
21
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
22
22
  import { collectStewardNudge } from './team-parliament-io.mjs';
23
+ import { formatArkRunDoctorLines, summarizeArkRunSection } from './ark-run-doctor.mjs';
23
24
 
24
- export function computeDoctorAdvisories(root, config, cov, rules, files, ts, parseHealth, facts) {
25
+ function classificationFromCoverage(cov) {
26
+ return {
27
+ governedPercent: cov?.governed?.percent ?? null,
28
+ populatedLayerCount: Array.isArray(cov?.layers)
29
+ ? cov.layers.filter((row) => (row?.files ?? 0) > 0).length
30
+ : null,
31
+ classifiedFiles: cov?.governed?.classifiedFiles ?? null,
32
+ };
33
+ }
34
+
35
+ /** `activeViolations` must already exclude frozen baseline keys (report residual parity). */
36
+ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, parseHealth, facts, activeViolations) {
25
37
  const physicalCohesion = computePhysicalCohesion(root, files);
26
38
  const decisionMemory = computeReshapeDecisionMemory(root, files);
27
39
  physicalCohesion.reshapeDecisions = decisionMemory.summary;
@@ -41,6 +53,27 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
41
53
  })).filter((f) => f.path),
42
54
  }
43
55
  : undefined);
56
+ const classification = classificationFromCoverage(cov);
57
+ const rulesUnderContract = summarizeRulesUnderContract(root, config, factPaths, classification);
58
+ const arkRun = summarizeArkRunSection({
59
+ arkRun: config?.arkRun,
60
+ findings: activeViolations,
61
+ classification,
62
+ arkRules: {
63
+ active: rulesUnderContract?.active === true,
64
+ structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
65
+ structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
66
+ structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
67
+ invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
68
+ invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
69
+ invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
70
+ covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
71
+ uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
72
+ },
73
+ });
74
+ if (rulesUnderContract?.mergePlanes) {
75
+ rulesUnderContract.mergePlanes = arkRun.mergePlanes;
76
+ }
44
77
  return {
45
78
  contractHealth: computeContractHealth(root, config, cov, rules),
46
79
  ambientState: computeAmbientState(ts, root, config, files),
@@ -51,13 +84,8 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
51
84
  // AR12 — Rules under contract (honest counts; real test I/O, never empty-fileContents stub).
52
85
  // P1M: pass classification so extraMergeTeeth cannot arm at 0% governed.
53
86
  stewardNudge: collectStewardNudge(root, config),
54
- rulesUnderContract: summarizeRulesUnderContract(root, config, factPaths, {
55
- governedPercent: cov?.governed?.percent ?? null,
56
- populatedLayerCount: Array.isArray(cov?.layers)
57
- ? cov.layers.filter((row) => (row?.files ?? 0) > 0).length
58
- : null,
59
- classifiedFiles: cov?.governed?.classifiedFiles ?? null,
60
- }),
87
+ rulesUnderContract,
88
+ arkRun,
61
89
  };
62
90
  }
63
91
 
@@ -79,4 +107,13 @@ export function printDoctorAdvisories(advisories, io) {
79
107
  io.line(io.warn, nudge.ask);
80
108
  if (nudge.nextAction) io.line(' ', io.color.dim(`Next: ${nudge.nextAction}`));
81
109
  }
110
+ const arkRun = advisories.arkRun;
111
+ if (arkRun && arkRun.notAScore === true) {
112
+ console.log('');
113
+ console.log(io.color.bold('ArkRun (not a score)'));
114
+ const mark = arkRun.active && arkRun.residual?.count > 0 ? io.warn : ' ';
115
+ for (const text of formatArkRunDoctorLines(arkRun)) {
116
+ io.line(mark, text);
117
+ }
118
+ }
82
119
  }