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.
- package/CHANGELOG.md +139 -2
- package/README.md +22 -10
- package/SECURITY.md +1 -1
- package/bin/ark-check-runtime.mjs +21 -341
- package/bin/ark-mcp-runtime.mjs +71 -325
- package/bin/ark-shared.mjs +24 -158
- 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/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/config-contract.mjs +86 -11
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/diagnostic-catalog.mjs +8 -0
- package/bin/lib/doctor-advisories.mjs +45 -8
- package/bin/lib/doctor-human.mjs +519 -0
- package/bin/lib/doctor-plan.mjs +62 -445
- package/bin/lib/extra-merge-teeth.mjs +187 -0
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +22 -2
- package/bin/lib/html-report.mjs +40 -7
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +4 -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/bin/lib/team-parliament-io.mjs +4 -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 +4 -3
- 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 +17 -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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/arkRunDoctor.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/ark-run-doctor.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkRunRuleId, } from './extra-merge-teeth.mjs';
|
|
12
|
+
export const ARK_RUN_DOCTOR_SCHEMA_VERSION = '1.0';
|
|
13
|
+
const RESIDUAL_RULE_CAP = 12;
|
|
14
|
+
function closedMode(value) {
|
|
15
|
+
return value === 'enforced' || value === 'advisory' ? value : null;
|
|
16
|
+
}
|
|
17
|
+
function uniqueArkRunRuleIds(findings) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
if (!Array.isArray(findings))
|
|
20
|
+
return [];
|
|
21
|
+
for (const finding of findings) {
|
|
22
|
+
const id = finding?.ruleId;
|
|
23
|
+
if (typeof id !== 'string' || !isArkRunRuleId(id) || seen.has(id))
|
|
24
|
+
continue;
|
|
25
|
+
seen.add(id);
|
|
26
|
+
}
|
|
27
|
+
return [...seen].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
28
|
+
}
|
|
29
|
+
function extraFromConfig(arkRun) {
|
|
30
|
+
if (!arkRun || typeof arkRun !== 'object') {
|
|
31
|
+
return { present: false, mode: null, roots: 0, layers: 0, requireDeclarations: null };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
present: true,
|
|
35
|
+
mode: closedMode(arkRun.mode),
|
|
36
|
+
roots: Array.isArray(arkRun.compositionRoots) ? arkRun.compositionRoots.length : 0,
|
|
37
|
+
layers: Array.isArray(arkRun.managedLayers) ? arkRun.managedLayers.length : 0,
|
|
38
|
+
requireDeclarations: arkRun.requireDeclarations === true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Doctor / HTML ArkRun advisory. Always emitted; absence is an honest silent row.
|
|
43
|
+
*/
|
|
44
|
+
export function summarizeArkRunSection(input = {}) {
|
|
45
|
+
const extra = extraFromConfig(input.arkRun);
|
|
46
|
+
const uniqueIds = extra.present ? uniqueArkRunRuleIds(input.findings) : [];
|
|
47
|
+
const ruleIds = uniqueIds.slice(0, RESIDUAL_RULE_CAP);
|
|
48
|
+
const residualCount = uniqueIds.length;
|
|
49
|
+
const mergePlanes = composeMergePlanesHonesty({
|
|
50
|
+
classification: input.classification,
|
|
51
|
+
arkRules: {
|
|
52
|
+
active: input.arkRules?.active === true,
|
|
53
|
+
structureEnforced: input.arkRules?.structureEnforced,
|
|
54
|
+
structureTotal: input.arkRules?.structureTotal,
|
|
55
|
+
structureAdvisory: input.arkRules?.structureAdvisory,
|
|
56
|
+
invariantEnforced: input.arkRules?.invariantEnforced,
|
|
57
|
+
invariantTotal: input.arkRules?.invariantTotal,
|
|
58
|
+
invariantAdvisory: input.arkRules?.invariantAdvisory,
|
|
59
|
+
covered: input.arkRules?.covered,
|
|
60
|
+
uncovered: input.arkRules?.uncovered,
|
|
61
|
+
},
|
|
62
|
+
arkRun: {
|
|
63
|
+
present: extra.present,
|
|
64
|
+
mode: extra.mode,
|
|
65
|
+
residualCount,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const extraMergeTeeth = extra.present && extra.mode === 'enforced' && extraMergeTeethAllowed(input.classification);
|
|
69
|
+
let note;
|
|
70
|
+
if (!extra.present) {
|
|
71
|
+
note =
|
|
72
|
+
'Absence of arkRun is silent — Layers and ArkRules verdicts unchanged. Not a score.';
|
|
73
|
+
}
|
|
74
|
+
else if (extra.mode === 'advisory') {
|
|
75
|
+
note =
|
|
76
|
+
'Advisory ArkRun residual only — never flips valid or --strict-merge. Residual is a finding-id count, never a score.';
|
|
77
|
+
}
|
|
78
|
+
else if (extraMergeTeeth) {
|
|
79
|
+
note =
|
|
80
|
+
'Enforced ArkRun is on the extra merge plane. Residual is a finding-id count, never a score.';
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
note =
|
|
84
|
+
'Enforced ArkRun extra teeth stay demoted until the layer plane is honestly classified. Residual is a finding-id count, never a score.';
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: ARK_RUN_DOCTOR_SCHEMA_VERSION,
|
|
88
|
+
notAScore: true,
|
|
89
|
+
active: extra.present,
|
|
90
|
+
mode: extra.mode,
|
|
91
|
+
compositionRoots: extra.roots,
|
|
92
|
+
managedLayers: extra.layers,
|
|
93
|
+
requireDeclarations: extra.requireDeclarations,
|
|
94
|
+
residual: { count: residualCount, ruleIds },
|
|
95
|
+
extraMergeTeeth,
|
|
96
|
+
failMergeWhen: mergePlanes.failMergeWhen,
|
|
97
|
+
note,
|
|
98
|
+
mergePlanes,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Thin status slice — counts only; residual null means unknown, not green. */
|
|
102
|
+
export function projectStatusArkRun(input = {}) {
|
|
103
|
+
const present = input.present === true;
|
|
104
|
+
const mode = closedMode(input.mode);
|
|
105
|
+
const residualRaw = input.residual;
|
|
106
|
+
let residual = null;
|
|
107
|
+
if (typeof residualRaw === 'number' && Number.isFinite(residualRaw) && residualRaw >= 0) {
|
|
108
|
+
residual = Math.floor(residualRaw);
|
|
109
|
+
}
|
|
110
|
+
if (!present)
|
|
111
|
+
residual = residual == null ? 0 : residual;
|
|
112
|
+
return {
|
|
113
|
+
notAScore: true,
|
|
114
|
+
present,
|
|
115
|
+
mode: present ? mode : null,
|
|
116
|
+
extraMergeTeeth: present && mode === 'enforced' && input.extraMergeTeeth === true,
|
|
117
|
+
residual,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function formatArkRunDoctorLines(section) {
|
|
121
|
+
if (!section || section.notAScore !== true)
|
|
122
|
+
return [];
|
|
123
|
+
if (section.active !== true) {
|
|
124
|
+
return ['ArkRun extra is off — silent on Layers/ArkRules (not a score).'];
|
|
125
|
+
}
|
|
126
|
+
const mode = section.mode ?? 'unknown';
|
|
127
|
+
const teeth = section.extraMergeTeeth === true ? 'armed' : 'not armed';
|
|
128
|
+
const lines = [
|
|
129
|
+
`mode: ${mode} · extra merge teeth ${teeth} · not a score`,
|
|
130
|
+
];
|
|
131
|
+
if (section.residual.count > 0) {
|
|
132
|
+
const shown = section.residual.ruleIds.join(', ');
|
|
133
|
+
const more = section.residual.count > section.residual.ruleIds.length
|
|
134
|
+
? ` (+${section.residual.count - section.residual.ruleIds.length} more)`
|
|
135
|
+
: '';
|
|
136
|
+
lines.push(`Residual: ${shown}${more}`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push('Residual: none on this scan (not a score — green extras ≠ finished kernel wiring).');
|
|
140
|
+
}
|
|
141
|
+
if (section.failMergeWhen)
|
|
142
|
+
lines.push(section.failMergeWhen);
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/arkRunFacts.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/ark-run-facts.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ARKRUN_KERNEL_FACTORY_CALLEES = [
|
|
12
|
+
'createArkKernel',
|
|
13
|
+
'createStrictArkKernel',
|
|
14
|
+
'createArkKernelFromConfig',
|
|
15
|
+
'createStrictArkKernelFromConfig',
|
|
16
|
+
];
|
|
17
|
+
/** Closed interaction callees from ADR 0022 undeclared-emit/handle/depend. */
|
|
18
|
+
export const ARKRUN_KERNEL_INTERACTION_CALLEES = [
|
|
19
|
+
'publisher',
|
|
20
|
+
'publish',
|
|
21
|
+
'raise',
|
|
22
|
+
'raiseAsync',
|
|
23
|
+
'send',
|
|
24
|
+
'sendTo',
|
|
25
|
+
'subscribe',
|
|
26
|
+
'registerHandler',
|
|
27
|
+
'resolve',
|
|
28
|
+
'resolveSingleton',
|
|
29
|
+
];
|
|
30
|
+
const FACTORY_CALLEES = new Set(ARKRUN_KERNEL_FACTORY_CALLEES);
|
|
31
|
+
const BUILTIN_CTORS = new Set([
|
|
32
|
+
'AggregateError',
|
|
33
|
+
'Array',
|
|
34
|
+
'ArrayBuffer',
|
|
35
|
+
'BigInt64Array',
|
|
36
|
+
'BigUint64Array',
|
|
37
|
+
'Boolean',
|
|
38
|
+
'DataView',
|
|
39
|
+
'Date',
|
|
40
|
+
'Error',
|
|
41
|
+
'EvalError',
|
|
42
|
+
'FinalizationRegistry',
|
|
43
|
+
'Float32Array',
|
|
44
|
+
'Float64Array',
|
|
45
|
+
'Function',
|
|
46
|
+
'Int8Array',
|
|
47
|
+
'Int16Array',
|
|
48
|
+
'Int32Array',
|
|
49
|
+
'Map',
|
|
50
|
+
'Number',
|
|
51
|
+
'Object',
|
|
52
|
+
'Promise',
|
|
53
|
+
'Proxy',
|
|
54
|
+
'RangeError',
|
|
55
|
+
'ReferenceError',
|
|
56
|
+
'RegExp',
|
|
57
|
+
'Set',
|
|
58
|
+
'SharedArrayBuffer',
|
|
59
|
+
'String',
|
|
60
|
+
'Symbol',
|
|
61
|
+
'SyntaxError',
|
|
62
|
+
'TypeError',
|
|
63
|
+
'URIError',
|
|
64
|
+
'Uint8Array',
|
|
65
|
+
'Uint8ClampedArray',
|
|
66
|
+
'Uint16Array',
|
|
67
|
+
'Uint32Array',
|
|
68
|
+
'WeakMap',
|
|
69
|
+
'WeakRef',
|
|
70
|
+
'WeakSet',
|
|
71
|
+
]);
|
|
72
|
+
/** Receivers whose `.resolve`/`.publish` are ambient, not kernel APIs. */
|
|
73
|
+
const SKIP_INTERACTION_RECEIVERS = new Set([
|
|
74
|
+
'Array',
|
|
75
|
+
'Atomics',
|
|
76
|
+
'Buffer',
|
|
77
|
+
'JSON',
|
|
78
|
+
'Math',
|
|
79
|
+
'Number',
|
|
80
|
+
'Object',
|
|
81
|
+
'Promise',
|
|
82
|
+
'Reflect',
|
|
83
|
+
'String',
|
|
84
|
+
'console',
|
|
85
|
+
'fs',
|
|
86
|
+
'path',
|
|
87
|
+
'url',
|
|
88
|
+
'util',
|
|
89
|
+
]);
|
|
90
|
+
export function isArkRunKernelModuleSpecifier(specifier) {
|
|
91
|
+
return (specifier === '@arkgate/runtime' ||
|
|
92
|
+
specifier.startsWith('@arkgate/runtime/') ||
|
|
93
|
+
specifier === 'arkgate/runtime' ||
|
|
94
|
+
specifier.startsWith('arkgate/runtime/'));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Closed broker / queue / emitter specifiers for `arkrun-transport-bypass`
|
|
98
|
+
* (ADR 0022 D4). Exact entry or package-root subpath only — never substring.
|
|
99
|
+
*/
|
|
100
|
+
export const ARKRUN_TRANSPORT_BYPASS_SPECIFIERS = [
|
|
101
|
+
'events',
|
|
102
|
+
'node:events',
|
|
103
|
+
'eventemitter2',
|
|
104
|
+
'eventemitter3',
|
|
105
|
+
'emittery',
|
|
106
|
+
'kafkajs',
|
|
107
|
+
'kafka-node',
|
|
108
|
+
'amqplib',
|
|
109
|
+
'amqp',
|
|
110
|
+
'bull',
|
|
111
|
+
'bullmq',
|
|
112
|
+
'mqtt',
|
|
113
|
+
'nats',
|
|
114
|
+
'@aws-sdk/client-sqs',
|
|
115
|
+
'@aws-sdk/client-sns',
|
|
116
|
+
'@aws-sdk/client-eventbridge',
|
|
117
|
+
'@google-cloud/pubsub',
|
|
118
|
+
'@azure/service-bus',
|
|
119
|
+
];
|
|
120
|
+
const TRANSPORT_BYPASS = new Set(ARKRUN_TRANSPORT_BYPASS_SPECIFIERS);
|
|
121
|
+
export function isArkRunTransportBypassSpecifier(specifier) {
|
|
122
|
+
if (!specifier || specifier.startsWith('.') || specifier.startsWith('/'))
|
|
123
|
+
return false;
|
|
124
|
+
if (TRANSPORT_BYPASS.has(specifier))
|
|
125
|
+
return true;
|
|
126
|
+
const first = specifier.indexOf('/');
|
|
127
|
+
if (first < 0)
|
|
128
|
+
return false;
|
|
129
|
+
const root = specifier.slice(0, first);
|
|
130
|
+
if (TRANSPORT_BYPASS.has(root))
|
|
131
|
+
return true;
|
|
132
|
+
const second = specifier.indexOf('/', first + 1);
|
|
133
|
+
if (second < 0)
|
|
134
|
+
return false;
|
|
135
|
+
return TRANSPORT_BYPASS.has(specifier.slice(0, second));
|
|
136
|
+
}
|
|
137
|
+
export function arkRunKernelCallKind(callee) {
|
|
138
|
+
if (FACTORY_CALLEES.has(callee))
|
|
139
|
+
return 'factory';
|
|
140
|
+
switch (callee) {
|
|
141
|
+
case 'publisher':
|
|
142
|
+
return 'publisher';
|
|
143
|
+
case 'publish':
|
|
144
|
+
return 'publish';
|
|
145
|
+
case 'raise':
|
|
146
|
+
case 'raiseAsync':
|
|
147
|
+
return 'raise';
|
|
148
|
+
case 'send':
|
|
149
|
+
case 'sendTo':
|
|
150
|
+
return 'send';
|
|
151
|
+
case 'subscribe':
|
|
152
|
+
return 'subscribe';
|
|
153
|
+
case 'registerHandler':
|
|
154
|
+
return 'register-handler';
|
|
155
|
+
case 'resolve':
|
|
156
|
+
return 'resolve';
|
|
157
|
+
case 'resolveSingleton':
|
|
158
|
+
return 'resolve-singleton';
|
|
159
|
+
default:
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function lineAt(content, index) {
|
|
164
|
+
let line = 1;
|
|
165
|
+
for (let i = 0; i < index; i += 1) {
|
|
166
|
+
if (content.charCodeAt(i) === 10)
|
|
167
|
+
line += 1;
|
|
168
|
+
}
|
|
169
|
+
return line;
|
|
170
|
+
}
|
|
171
|
+
/** Replace comments with spaces so line numbers stay aligned. */
|
|
172
|
+
function stripCommentsPreservingLines(content) {
|
|
173
|
+
return content
|
|
174
|
+
.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
|
|
175
|
+
.replace(/(^|[^:\\])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (tail) => ' '.repeat(tail.length)));
|
|
176
|
+
}
|
|
177
|
+
function firstStringLiteralArg(content, openParenEnd) {
|
|
178
|
+
const slice = content.slice(openParenEnd);
|
|
179
|
+
const match = /^\s*(['"])((?:\\.|[^\\])*?)\1/.exec(slice);
|
|
180
|
+
if (!match)
|
|
181
|
+
return undefined;
|
|
182
|
+
const value = match[2] ?? '';
|
|
183
|
+
return value.length > 0 ? value : undefined;
|
|
184
|
+
}
|
|
185
|
+
function keywordBefore(content, index, keyword) {
|
|
186
|
+
const start = Math.max(0, index - keyword.length - 8);
|
|
187
|
+
const before = content.slice(start, index);
|
|
188
|
+
return new RegExp(`\\b${keyword}\\s+$`).test(before);
|
|
189
|
+
}
|
|
190
|
+
function parseValueImportClause(content, onClause) {
|
|
191
|
+
const importRe = /\b(?:import|export)(\s+type)?\s+([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = importRe.exec(content)) !== null) {
|
|
194
|
+
if (match[1])
|
|
195
|
+
continue;
|
|
196
|
+
onClause(match[2] ?? '', match[3] ?? '');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Value `import`/`export … from` clauses. Strips comments so callers may pass raw source. */
|
|
200
|
+
export function forEachArkRunValueImportClause(content, onClause) {
|
|
201
|
+
parseValueImportClause(stripCommentsPreservingLines(content), onClause);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Lexical import/export-from and require/import() specifier edges for the
|
|
205
|
+
* editor / snippet envelope. Resolution stays unresolved-external — sensors
|
|
206
|
+
* only need the specifier and from-file.
|
|
207
|
+
*/
|
|
208
|
+
export function extractArkRunValueImportDependenciesFromSource(file, content) {
|
|
209
|
+
const source = stripCommentsPreservingLines(content);
|
|
210
|
+
const out = [];
|
|
211
|
+
const fromRe = /\b(?:import|export)(\s+type)?\s+([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g;
|
|
212
|
+
let match;
|
|
213
|
+
while ((match = fromRe.exec(source)) !== null) {
|
|
214
|
+
const specifier = match[3] ?? '';
|
|
215
|
+
if (!specifier)
|
|
216
|
+
continue;
|
|
217
|
+
const statement = match[0] ?? '';
|
|
218
|
+
out.push({
|
|
219
|
+
from: file,
|
|
220
|
+
specifier,
|
|
221
|
+
kind: /^\s*export/.test(statement) ? 'export' : 'import',
|
|
222
|
+
typeOnly: Boolean(match[1]),
|
|
223
|
+
line: lineAt(content, match.index),
|
|
224
|
+
resolution: 'resolved-external',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const callRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
228
|
+
while ((match = callRe.exec(source)) !== null) {
|
|
229
|
+
const specifier = match[1] ?? '';
|
|
230
|
+
if (!specifier)
|
|
231
|
+
continue;
|
|
232
|
+
const kind = match[0]?.startsWith('import') ? 'dynamic-import' : 'require';
|
|
233
|
+
out.push({
|
|
234
|
+
from: file,
|
|
235
|
+
specifier,
|
|
236
|
+
kind,
|
|
237
|
+
typeOnly: false,
|
|
238
|
+
line: lineAt(content, match.index),
|
|
239
|
+
resolution: 'resolved-external',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
/** PascalCase named bindings from value import clauses (snippet admitted constructors). */
|
|
245
|
+
export function extractArkRunImportedConstructorNamesFromSource(content) {
|
|
246
|
+
const names = [];
|
|
247
|
+
forEachArkRunValueImportClause(content, (clause) => {
|
|
248
|
+
const braced = /\{([^}]*)\}/.exec(clause);
|
|
249
|
+
if (!braced?.[1])
|
|
250
|
+
return;
|
|
251
|
+
for (const part of braced[1].split(',')) {
|
|
252
|
+
const piece = part.trim();
|
|
253
|
+
if (!piece || piece.startsWith('type '))
|
|
254
|
+
continue;
|
|
255
|
+
const alias = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece);
|
|
256
|
+
const local = alias?.[2] ?? /^([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece)?.[1];
|
|
257
|
+
const original = alias?.[1] ?? local;
|
|
258
|
+
if (local && original && /^[A-Z]/.test(original))
|
|
259
|
+
names.push(local, original);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return uniqueSorted(names);
|
|
263
|
+
}
|
|
264
|
+
function collectKernelImportBindings(content) {
|
|
265
|
+
const named = new Map();
|
|
266
|
+
const namespaces = new Set();
|
|
267
|
+
parseValueImportClause(content, (clause, specifier) => {
|
|
268
|
+
if (!isArkRunKernelModuleSpecifier(specifier))
|
|
269
|
+
return;
|
|
270
|
+
const namespace = /\*\s+as\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(clause);
|
|
271
|
+
if (namespace?.[1])
|
|
272
|
+
namespaces.add(namespace[1]);
|
|
273
|
+
const defaultIdent = /^([A-Za-z_][A-Za-z0-9_]*)\s*(?:,|$)/.exec(clause.trim());
|
|
274
|
+
if (defaultIdent?.[1])
|
|
275
|
+
named.set(defaultIdent[1], defaultIdent[1]);
|
|
276
|
+
const braced = /\{([^}]*)\}/.exec(clause);
|
|
277
|
+
if (!braced?.[1])
|
|
278
|
+
return;
|
|
279
|
+
for (const part of braced[1].split(',')) {
|
|
280
|
+
const piece = part.trim();
|
|
281
|
+
if (!piece || piece.startsWith('type '))
|
|
282
|
+
continue;
|
|
283
|
+
const alias = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece);
|
|
284
|
+
if (alias) {
|
|
285
|
+
named.set(alias[2], alias[1]);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const ident = /^([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece);
|
|
289
|
+
if (ident?.[1])
|
|
290
|
+
named.set(ident[1], ident[1]);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return { named, namespaces };
|
|
294
|
+
}
|
|
295
|
+
function collectImportedConstructors(content, admitted) {
|
|
296
|
+
const out = new Set(admitted);
|
|
297
|
+
parseValueImportClause(content, (clause, specifier) => {
|
|
298
|
+
const braced = /\{([^}]*)\}/.exec(clause);
|
|
299
|
+
if (!braced?.[1])
|
|
300
|
+
return;
|
|
301
|
+
for (const part of braced[1].split(',')) {
|
|
302
|
+
const piece = part.trim();
|
|
303
|
+
if (!piece || piece.startsWith('type '))
|
|
304
|
+
continue;
|
|
305
|
+
const alias = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece);
|
|
306
|
+
const local = alias?.[2] ?? /^([A-Za-z_][A-Za-z0-9_]*)$/.exec(piece)?.[1];
|
|
307
|
+
const original = alias?.[1] ?? local;
|
|
308
|
+
if (!local || !original || !/^[A-Z]/.test(original))
|
|
309
|
+
continue;
|
|
310
|
+
if (isArkRunKernelModuleSpecifier(specifier) || admitted.has(original) || admitted.has(local)) {
|
|
311
|
+
out.add(local);
|
|
312
|
+
out.add(original);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
function importedFromForName(content, localName) {
|
|
319
|
+
let found;
|
|
320
|
+
parseValueImportClause(content, (clause, specifier) => {
|
|
321
|
+
if (!found && new RegExp(`\\b${localName}\\b`).test(clause))
|
|
322
|
+
found = specifier;
|
|
323
|
+
});
|
|
324
|
+
return found;
|
|
325
|
+
}
|
|
326
|
+
export function extractArkRunKernelCallsFromSource(file, content) {
|
|
327
|
+
const source = stripCommentsPreservingLines(content);
|
|
328
|
+
const bindings = collectKernelImportBindings(source);
|
|
329
|
+
const facts = [];
|
|
330
|
+
const callRe = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]*>)?\s*\(/g;
|
|
331
|
+
let match;
|
|
332
|
+
while ((match = callRe.exec(source)) !== null) {
|
|
333
|
+
const callee = match[1];
|
|
334
|
+
const index = match.index;
|
|
335
|
+
if (keywordBefore(source, index, 'function') || keywordBefore(source, index, 'class'))
|
|
336
|
+
continue;
|
|
337
|
+
const dotted = source.slice(0, index).match(/([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*$/);
|
|
338
|
+
const receiver = dotted?.[1];
|
|
339
|
+
const original = bindings.named.get(callee) ?? callee;
|
|
340
|
+
const kind = arkRunKernelCallKind(original) ?? arkRunKernelCallKind(callee);
|
|
341
|
+
if (!kind)
|
|
342
|
+
continue;
|
|
343
|
+
const viaImport = bindings.named.has(callee) || (receiver !== undefined && bindings.namespaces.has(receiver));
|
|
344
|
+
if (kind !== 'factory') {
|
|
345
|
+
if (!viaImport && receiver === undefined)
|
|
346
|
+
continue;
|
|
347
|
+
if (receiver && SKIP_INTERACTION_RECEIVERS.has(receiver) && !viaImport)
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const nameLiteral = firstStringLiteralArg(source, index + match[0].length);
|
|
351
|
+
facts.push({
|
|
352
|
+
file,
|
|
353
|
+
line: lineAt(content, index),
|
|
354
|
+
kind,
|
|
355
|
+
callee,
|
|
356
|
+
viaImport,
|
|
357
|
+
...(receiver ? { receiver } : {}),
|
|
358
|
+
...(nameLiteral ? { nameLiteral } : {}),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return facts;
|
|
362
|
+
}
|
|
363
|
+
export function extractArkRunManagedNewsFromSource(file, content, admittedTypeNames) {
|
|
364
|
+
const source = stripCommentsPreservingLines(content);
|
|
365
|
+
const admitted = collectImportedConstructors(source, admittedTypeNames);
|
|
366
|
+
const facts = [];
|
|
367
|
+
const newRe = /\bnew\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*([A-Z][A-Za-z0-9_]*)\s*(?:<[^>]*>)?\s*\(/g;
|
|
368
|
+
let match;
|
|
369
|
+
while ((match = newRe.exec(source)) !== null) {
|
|
370
|
+
const typeName = match[1];
|
|
371
|
+
if (BUILTIN_CTORS.has(typeName) || !admitted.has(typeName))
|
|
372
|
+
continue;
|
|
373
|
+
const importedFrom = importedFromForName(source, typeName);
|
|
374
|
+
facts.push({
|
|
375
|
+
file,
|
|
376
|
+
line: lineAt(content, match.index),
|
|
377
|
+
typeName,
|
|
378
|
+
...(importedFrom ? { importedFrom } : {}),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return facts;
|
|
382
|
+
}
|
|
383
|
+
function matchingBracketEnd(source, openIndex) {
|
|
384
|
+
let depth = 0;
|
|
385
|
+
let quote;
|
|
386
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
387
|
+
const ch = source[i];
|
|
388
|
+
if (quote) {
|
|
389
|
+
if (ch === '\\') {
|
|
390
|
+
i += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (ch === quote)
|
|
394
|
+
quote = undefined;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
398
|
+
quote = ch;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ch === '[')
|
|
402
|
+
depth += 1;
|
|
403
|
+
else if (ch === ']') {
|
|
404
|
+
depth -= 1;
|
|
405
|
+
if (depth === 0)
|
|
406
|
+
return i;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return -1;
|
|
410
|
+
}
|
|
411
|
+
function stringLiteralsInList(source, openIndex, closeIndex) {
|
|
412
|
+
const slice = source.slice(openIndex + 1, closeIndex);
|
|
413
|
+
const out = [];
|
|
414
|
+
const re = /(['"])((?:\\.|[^\\])*?)\1/g;
|
|
415
|
+
let match;
|
|
416
|
+
while ((match = re.exec(slice)) !== null) {
|
|
417
|
+
const value = match[2] ?? '';
|
|
418
|
+
if (value.length > 0)
|
|
419
|
+
out.push(value);
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
function uniqueSorted(values) {
|
|
424
|
+
return [...new Set(values)].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
425
|
+
}
|
|
426
|
+
/** File-scoped `uses` / `reactsTo` / `raises` / `sends` string-literal lists (ADR 0023). */
|
|
427
|
+
export function extractArkRunDeclarationsFromSource(file, content) {
|
|
428
|
+
const source = stripCommentsPreservingLines(content);
|
|
429
|
+
const fieldRe = /\b(uses|reactsTo|raises|sends)\s*:/g;
|
|
430
|
+
const uses = [];
|
|
431
|
+
const reactsTo = [];
|
|
432
|
+
const raises = [];
|
|
433
|
+
const sends = [];
|
|
434
|
+
let firstIndex;
|
|
435
|
+
let match;
|
|
436
|
+
while ((match = fieldRe.exec(source)) !== null) {
|
|
437
|
+
const after = source.slice(match.index + match[0].length);
|
|
438
|
+
const bracket = /^\s*\[/.exec(after);
|
|
439
|
+
if (!bracket)
|
|
440
|
+
continue;
|
|
441
|
+
const openIndex = match.index + match[0].length + (bracket[0].length - 1);
|
|
442
|
+
const closeIndex = matchingBracketEnd(source, openIndex);
|
|
443
|
+
if (closeIndex < 0)
|
|
444
|
+
continue;
|
|
445
|
+
const names = stringLiteralsInList(source, openIndex, closeIndex);
|
|
446
|
+
if (names.length === 0)
|
|
447
|
+
continue;
|
|
448
|
+
if (firstIndex === undefined)
|
|
449
|
+
firstIndex = match.index;
|
|
450
|
+
const field = match[1];
|
|
451
|
+
if (field === 'uses')
|
|
452
|
+
uses.push(...names);
|
|
453
|
+
else if (field === 'reactsTo')
|
|
454
|
+
reactsTo.push(...names);
|
|
455
|
+
else if (field === 'raises')
|
|
456
|
+
raises.push(...names);
|
|
457
|
+
else
|
|
458
|
+
sends.push(...names);
|
|
459
|
+
}
|
|
460
|
+
if (firstIndex === undefined)
|
|
461
|
+
return [];
|
|
462
|
+
return [
|
|
463
|
+
{
|
|
464
|
+
file,
|
|
465
|
+
line: lineAt(content, firstIndex),
|
|
466
|
+
uses: uniqueSorted(uses),
|
|
467
|
+
reactsTo: uniqueSorted(reactsTo),
|
|
468
|
+
raises: uniqueSorted(raises),
|
|
469
|
+
sends: uniqueSorted(sends),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML for the doctor ArkRun advisory (report parity: data-advisory="arkRun").
|
|
3
|
+
*/
|
|
4
|
+
export function formatArkRunHtml(section, esc) {
|
|
5
|
+
if (!section || typeof section !== 'object' || section.notAScore !== true) return '';
|
|
6
|
+
const escape = typeof esc === 'function' ? esc : (v) => String(v);
|
|
7
|
+
const note = section.note ? `<p class="muted">${escape(section.note)}</p>` : '';
|
|
8
|
+
if (section.active !== true) {
|
|
9
|
+
return `
|
|
10
|
+
<section class="section card" data-advisory="arkRun">
|
|
11
|
+
<h2>ArkRun <span class="muted">(opt-in extra — not a score)</span></h2>
|
|
12
|
+
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
13
|
+
Kernel usage + complete declarations. Absence is silent — Layers and ArkRules verdicts stay the same.
|
|
14
|
+
</p>
|
|
15
|
+
${note}
|
|
16
|
+
</section>`;
|
|
17
|
+
}
|
|
18
|
+
const residual = section.residual && typeof section.residual === 'object' ? section.residual : { count: 0, ruleIds: [] };
|
|
19
|
+
const ids = Array.isArray(residual.ruleIds) ? residual.ruleIds : [];
|
|
20
|
+
const residualHtml =
|
|
21
|
+
residual.count > 0
|
|
22
|
+
? `<p><span class="tag warn">residual</span> ${ids
|
|
23
|
+
.map((id) => `<code>${escape(id)}</code>`)
|
|
24
|
+
.join(' · ')}${
|
|
25
|
+
residual.count > ids.length ? ` <span class="muted">(+${residual.count - ids.length} more)</span>` : ''
|
|
26
|
+
}</p>`
|
|
27
|
+
: '<p class="muted">Residual: none on this scan (not a score — green extras ≠ finished kernel wiring).</p>';
|
|
28
|
+
const teeth = section.extraMergeTeeth === true
|
|
29
|
+
? '<span class="tag">extra merge teeth armed</span>'
|
|
30
|
+
: '<span class="tag warn">extra merge teeth not armed</span>';
|
|
31
|
+
const mergeSentence =
|
|
32
|
+
section.mergePlanes && typeof section.mergePlanes.failMergeWhen === 'string'
|
|
33
|
+
? section.mergePlanes.failMergeWhen
|
|
34
|
+
: section.failMergeWhen;
|
|
35
|
+
const merge =
|
|
36
|
+
mergeSentence
|
|
37
|
+
? `<p class="muted" style="margin:.35rem 0 .55rem;font-size:.86rem"><b>Merge planes:</b> ${escape(mergeSentence)}</p>`
|
|
38
|
+
: '';
|
|
39
|
+
return `
|
|
40
|
+
<section class="section card" data-advisory="arkRun">
|
|
41
|
+
<h2>ArkRun <span class="muted">(not a score)</span></h2>
|
|
42
|
+
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
43
|
+
<b>[ArkRun]</b> Kernel usage + declarations — separate from <b>[Layer]</b> imports and <b>[ArkRules]</b> shape.
|
|
44
|
+
Advisory never flips <code>valid</code>. Enforced extra teeth only when the layer plane is classified.
|
|
45
|
+
</p>
|
|
46
|
+
${merge}
|
|
47
|
+
<div class="kpis" style="margin-bottom:.55rem">
|
|
48
|
+
<div class="kpi"><b>${escape(section.mode || '—')}</b><span>Mode</span></div>
|
|
49
|
+
<div class="kpi"><b>${Number(residual.count) || 0}</b><span>Residual ids</span></div>
|
|
50
|
+
<div class="kpi"><b>${Number(section.compositionRoots) || 0}</b><span>Composition roots</span></div>
|
|
51
|
+
<div class="kpi"><b>${Number(section.managedLayers) || 0}</b><span>Managed layers</span></div>
|
|
52
|
+
</div>
|
|
53
|
+
<p>${teeth} · <code>notAScore</code></p>
|
|
54
|
+
${residualHtml}
|
|
55
|
+
${note}
|
|
56
|
+
</section>`;
|
|
57
|
+
}
|