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.
- package/CHANGELOG.md +109 -0
- package/README.md +20 -9
- package/SECURITY.md +1 -1
- package/bin/ark-check-runtime.mjs +13 -1
- package/bin/ark-mcp-runtime.mjs +65 -3
- package/bin/lib/adapter-contract.mjs +17 -36
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/ark-run-doctor.mjs +144 -0
- package/bin/lib/ark-run-facts.mjs +472 -0
- package/bin/lib/ark-run-report.mjs +57 -0
- package/bin/lib/ark-run-sensors.mjs +309 -0
- package/bin/lib/config-contract.mjs +86 -11
- package/bin/lib/diagnostic-catalog.mjs +8 -0
- package/bin/lib/doctor-advisories.mjs +45 -8
- package/bin/lib/doctor-human.mjs +10 -0
- package/bin/lib/doctor-plan.mjs +20 -16
- package/bin/lib/extra-merge-teeth.mjs +187 -0
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +22 -2
- package/bin/lib/html-report.mjs +16 -0
- package/bin/lib/remediation.mjs +132 -0
- package/bin/lib/resolved-candidate-facts.mjs +67 -2
- package/bin/lib/rules-under-contract.mjs +37 -89
- package/bin/lib/snippet-analysis.mjs +43 -2
- package/bin/lib/status-command.mjs +28 -0
- package/bin/lib/status-manifest.mjs +23 -0
- package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
- package/dist/eslint/index.cjs +6 -2
- package/dist/eslint/index.d.ts +70 -2
- package/dist/eslint/index.js +6 -2
- package/dist/index.cjs +35 -35
- package/dist/index.d.ts +787 -272
- package/dist/index.js +35 -35
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +21 -15
- package/docs/ai-gates.md +13 -0
- package/docs/configuration.md +24 -11
- package/docs/develop.md +12 -3
- package/docs/diagnostics.md +75 -0
- package/docs/enthusiast/README.md +4 -3
- package/docs/package-surface.md +16 -13
- package/docs/product-voice.md +6 -3
- package/docs/threat-model.md +1 -1
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +41 -2
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +47 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
- package/templates/agent-skills/ark-place/SKILL.md +26 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
- package/templates/skills/ark-adopt.md +23 -2
- package/templates/skills/ark-place.md +26 -2
- 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
|
|
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
|
},
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -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
|
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -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.
|
|
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
|
}
|