arkgate 4.6.7 → 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 (56) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +20 -9
  3. package/SECURITY.md +1 -1
  4. package/bin/ark-check-runtime.mjs +13 -1
  5. package/bin/ark-mcp-runtime.mjs +65 -3
  6. package/bin/lib/adapter-contract.mjs +17 -36
  7. package/bin/lib/analysis-engine.mjs +6 -6
  8. package/bin/lib/ark-run-doctor.mjs +144 -0
  9. package/bin/lib/ark-run-facts.mjs +472 -0
  10. package/bin/lib/ark-run-report.mjs +57 -0
  11. package/bin/lib/ark-run-sensors.mjs +309 -0
  12. package/bin/lib/config-contract.mjs +86 -11
  13. package/bin/lib/diagnostic-catalog.mjs +8 -0
  14. package/bin/lib/doctor-advisories.mjs +45 -8
  15. package/bin/lib/doctor-human.mjs +10 -0
  16. package/bin/lib/doctor-plan.mjs +20 -16
  17. package/bin/lib/extra-merge-teeth.mjs +187 -0
  18. package/bin/lib/html-report-advisories.mjs +2 -0
  19. package/bin/lib/html-report-depth.mjs +22 -2
  20. package/bin/lib/html-report.mjs +16 -0
  21. package/bin/lib/remediation.mjs +132 -0
  22. package/bin/lib/resolved-candidate-facts.mjs +67 -2
  23. package/bin/lib/rules-under-contract.mjs +37 -89
  24. package/bin/lib/snippet-analysis.mjs +43 -2
  25. package/bin/lib/status-command.mjs +28 -0
  26. package/bin/lib/status-manifest.mjs +23 -0
  27. package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
  28. package/dist/eslint/index.cjs +6 -2
  29. package/dist/eslint/index.d.ts +70 -2
  30. package/dist/eslint/index.js +6 -2
  31. package/dist/index.cjs +35 -35
  32. package/dist/index.d.ts +787 -272
  33. package/dist/index.js +35 -35
  34. package/docs/README.md +2 -1
  35. package/docs/agent-guide.md +21 -15
  36. package/docs/ai-gates.md +13 -0
  37. package/docs/configuration.md +24 -11
  38. package/docs/develop.md +12 -3
  39. package/docs/diagnostics.md +75 -0
  40. package/docs/enthusiast/README.md +4 -3
  41. package/docs/package-surface.md +16 -13
  42. package/docs/product-voice.md +6 -3
  43. package/docs/threat-model.md +1 -1
  44. package/docs/use.md +5 -4
  45. package/package.json +1 -1
  46. package/schemas/ark.config.schema.json +41 -2
  47. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  48. package/schemas/ark.status-manifest.schema.json +47 -0
  49. package/server.json +2 -2
  50. package/templates/agent-skills/README.md +1 -1
  51. package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
  52. package/templates/agent-skills/ark-place/SKILL.md +26 -2
  53. package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
  54. package/templates/skills/ark-adopt.md +23 -2
  55. package/templates/skills/ark-place.md +26 -2
  56. package/templates/skills/ark-runtime.md +66 -24
@@ -0,0 +1,187 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/extraMergeTeeth.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/extra-merge-teeth.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
12
+ export function normalizeExtraMergeTeethClassification(classification) {
13
+ if (!classification)
14
+ return {};
15
+ const governedPercent = typeof classification.governedPercent === 'number' ? classification.governedPercent : null;
16
+ let populatedLayerCount = typeof classification.populatedLayerCount === 'number'
17
+ ? classification.populatedLayerCount
18
+ : null;
19
+ if (populatedLayerCount == null &&
20
+ typeof classification.classifiedFiles === 'number') {
21
+ populatedLayerCount = classification.classifiedFiles > 0 ? 1 : 0;
22
+ }
23
+ return { governedPercent, populatedLayerCount };
24
+ }
25
+ export function extraMergeTeethAllowed(classification) {
26
+ const normalized = normalizeExtraMergeTeethClassification(classification);
27
+ const governed = typeof normalized.governedPercent === 'number' ? normalized.governedPercent : null;
28
+ const populated = typeof normalized.populatedLayerCount === 'number' ? normalized.populatedLayerCount : null;
29
+ if (governed == null && populated == null)
30
+ return true;
31
+ return ((governed ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR && (populated ?? 0) >= 1);
32
+ }
33
+ export function classifyResolvedLayerCoverage(files) {
34
+ const total = files.length;
35
+ let classified = 0;
36
+ const populated = new Set();
37
+ for (const file of files) {
38
+ const layer = typeof file.layer === 'string' && file.layer.length > 0 ? file.layer : null;
39
+ if (!layer)
40
+ continue;
41
+ classified += 1;
42
+ populated.add(layer);
43
+ }
44
+ return {
45
+ governedPercent: total > 0 ? Math.round((classified / total) * 100) : 0,
46
+ populatedLayerCount: populated.size,
47
+ };
48
+ }
49
+ export function isArkRunRuleId(ruleId) {
50
+ return typeof ruleId === 'string' && ruleId.startsWith('ARKRUN_');
51
+ }
52
+ export function isExtraPlaneFinding(violation) {
53
+ if (violation?.arkruleId != null)
54
+ return true;
55
+ const id = typeof violation?.ruleId === 'string' ? violation.ruleId : '';
56
+ return id.startsWith('ARKRULE') || id.startsWith('arkrule') || id.startsWith('ARKRUN_');
57
+ }
58
+ /**
59
+ * Under the classification floor, demote enforced extra-plane findings in place
60
+ * so merge/write/CI match (layer graph only). Unknown classification is a no-op.
61
+ */
62
+ export function demoteExtraPlaneTeethUnderClassificationFloor(violations, classification = {}) {
63
+ if (!Array.isArray(violations))
64
+ return violations;
65
+ if (extraMergeTeethAllowed(classification))
66
+ return violations;
67
+ for (const violation of violations) {
68
+ if (isExtraPlaneFinding(violation) && violation.failsStrict !== false) {
69
+ violation.failsStrict = false;
70
+ if (violation.severity === 'error')
71
+ violation.severity = 'warning';
72
+ }
73
+ }
74
+ return violations;
75
+ }
76
+ /** Stamp for extra-plane honesty: never one architecture score. */
77
+ export const MERGE_PLANES_DUAL_STAMP = 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime); ArkRun = kernel usage + declarations (not a score). Extra planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth. Advisory ArkRun ≠ merge teeth.';
78
+ function countOrZero(value) {
79
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
80
+ }
81
+ /**
82
+ * Which extra planes can fail merge. Counts and stamps only — never a score.
83
+ */
84
+ export function composeMergePlanesHonesty(input = {}) {
85
+ const normalized = normalizeExtraMergeTeethClassification(input.classification);
86
+ const governedPercent = typeof normalized.governedPercent === 'number' ? normalized.governedPercent : null;
87
+ const populatedLayerCount = typeof normalized.populatedLayerCount === 'number' ? normalized.populatedLayerCount : null;
88
+ const classificationKnown = governedPercent != null || populatedLayerCount != null;
89
+ const classificationAllowsTeeth = extraMergeTeethAllowed(normalized);
90
+ const arkRules = input.arkRules;
91
+ const structureEnforced = countOrZero(arkRules?.structureEnforced);
92
+ const structureTotal = countOrZero(arkRules?.structureTotal);
93
+ const structureAdvisory = typeof arkRules?.structureAdvisory === 'number'
94
+ ? countOrZero(arkRules.structureAdvisory)
95
+ : Math.max(0, structureTotal - structureEnforced);
96
+ const invariantEnforced = countOrZero(arkRules?.invariantEnforced);
97
+ const invariantTotal = countOrZero(arkRules?.invariantTotal);
98
+ const invariantAdvisory = typeof arkRules?.invariantAdvisory === 'number'
99
+ ? countOrZero(arkRules.invariantAdvisory)
100
+ : Math.max(0, invariantTotal - invariantEnforced);
101
+ const arkRulesHasEnforced = structureEnforced > 0 || invariantEnforced > 0;
102
+ const arkRunPresent = input.arkRun?.present === true;
103
+ const arkRunMode = input.arkRun?.mode === 'enforced' || input.arkRun?.mode === 'advisory'
104
+ ? input.arkRun.mode
105
+ : null;
106
+ const arkRunResidual = countOrZero(input.arkRun?.residualCount);
107
+ const arkRunHasEnforced = arkRunPresent && arkRunMode === 'enforced';
108
+ const arkRunTeeth = arkRunHasEnforced && classificationAllowsTeeth;
109
+ const hasEnforcedTeeth = arkRulesHasEnforced || arkRunHasEnforced;
110
+ const extraMergeTeeth = hasEnforcedTeeth && classificationAllowsTeeth;
111
+ const teethDeferredForClassification = hasEnforcedTeeth && classificationKnown && !classificationAllowsTeeth;
112
+ let failMergeWhen;
113
+ if (extraMergeTeeth) {
114
+ const extras = [];
115
+ if (arkRulesHasEnforced)
116
+ extras.push('enforced structure/invariant findings');
117
+ if (arkRunHasEnforced)
118
+ extras.push('enforced ArkRun skip findings');
119
+ failMergeWhen = `Layer graph failures plus ${extras.join(' and ')} (advisory extras never fail merge alone).`;
120
+ }
121
+ else if (teethDeferredForClassification) {
122
+ const which = [
123
+ arkRulesHasEnforced ? 'ArkRules structure/invariant' : null,
124
+ arkRunHasEnforced ? 'ArkRun' : null,
125
+ ]
126
+ .filter((part) => Boolean(part))
127
+ .join(' and ');
128
+ failMergeWhen = `Layer graph only — enforced ${which} findings are demoted under the teeth floor (need ≥${EXTRA_MERGE_TEETH_GOVERNED_FLOOR}% governed and ≥1 populated layer); they do not merge-block until classification is honest.`;
129
+ }
130
+ else {
131
+ const arkRunBit = !arkRunPresent
132
+ ? ' Absence of arkRun is silent.'
133
+ : arkRunMode === 'advisory'
134
+ ? ' Advisory ArkRun never merge-blocks.'
135
+ : ' ArkRun extra is present but does not arm merge teeth.';
136
+ failMergeWhen =
137
+ 'Layer graph only — no enforced ArkRules structure/invariant teeth on this tree. Advisory packs do not arm merge teeth.' +
138
+ arkRunBit;
139
+ }
140
+ const out = {
141
+ layers: {
142
+ role: 'inter-layer-edges',
143
+ alwaysOnGate: true,
144
+ note: 'Import/export layer graph — the default merge plane. Absent arkRules or arkRun changes nothing here.',
145
+ },
146
+ structureSensors: {
147
+ role: 'intra-layer-heuristics',
148
+ total: structureTotal,
149
+ enforced: structureEnforced,
150
+ advisory: structureAdvisory,
151
+ note: 'Structure sensors are heuristics (prefer false negatives). Only mode:enforced fails merge; noisy sensors stay advisory by default. Advisory-only packs never add merge teeth (FG-ARKRULES-ADVISORY-ONLY).',
152
+ },
153
+ invariants: {
154
+ role: 'catalog-plus-coverage',
155
+ total: invariantTotal,
156
+ enforced: invariantEnforced,
157
+ advisory: invariantAdvisory,
158
+ covered: countOrZero(arkRules?.covered),
159
+ uncovered: countOrZero(arkRules?.uncovered),
160
+ note: 'Invariants are catalog + coverage evidence, not a business runtime. Enforced + proven-uncovered fails merge; absence of enforced rules adds no extra teeth.',
161
+ },
162
+ arkRun: {
163
+ role: 'kernel-usage-and-declarations',
164
+ present: arkRunPresent,
165
+ mode: arkRunMode,
166
+ residualCount: arkRunResidual,
167
+ extraMergeTeeth: arkRunTeeth,
168
+ note: arkRunPresent
169
+ ? arkRunMode === 'enforced'
170
+ ? 'Enforced ArkRun arms extra merge teeth only when the layer plane is classified. Residual is a count, never a score.'
171
+ : 'Advisory ArkRun never adds merge teeth and never flips valid. Residual is a count, never a score.'
172
+ : 'Absence of arkRun is silent — Layers and ArkRules verdicts unchanged. The extra never becomes a score.',
173
+ },
174
+ extraMergeTeeth,
175
+ dualPlaneStamp: MERGE_PLANES_DUAL_STAMP,
176
+ failMergeWhen,
177
+ };
178
+ if (classificationKnown) {
179
+ out.classificationGate = {
180
+ governedPercent,
181
+ populatedLayerCount,
182
+ floorPercent: EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
183
+ allowsTeeth: classificationAllowsTeeth,
184
+ };
185
+ }
186
+ return out;
187
+ }
@@ -10,6 +10,7 @@
10
10
  import { effectiveCapabilityDeny } from './analysis-engine.mjs';
11
11
  import { graphBlindSpotsHtml } from './graph-blind.mjs';
12
12
  import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
13
+ import { formatArkRunHtml } from './ark-run-report.mjs';
13
14
  import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
14
15
 
15
16
  // htmlEscape is injected by the caller (html-report.mjs) — importing it back
@@ -351,6 +352,7 @@ export function renderAdvisorySections(advisories, escape) {
351
352
  parseHealthHtml(advisories.parseHealth),
352
353
  graphBlindSpotsHtml(advisories.graphBlindSpots, esc),
353
354
  rulesUnderContractHtml(advisories.rulesUnderContract),
355
+ formatArkRunHtml(advisories.arkRun, esc),
354
356
  ]
355
357
  .filter(Boolean)
356
358
  .join('\n');
@@ -19,6 +19,7 @@ import {
19
19
  buildProductHonesty,
20
20
  } from './enforcement-honesty.mjs';
21
21
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
22
+ import { summarizeArkRunSection } from './ark-run-doctor.mjs';
22
23
  import { readBaseline, baselineOccurrenceKeys } from './violations.mjs';
23
24
  import { describePackageVersionDualTruth } from './field-install.mjs';
24
25
  import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
@@ -127,13 +128,31 @@ export function buildReportDepthPayload(
127
128
  packageVersionTruth?.selfHost === true ||
128
129
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
129
130
  });
130
- const rulesUnderContract = summarizeRulesUnderContract(root, config, undefined, {
131
+ const classification = {
131
132
  governedPercent: coverage?.governed?.percent ?? null,
132
133
  populatedLayerCount: Array.isArray(coverage?.layers)
133
134
  ? coverage.layers.filter((row) => (row?.files ?? 0) > 0).length
134
135
  : null,
135
136
  classifiedFiles: coverage?.governed?.classifiedFiles ?? null,
137
+ };
138
+ const rulesUnderContract = summarizeRulesUnderContract(root, config, undefined, classification);
139
+ const arkRun = summarizeArkRunSection({
140
+ arkRun: config?.arkRun,
141
+ findings: activeViolations,
142
+ classification,
143
+ arkRules: {
144
+ active: rulesUnderContract?.active === true,
145
+ structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
146
+ structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
147
+ structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
148
+ invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
149
+ invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
150
+ invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
151
+ covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
152
+ uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
153
+ },
136
154
  });
155
+ if (rulesUnderContract?.mergePlanes) rulesUnderContract.mergePlanes = arkRun.mergePlanes;
137
156
  // Single residual expression (parity with doctor): nextPilot || extractionCard.
138
157
  const residualPilot =
139
158
  pilotLoop?.nextPilot || pilotLoop?.extractionCard || null;
@@ -155,7 +174,7 @@ export function buildReportDepthPayload(
155
174
  residualPilots: Boolean(residualPilot) && designFitness.designWeak === true,
156
175
  pilotTarget: residualPilot?.pilotTarget ?? residualPilot?.pilot ?? null,
157
176
  arkRulesMergeHonesty: rulesUnderContract?.mergePlanes
158
- ? { active: rulesUnderContract.active === true, ...rulesUnderContract.mergePlanes }
177
+ ? { active: rulesUnderContract.active === true || arkRun?.active === true, ...rulesUnderContract.mergePlanes }
159
178
  : null,
160
179
  primaryNextAction: postGreenPath?.action ?? dualTruthNext,
161
180
  activeBlockingViolations: activeBlockingCount,
@@ -198,6 +217,7 @@ export function buildReportDepthPayload(
198
217
  // P0-B / P1-M — folded into designDepth so --report stays a single payload.
199
218
  productHonesty,
200
219
  mergePlanes: rulesUnderContract?.mergePlanes ?? null,
220
+ arkRun,
201
221
  improvementCompass,
202
222
  deepModuleCoach,
203
223
  },
@@ -149,6 +149,8 @@ export function buildReportSnapshot({
149
149
  mode,
150
150
  /** DF02 — thin status compass slice (mode + residual ids, notAScore). */
151
151
  improvementCompass = null,
152
+ /** RN08 — thin status ArkRun slice (notAScore; residual count, never a score). */
153
+ arkRun = null,
152
154
  }) {
153
155
  const layers = Array.isArray(config?.layers) ? config.layers : [];
154
156
  const rules = Array.isArray(config?.rules) ? config.rules : [];
@@ -196,6 +198,20 @@ export function buildReportSnapshot({
196
198
  if (improvementCompass && typeof improvementCompass === 'object') {
197
199
  snapshot.improvementCompass = improvementCompass;
198
200
  }
201
+ if (arkRun && typeof arkRun === 'object' && arkRun.notAScore === true) {
202
+ snapshot.arkRun = {
203
+ notAScore: true,
204
+ present: arkRun.active === true || arkRun.present === true,
205
+ mode: arkRun.mode === 'enforced' || arkRun.mode === 'advisory' ? arkRun.mode : null,
206
+ extraMergeTeeth: arkRun.extraMergeTeeth === true,
207
+ residual:
208
+ typeof arkRun.residual === 'number'
209
+ ? arkRun.residual
210
+ : typeof arkRun.residual?.count === 'number'
211
+ ? arkRun.residual.count
212
+ : null,
213
+ };
214
+ }
199
215
  return snapshot;
200
216
  }
201
217
 
@@ -19,6 +19,7 @@ export const MECHANICAL_SAFE_KINDS = [
19
19
  'type-only-import-move',
20
20
  'import-type-from-pure-type-module',
21
21
  'import-type-of-type-exports',
22
+ 'arkrun-declaration-list',
22
23
  // port-proof-inject-binding is intentionally NOT mechanical-safe (signature change).
23
24
  ];
24
25
  /**
@@ -40,6 +41,7 @@ export const KNOWN_FIX_CLASSES = [
40
41
  'intent-relocation',
41
42
  'break-cycle',
42
43
  'review-contract',
44
+ 'arkrun-usage',
43
45
  ];
44
46
  const PURE_SHARED_RE = /(^|\/)(constants|types|enums|shared-types|shared\/(?:types|constants)|test-projects)(\/|\.|$)|(?:^|\/)[^/]*(?:constants|types)(?:\.[cm]?[jt]sx?)?$/i;
45
47
  const KERNEL_EMIT_RE = /(^|\/)(?:kernel(?:\/|$)|events?(?:\/|\.|$)|bootstrap(?:\.[cm]?[jt]sx?)?$|emitter(?:\.[cm]?[jt]sx?)?$)|(?:^|\/)(?:intents?|publish)(?:\/|\.|$)/i;
@@ -87,6 +89,101 @@ export function layerImportNextAction(violation) {
87
89
  }
88
90
  return 'Classify the import: if it is constants/types/pure, adopt into DomainModel or SharedKernel; define a port only if the target is a real use-case. Then preflight again.';
89
91
  }
92
+ const ARKRUN_UNDECLARED_RULE_IDS = new Set([
93
+ 'ARKRUN_UNDECLARED_EMIT',
94
+ 'ARKRUN_UNDECLARED_HANDLE',
95
+ 'ARKRUN_UNDECLARED_DEPEND',
96
+ ]);
97
+ const ARKRUN_JUDGMENT_RULE_IDS = new Set([
98
+ 'ARKRUN_MISSING_ROOT',
99
+ 'ARKRUN_KERNEL_IN_DOMAIN',
100
+ 'ARKRUN_DIRECT_NEW',
101
+ 'ARKRUN_TRANSPORT_BYPASS',
102
+ ]);
103
+ function arkRunCallSiteName(violation) {
104
+ return typeof violation.target === 'string' && violation.target.trim().length > 0
105
+ ? violation.target.trim()
106
+ : undefined;
107
+ }
108
+ function isArkRunDeclarationListSafe(violation) {
109
+ return (typeof violation.ruleId === 'string' &&
110
+ ARKRUN_UNDECLARED_RULE_IDS.has(violation.ruleId) &&
111
+ arkRunCallSiteName(violation) !== undefined);
112
+ }
113
+ /** Catalog `fix` is the no-target form; a present `target` specializes it. */
114
+ function arkRunNextAction(violation) {
115
+ const target = arkRunCallSiteName(violation);
116
+ const fromLayer = typeof violation.fromLayer === 'string' && violation.fromLayer.length > 0
117
+ ? violation.fromLayer
118
+ : undefined;
119
+ switch (violation.ruleId) {
120
+ case 'ARKRUN_MISSING_ROOT':
121
+ return target
122
+ ? `Import createStrictArkKernel from @arkgate/runtime and call it in composition root ${target} listed in arkRun.compositionRoots, then preflight again.`
123
+ : '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.';
124
+ case 'ARKRUN_KERNEL_IN_DOMAIN':
125
+ return target
126
+ ? `Move the kernel import of ${target} out of ${fromLayer ?? 'the Domain-role layer'} into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again.`
127
+ : '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.';
128
+ case 'ARKRUN_DIRECT_NEW':
129
+ return target
130
+ ? `Resolve ${target} from the kernel instead of constructing it with new, then preflight again.`
131
+ : 'Resolve the type from the kernel instead of constructing it with new, then preflight again. Never mechanical-safe — rewiring construction is a design decision.';
132
+ case 'ARKRUN_UNDECLARED_EMIT':
133
+ return target
134
+ ? `Add ${target} to raises or sends on the managed component, then preflight again.`
135
+ : '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.';
136
+ case 'ARKRUN_UNDECLARED_HANDLE':
137
+ return target
138
+ ? `Add ${target} to reactsTo on the managed component, then preflight again.`
139
+ : '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.';
140
+ case 'ARKRUN_UNDECLARED_DEPEND':
141
+ return target
142
+ ? `Add ${target} to uses on the managed component, then preflight again.`
143
+ : '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.';
144
+ case 'ARKRUN_TRANSPORT_BYPASS':
145
+ return target
146
+ ? `Send through the ArkRun kernel transport instead of importing ${target}, then preflight again.`
147
+ : 'Send through the ArkRun kernel transport instead of importing that broker or emitter, then preflight again. Never mechanical-safe — homemade buses stay judgment.';
148
+ default:
149
+ return `Resolve ${typeof violation.ruleId === 'string' && violation.ruleId.length > 0 ? violation.ruleId : 'ARK_UNKNOWN'} without weakening ark.config.json, then run Ark again.`;
150
+ }
151
+ }
152
+ function arkRunEnthusiastHint(violation) {
153
+ const target = arkRunCallSiteName(violation);
154
+ switch (violation.ruleId) {
155
+ case 'ARKRUN_MISSING_ROOT':
156
+ return target
157
+ ? `Call createStrictArkKernel from @arkgate/runtime in ${target} so the app actually uses the kernel.`
158
+ : 'Call createStrictArkKernel from @arkgate/runtime in a listed composition root so the app actually uses the kernel.';
159
+ case 'ARKRUN_KERNEL_IN_DOMAIN':
160
+ return target
161
+ ? `Domain stays kernel-free. Move the ${target} import to a composition root or adapter — never a removed arkgate/runtime shim.`
162
+ : 'Domain stays kernel-free. Move that @arkgate/runtime import to a composition root or adapter — never a removed arkgate/runtime shim.';
163
+ case 'ARKRUN_DIRECT_NEW':
164
+ return target
165
+ ? `Do not construct ${target} with new. Resolve it from the kernel instead.`
166
+ : 'Do not construct that managed type with new. Resolve it from the kernel instead.';
167
+ case 'ARKRUN_UNDECLARED_EMIT':
168
+ return target
169
+ ? `Add "${target}" to raises or sends. Do not invent a new emit.`
170
+ : 'Add the name you already publish to raises or sends. Do not invent a new emit.';
171
+ case 'ARKRUN_UNDECLARED_HANDLE':
172
+ return target
173
+ ? `Add "${target}" to reactsTo. Do not invent a new handle.`
174
+ : 'Add the name you already subscribe to reactsTo.';
175
+ case 'ARKRUN_UNDECLARED_DEPEND':
176
+ return target
177
+ ? `Add "${target}" to uses. Do not invent a new depend.`
178
+ : 'Add the name you already resolve to uses.';
179
+ case 'ARKRUN_TRANSPORT_BYPASS':
180
+ return target
181
+ ? `Do not import ${target} here. Send through the ArkRun kernel transport.`
182
+ : 'Do not import that broker or EventEmitter here. Send through the ArkRun kernel transport.';
183
+ default:
184
+ return 'Read the ArkRun finding and use the kernel instead of skipping it.';
185
+ }
186
+ }
90
187
  /** One deterministic re-entry action shared by human and machine denial surfaces. */
91
188
  export function deterministicNextAction(violation) {
92
189
  switch (violation.ruleId) {
@@ -110,6 +207,14 @@ export function deterministicNextAction(violation) {
110
207
  : 'the ArkRule'} (declared in ${typeof violation.arkruleSource === 'string' && violation.arkruleSource.length > 0
111
208
  ? violation.arkruleSource
112
209
  : 'arkrules/<Layer>.json'}), then preflight again. Do not demote the rule without a hash-bound policy acknowledgement.`;
210
+ case 'ARKRUN_MISSING_ROOT':
211
+ case 'ARKRUN_KERNEL_IN_DOMAIN':
212
+ case 'ARKRUN_DIRECT_NEW':
213
+ case 'ARKRUN_UNDECLARED_EMIT':
214
+ case 'ARKRUN_UNDECLARED_HANDLE':
215
+ case 'ARKRUN_UNDECLARED_DEPEND':
216
+ case 'ARKRUN_TRANSPORT_BYPASS':
217
+ return arkRunNextAction(violation);
113
218
  default:
114
219
  if (typeof violation.ruleId === 'string' && violation.ruleId.startsWith('ARKRULE_')) {
115
220
  return `Fix the ArkRule ${typeof violation.arkruleId === 'string' ? violation.arkruleId : violation.ruleId}, then preflight again.`;
@@ -226,6 +331,22 @@ export function classifyRemediation(violation) {
226
331
  rationale: 'ArkRule structure/invariant findings are never mechanical-safe — restore private state, factory shape, event publish, coverage, or redesign the aggregate with judgment.',
227
332
  };
228
333
  }
334
+ if (violation && isArkRunDeclarationListSafe(violation)) {
335
+ return {
336
+ class: 'mechanical-safe',
337
+ confidence: 0.9,
338
+ remediationKind: 'arkrun-declaration-list',
339
+ rationale: 'Call-site literal already exists; adding it to the declaration list is behavior-preserving. Inventing a new emit/handle/depend stays judgment.',
340
+ };
341
+ }
342
+ if (typeof ruleId === 'string' &&
343
+ (ARKRUN_JUDGMENT_RULE_IDS.has(ruleId) || ARKRUN_UNDECLARED_RULE_IDS.has(ruleId))) {
344
+ return {
345
+ class: 'judgment',
346
+ confidence: 0.85,
347
+ rationale: 'ArkRun usage, construction, and homemade-transport findings are never mechanical-safe. Declaration-list edits are mechanical-safe only when the call-site literal is already present as target.',
348
+ };
349
+ }
229
350
  if (typeof ruleId === 'string' && ruleId.length > 0) {
230
351
  return {
231
352
  class: 'judgment',
@@ -338,6 +459,17 @@ export function enrichViolationWithFixClass(violation) {
338
459
  enriched.enthusiastHint =
339
460
  'Two modules import each other in a loop. Extract shared code, invert one dependency behind a port, or merge them if they are really one unit.';
340
461
  break;
462
+ case 'ARKRUN_MISSING_ROOT':
463
+ case 'ARKRUN_KERNEL_IN_DOMAIN':
464
+ case 'ARKRUN_DIRECT_NEW':
465
+ case 'ARKRUN_UNDECLARED_EMIT':
466
+ case 'ARKRUN_UNDECLARED_HANDLE':
467
+ case 'ARKRUN_UNDECLARED_DEPEND':
468
+ case 'ARKRUN_TRANSPORT_BYPASS':
469
+ enriched.fixClass = 'arkrun-usage';
470
+ enriched.effort = ARKRUN_UNDECLARED_RULE_IDS.has(violation.ruleId ?? '') ? 'small' : 'medium';
471
+ enriched.enthusiastHint = arkRunEnthusiastHint(violation);
472
+ break;
341
473
  default:
342
474
  enriched.fixClass = 'review-contract';
343
475
  enriched.effort = 'small';
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
 
11
- import { isScanExcludedRelative } from '../ark-shared.mjs';
11
+ import { globToRegExp, isScanExcludedRelative } from '../ark-shared.mjs';
12
12
  import {
13
13
  AMBIENT_CAPABILITY_ENTRIES,
14
14
  collectCapabilityUses,
@@ -35,6 +35,11 @@ import {
35
35
  } from './ast-scan.mjs';
36
36
  import { provePortProofInject } from './port-proof.mjs';
37
37
  import { extractClassShapesFromSource } from './arkrules-sensors.mjs';
38
+ import {
39
+ extractArkRunDeclarationsFromSource,
40
+ extractArkRunKernelCallsFromSource,
41
+ extractArkRunManagedNewsFromSource,
42
+ } from './ark-run-facts.mjs';
38
43
  import {
39
44
  collectGovernedFiles,
40
45
  isGovernableSourceFile,
@@ -998,6 +1003,12 @@ export function resolveCandidateFacts({
998
1003
  const safetyUses = [];
999
1004
  /** ADR 0013 class-shape facts for ArkRules structure sensors. */
1000
1005
  const classShapes = [];
1006
+ /** ADR 0022 / RN03 — syntax evidence only; sensors emit in RN04. */
1007
+ const arkRunKernelCalls = [];
1008
+ const arkRunManagedNews = [];
1009
+ const arkRunCompositionRootHits = [];
1010
+ const arkRunDeclarations = [];
1011
+ const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
1001
1012
 
1002
1013
  for (const candidate of candidateFiles) {
1003
1014
  const sourceFile = ts.createSourceFile(
@@ -1087,6 +1098,56 @@ export function resolveCandidateFacts({
1087
1098
  } catch {
1088
1099
  // Never fail the resolver for shape extraction; sensors stay silent on this file.
1089
1100
  }
1101
+ try {
1102
+ arkRunKernelCalls.push(
1103
+ ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1104
+ );
1105
+ } catch {
1106
+ // Never fail the resolver for ArkRun call extraction.
1107
+ }
1108
+ try {
1109
+ arkRunDeclarations.push(
1110
+ ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1111
+ );
1112
+ } catch {
1113
+ // Never fail the resolver for ArkRun declaration extraction.
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1119
+ for (const candidate of candidateFiles) {
1120
+ if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1121
+ try {
1122
+ arkRunManagedNews.push(
1123
+ ...extractArkRunManagedNewsFromSource(
1124
+ candidate.path,
1125
+ candidate.content,
1126
+ admittedTypeNames
1127
+ )
1128
+ );
1129
+ } catch {
1130
+ // Never fail the resolver for managed-new extraction.
1131
+ }
1132
+ }
1133
+
1134
+ if (compositionRootPatterns.length > 0) {
1135
+ const factoryFiles = new Set(
1136
+ arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
1137
+ );
1138
+ for (const candidate of candidateFiles) {
1139
+ for (const pattern of compositionRootPatterns) {
1140
+ try {
1141
+ if (!globToRegExp(pattern).test(candidate.path)) continue;
1142
+ } catch {
1143
+ continue;
1144
+ }
1145
+ arkRunCompositionRootHits.push({
1146
+ file: candidate.path,
1147
+ matchedRoot: pattern,
1148
+ hasKernelFactory: factoryFiles.has(candidate.path),
1149
+ });
1150
+ }
1090
1151
  }
1091
1152
  }
1092
1153
 
@@ -1152,7 +1213,7 @@ export function resolveCandidateFacts({
1152
1213
 
1153
1214
  const projectPackageName = readPackageName(canonicalRoot, observeInput);
1154
1215
  return createTrustedResolvedCandidateFacts({
1155
- schemaVersion: '1.1',
1216
+ schemaVersion: '1.2',
1156
1217
  completeness: completenessReasons.length === 0 ? 'complete' : 'partial',
1157
1218
  completenessReasons,
1158
1219
  resolverIdentity: RESOLVED_FACTS_RESOLVER_IDENTITY,
@@ -1169,5 +1230,9 @@ export function resolveCandidateFacts({
1169
1230
  intentReferences,
1170
1231
  safetyUses,
1171
1232
  classShapes,
1233
+ arkRunKernelCalls,
1234
+ arkRunManagedNews,
1235
+ arkRunCompositionRootHits,
1236
+ arkRunDeclarations,
1172
1237
  });
1173
1238
  }