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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/arkRunSensors.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-sensors.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { extractArkRunImportedConstructorNamesFromSource, extractArkRunKernelCallsFromSource, extractArkRunManagedNewsFromSource, extractArkRunValueImportDependenciesFromSource, isArkRunKernelModuleSpecifier, isArkRunTransportBypassSpecifier, } from './ark-run-facts.mjs';
|
|
12
|
+
import { extraMergeTeethAllowed, } from './extra-merge-teeth.mjs';
|
|
13
|
+
import { deterministicNextAction } from './remediation.mjs';
|
|
14
|
+
export const ARKRUN_TIER1_SENSOR_IDS = [
|
|
15
|
+
'arkrun-missing-root',
|
|
16
|
+
'arkrun-kernel-in-domain',
|
|
17
|
+
'arkrun-direct-new',
|
|
18
|
+
'arkrun-undeclared-emit',
|
|
19
|
+
'arkrun-undeclared-handle',
|
|
20
|
+
'arkrun-undeclared-depend',
|
|
21
|
+
'arkrun-transport-bypass',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Import / `new` envelope for `arkgate/eslint`.
|
|
25
|
+
* Missing-root and undeclared-* stay CLI/MCP/preflight (project-wide or declaration facts).
|
|
26
|
+
*/
|
|
27
|
+
export const ARKRUN_EDITOR_SENSOR_IDS = [
|
|
28
|
+
'arkrun-kernel-in-domain',
|
|
29
|
+
'arkrun-direct-new',
|
|
30
|
+
'arkrun-transport-bypass',
|
|
31
|
+
];
|
|
32
|
+
const EDITOR_SENSOR_SET = new Set(ARKRUN_EDITOR_SENSOR_IDS);
|
|
33
|
+
export function isArkRunEditorSensor(sensor) {
|
|
34
|
+
return EDITOR_SENSOR_SET.has(sensor);
|
|
35
|
+
}
|
|
36
|
+
export const ARKRUN_RULE_IDS = {
|
|
37
|
+
'arkrun-missing-root': 'ARKRUN_MISSING_ROOT',
|
|
38
|
+
'arkrun-kernel-in-domain': 'ARKRUN_KERNEL_IN_DOMAIN',
|
|
39
|
+
'arkrun-direct-new': 'ARKRUN_DIRECT_NEW',
|
|
40
|
+
'arkrun-undeclared-emit': 'ARKRUN_UNDECLARED_EMIT',
|
|
41
|
+
'arkrun-undeclared-handle': 'ARKRUN_UNDECLARED_HANDLE',
|
|
42
|
+
'arkrun-undeclared-depend': 'ARKRUN_UNDECLARED_DEPEND',
|
|
43
|
+
'arkrun-transport-bypass': 'ARKRUN_TRANSPORT_BYPASS',
|
|
44
|
+
};
|
|
45
|
+
export const ARKRUN_INTERACTION_NAME_INCOMPLETE = 'ARKRUN_INTERACTION_NAME_INCOMPLETE';
|
|
46
|
+
function isDomainRoleLayer(layer, intentPrefixes = []) {
|
|
47
|
+
const name = layer.trim();
|
|
48
|
+
// Start-anchored Domain/entity/aggregate — unanchored "model" matches ReportingReadModels.
|
|
49
|
+
if (/^domain(?:model)?$/i.test(name) || /^domain(?=[A-Z_\-\s])/i.test(name))
|
|
50
|
+
return true;
|
|
51
|
+
if (/^(?:entit(?:y|ies)|aggregates?)(?:$|(?=[A-Z_\-\s]))/i.test(name))
|
|
52
|
+
return true;
|
|
53
|
+
return intentPrefixes.some((prefix) => {
|
|
54
|
+
const normalized = prefix.trim().replace(/\.+$/, '');
|
|
55
|
+
return normalized === 'Domain' || normalized.startsWith('Domain.');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function compareFindings(left, right) {
|
|
59
|
+
return (left.file.localeCompare(right.file) ||
|
|
60
|
+
left.ruleId.localeCompare(right.ruleId) ||
|
|
61
|
+
left.line - right.line ||
|
|
62
|
+
left.message.localeCompare(right.message));
|
|
63
|
+
}
|
|
64
|
+
function finding(extra, sensor, file, line, message, extras, teethAllowed) {
|
|
65
|
+
const failsStrict = extra.mode === 'enforced' && teethAllowed;
|
|
66
|
+
return {
|
|
67
|
+
ruleId: ARKRUN_RULE_IDS[sensor],
|
|
68
|
+
sensor,
|
|
69
|
+
message,
|
|
70
|
+
file,
|
|
71
|
+
line,
|
|
72
|
+
...(extras?.fromLayer ? { fromLayer: extras.fromLayer } : {}),
|
|
73
|
+
...(extras?.target ? { target: extras.target } : {}),
|
|
74
|
+
severity: failsStrict ? 'error' : 'warning',
|
|
75
|
+
failsStrict,
|
|
76
|
+
nextAction: deterministicNextAction({
|
|
77
|
+
ruleId: ARKRUN_RULE_IDS[sensor],
|
|
78
|
+
fromLayer: extras?.fromLayer,
|
|
79
|
+
target: extras?.target,
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function bagForFile(declarations, file) {
|
|
84
|
+
const uses = [];
|
|
85
|
+
const reactsTo = [];
|
|
86
|
+
const raises = [];
|
|
87
|
+
const sends = [];
|
|
88
|
+
for (const entry of declarations) {
|
|
89
|
+
if (entry.file !== file)
|
|
90
|
+
continue;
|
|
91
|
+
uses.push(...entry.uses);
|
|
92
|
+
reactsTo.push(...entry.reactsTo);
|
|
93
|
+
raises.push(...entry.raises);
|
|
94
|
+
sends.push(...entry.sends);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
uses: new Set(uses),
|
|
98
|
+
reactsTo: new Set(reactsTo),
|
|
99
|
+
raises: new Set(raises),
|
|
100
|
+
sends: new Set(sends),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function emitKinds(kind) {
|
|
104
|
+
return kind === 'publisher' || kind === 'publish' || kind === 'raise' || kind === 'send';
|
|
105
|
+
}
|
|
106
|
+
function handleKinds(kind) {
|
|
107
|
+
return kind === 'subscribe' || kind === 'register-handler';
|
|
108
|
+
}
|
|
109
|
+
function dependKinds(kind) {
|
|
110
|
+
return kind === 'resolve' || kind === 'resolve-singleton';
|
|
111
|
+
}
|
|
112
|
+
function evaluateMissingRoot(extra, hits, teethAllowed) {
|
|
113
|
+
const out = [];
|
|
114
|
+
const roots = extra.compositionRoots;
|
|
115
|
+
if (roots.length === 0) {
|
|
116
|
+
out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, 'ArkRun compositionRoots is empty; no createArkKernel factory site is declared.', undefined, teethAllowed));
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const hitsByRoot = new Map();
|
|
120
|
+
for (const hit of hits) {
|
|
121
|
+
const list = hitsByRoot.get(hit.matchedRoot) ?? [];
|
|
122
|
+
list.push(hit);
|
|
123
|
+
hitsByRoot.set(hit.matchedRoot, list);
|
|
124
|
+
}
|
|
125
|
+
for (const pattern of roots) {
|
|
126
|
+
const matched = [...(hitsByRoot.get(pattern) ?? [])].sort((left, right) => left.file.localeCompare(right.file));
|
|
127
|
+
if (matched.length === 0) {
|
|
128
|
+
out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, `ArkRun composition root ${JSON.stringify(pattern)} matched no governed files and has no createArkKernel factory.`, { target: pattern }, teethAllowed));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Factory required in the root set, not in every glob hit.
|
|
132
|
+
if (matched.some((hit) => hit.hasKernelFactory))
|
|
133
|
+
continue;
|
|
134
|
+
const first = matched[0];
|
|
135
|
+
out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun composition root ${JSON.stringify(pattern)} has no createArkKernel / createStrictArkKernel factory.`, { target: pattern }, teethAllowed));
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function evaluateKernelInDomain(extra, layers, dependencies, layerForFile, teethAllowed) {
|
|
140
|
+
const prefixes = new Map(layers.map((layer) => [layer.name, layer.intentPrefixes ?? []]));
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const dependency of dependencies) {
|
|
143
|
+
const specifier = dependency.specifier;
|
|
144
|
+
if (!specifier || !isArkRunKernelModuleSpecifier(specifier))
|
|
145
|
+
continue;
|
|
146
|
+
const fromLayer = layerForFile(dependency.from);
|
|
147
|
+
if (!fromLayer)
|
|
148
|
+
continue;
|
|
149
|
+
if (!isDomainRoleLayer(fromLayer, prefixes.get(fromLayer) ?? []))
|
|
150
|
+
continue;
|
|
151
|
+
out.push(finding(extra, 'arkrun-kernel-in-domain', dependency.from, dependency.line, `${fromLayer} must not import kernel module ${JSON.stringify(specifier)}.`, { fromLayer, target: specifier }, teethAllowed));
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function evaluateDirectNew(extra, layers, managedNews, hits, layerForFile, teethAllowed) {
|
|
156
|
+
const managed = new Set(extra.managedLayers);
|
|
157
|
+
if (managed.size === 0)
|
|
158
|
+
return [];
|
|
159
|
+
const prefixes = new Map(layers.map((layer) => [layer.name, layer.intentPrefixes ?? []]));
|
|
160
|
+
const admittedFactories = new Set(hits.filter((hit) => hit.hasKernelFactory).map((hit) => hit.file));
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const constructed of managedNews) {
|
|
163
|
+
if (admittedFactories.has(constructed.file))
|
|
164
|
+
continue;
|
|
165
|
+
const fromLayer = layerForFile(constructed.file);
|
|
166
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
167
|
+
continue;
|
|
168
|
+
if (isDomainRoleLayer(fromLayer, prefixes.get(fromLayer) ?? []))
|
|
169
|
+
continue;
|
|
170
|
+
out.push(finding(extra, 'arkrun-direct-new', constructed.file, constructed.line, `${fromLayer} must not construct ${constructed.typeName} with new outside an ArkRun composition-root factory.`, { fromLayer, target: constructed.typeName }, teethAllowed));
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function evaluateUndeclared(extra, kernelCalls, declarations, layerForFile, teethAllowed) {
|
|
175
|
+
const findings = [];
|
|
176
|
+
const completenessReasons = [];
|
|
177
|
+
if (extra.requireDeclarations !== true) {
|
|
178
|
+
return { findings, completenessReasons };
|
|
179
|
+
}
|
|
180
|
+
const managed = new Set(extra.managedLayers);
|
|
181
|
+
if (managed.size === 0)
|
|
182
|
+
return { findings, completenessReasons };
|
|
183
|
+
for (const call of kernelCalls) {
|
|
184
|
+
if (!emitKinds(call.kind) && !handleKinds(call.kind) && !dependKinds(call.kind))
|
|
185
|
+
continue;
|
|
186
|
+
const fromLayer = layerForFile(call.file);
|
|
187
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
188
|
+
continue;
|
|
189
|
+
if (!call.nameLiteral) {
|
|
190
|
+
if (extra.mode === 'enforced') {
|
|
191
|
+
completenessReasons.push({
|
|
192
|
+
code: ARKRUN_INTERACTION_NAME_INCOMPLETE,
|
|
193
|
+
file: call.file,
|
|
194
|
+
message: `ArkRun ${call.kind} call in ${call.file} has no string-literal name; enforced extra cannot prove the declaration.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const bag = bagForFile(declarations, call.file);
|
|
200
|
+
if (emitKinds(call.kind)) {
|
|
201
|
+
if (bag.raises.has(call.nameLiteral) || bag.sends.has(call.nameLiteral))
|
|
202
|
+
continue;
|
|
203
|
+
findings.push(finding(extra, 'arkrun-undeclared-emit', call.file, call.line, `Emit ${JSON.stringify(call.nameLiteral)} is not declared in raises or sends.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (handleKinds(call.kind)) {
|
|
207
|
+
if (bag.reactsTo.has(call.nameLiteral))
|
|
208
|
+
continue;
|
|
209
|
+
findings.push(finding(extra, 'arkrun-undeclared-handle', call.file, call.line, `Handle ${JSON.stringify(call.nameLiteral)} is not declared in reactsTo.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (bag.uses.has(call.nameLiteral))
|
|
213
|
+
continue;
|
|
214
|
+
findings.push(finding(extra, 'arkrun-undeclared-depend', call.file, call.line, `Depend ${JSON.stringify(call.nameLiteral)} is not declared in uses.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
215
|
+
}
|
|
216
|
+
return { findings, completenessReasons };
|
|
217
|
+
}
|
|
218
|
+
function evaluateTransportBypass(extra, dependencies, layerForFile, teethAllowed) {
|
|
219
|
+
const managed = new Set(extra.managedLayers);
|
|
220
|
+
if (managed.size === 0)
|
|
221
|
+
return [];
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const dependency of dependencies) {
|
|
224
|
+
if (dependency.typeOnly)
|
|
225
|
+
continue;
|
|
226
|
+
const specifier = dependency.specifier;
|
|
227
|
+
if (!specifier || !isArkRunTransportBypassSpecifier(specifier))
|
|
228
|
+
continue;
|
|
229
|
+
const fromLayer = layerForFile(dependency.from);
|
|
230
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
231
|
+
continue;
|
|
232
|
+
out.push(finding(extra, 'arkrun-transport-bypass', dependency.from, dependency.line, `${fromLayer} must not import broker/queue/emitter ${JSON.stringify(specifier)}; use the ArkRun kernel transport.`, { fromLayer, target: specifier }, teethAllowed));
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Evaluate closed tier-1 ArkRun sensors. Empty extra → no findings (silent).
|
|
238
|
+
*/
|
|
239
|
+
export function evaluateArkRunSensors(input) {
|
|
240
|
+
const extra = input.arkRun;
|
|
241
|
+
if (!extra)
|
|
242
|
+
return { findings: [], completenessReasons: [] };
|
|
243
|
+
const teethAllowed = extraMergeTeethAllowed(input.classification);
|
|
244
|
+
const undeclared = evaluateUndeclared(extra, input.kernelCalls, input.declarations, input.layerForFile, teethAllowed);
|
|
245
|
+
const findings = [
|
|
246
|
+
...evaluateMissingRoot(extra, input.compositionRootHits, teethAllowed),
|
|
247
|
+
...evaluateKernelInDomain(extra, input.layers, input.dependencies, input.layerForFile, teethAllowed),
|
|
248
|
+
...evaluateDirectNew(extra, input.layers, input.managedNews, input.compositionRootHits, input.layerForFile, teethAllowed),
|
|
249
|
+
...undeclared.findings,
|
|
250
|
+
...evaluateTransportBypass(extra, input.dependencies, input.layerForFile, teethAllowed),
|
|
251
|
+
].sort(compareFindings);
|
|
252
|
+
const completenessReasons = [...undeclared.completenessReasons].sort((left, right) => {
|
|
253
|
+
const leftKey = `${left.code}\0${left.file ?? ''}\0${left.message}`;
|
|
254
|
+
const rightKey = `${right.code}\0${right.file ?? ''}\0${right.message}`;
|
|
255
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
256
|
+
});
|
|
257
|
+
return { findings, completenessReasons };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Same sensors as `evaluateArkRunSensors`, filtered to the ESLint import/`new` envelope.
|
|
261
|
+
* Does not emit missing-root or undeclared-* (those need project-wide / declaration facts).
|
|
262
|
+
*/
|
|
263
|
+
export function evaluateArkRunEditorSensors(input) {
|
|
264
|
+
const result = evaluateArkRunSensors(input);
|
|
265
|
+
return {
|
|
266
|
+
findings: result.findings.filter((item) => isArkRunEditorSensor(item.sensor)),
|
|
267
|
+
completenessReasons: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function fileMatchesCompositionRoot(pattern, file) {
|
|
271
|
+
if (pattern === file)
|
|
272
|
+
return true;
|
|
273
|
+
const star = pattern.indexOf('*');
|
|
274
|
+
if (star < 0)
|
|
275
|
+
return false;
|
|
276
|
+
const prefix = pattern.slice(0, star).replace(/\/$/, '');
|
|
277
|
+
return prefix.length > 0 && (file === prefix || file.startsWith(`${prefix}/`));
|
|
278
|
+
}
|
|
279
|
+
function compositionRootHitsForSource(extra, file, source) {
|
|
280
|
+
const hasFactory = extractArkRunKernelCallsFromSource(file, source).some((call) => call.kind === 'factory');
|
|
281
|
+
const hits = [];
|
|
282
|
+
for (const pattern of extra.compositionRoots) {
|
|
283
|
+
if (!fileMatchesCompositionRoot(pattern, file))
|
|
284
|
+
continue;
|
|
285
|
+
hits.push({ file, matchedRoot: pattern, hasKernelFactory: hasFactory });
|
|
286
|
+
}
|
|
287
|
+
return hits;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Import / `new` envelope from one proposed source (hook Write/Edit, snippet MCP).
|
|
291
|
+
* Missing-root and undeclared-* stay project-wide CLI/MCP/preflight.
|
|
292
|
+
*/
|
|
293
|
+
export function evaluateArkRunEditorSensorsFromSource(input) {
|
|
294
|
+
const extra = input.arkRun;
|
|
295
|
+
if (!extra)
|
|
296
|
+
return { findings: [], completenessReasons: [] };
|
|
297
|
+
const admitted = new Set(extractArkRunImportedConstructorNamesFromSource(input.source));
|
|
298
|
+
return evaluateArkRunEditorSensors({
|
|
299
|
+
arkRun: extra,
|
|
300
|
+
layers: input.layers,
|
|
301
|
+
kernelCalls: [],
|
|
302
|
+
managedNews: extractArkRunManagedNewsFromSource(input.file, input.source, admitted),
|
|
303
|
+
compositionRootHits: compositionRootHitsForSource(extra, input.file, input.source),
|
|
304
|
+
declarations: [],
|
|
305
|
+
dependencies: extractArkRunValueImportDependenciesFromSource(input.file, input.source),
|
|
306
|
+
layerForFile: input.layerForFile,
|
|
307
|
+
classification: input.classification,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/config-contract.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** Current published ark.config.json schema version (ADR
|
|
12
|
-
export const ARK_CONFIG_SCHEMA_VERSION = '1.
|
|
11
|
+
/** Current published ark.config.json schema version (ADR 0020: 1.2 adds optional arkRun). */
|
|
12
|
+
export const ARK_CONFIG_SCHEMA_VERSION = '1.2';
|
|
13
13
|
export const ARK_CONFIG_SCHEMA_URL = 'https://unpkg.com/arkgate@2/schemas/ark.config.schema.json';
|
|
14
14
|
const DEFAULT_LAYER_NAMES = [
|
|
15
15
|
'DomainModel',
|
|
@@ -50,6 +50,7 @@ export const DEFAULT_ARK_CONFIG_RULES = createDefaultRules();
|
|
|
50
50
|
export const ARK_CONFIG_MIGRATIONS = [
|
|
51
51
|
{ from: 'unversioned', to: '1.0' },
|
|
52
52
|
{ from: '1.0', to: '1.1' },
|
|
53
|
+
{ from: '1.1', to: '1.2' },
|
|
53
54
|
];
|
|
54
55
|
const stringArraySchema = {
|
|
55
56
|
type: 'array',
|
|
@@ -112,6 +113,8 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
112
113
|
additionalProperties: { type: 'string', minLength: 1 },
|
|
113
114
|
default: {},
|
|
114
115
|
},
|
|
116
|
+
/** ADR 0020 — optional ArkRun extra. Absence is silent; unknown keys fail closed. */
|
|
117
|
+
arkRun: { $ref: '#/$defs/arkRun' },
|
|
115
118
|
/** Team parliament — GitHub handles or emails who may loosen the law (not part of policy hash). */
|
|
116
119
|
stewards: { ...stringArraySchema, default: [] },
|
|
117
120
|
},
|
|
@@ -181,6 +184,20 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
181
184
|
allowDisabledPeerIsolation: { type: 'boolean', default: false },
|
|
182
185
|
},
|
|
183
186
|
},
|
|
187
|
+
arkRun: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
properties: {
|
|
191
|
+
mode: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
enum: ['advisory', 'enforced'],
|
|
194
|
+
default: 'advisory',
|
|
195
|
+
},
|
|
196
|
+
compositionRoots: { ...stringArraySchema, default: [] },
|
|
197
|
+
managedLayers: { ...stringArraySchema, default: [] },
|
|
198
|
+
requireDeclarations: { type: 'boolean', default: true },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
184
201
|
},
|
|
185
202
|
};
|
|
186
203
|
export class ArkConfigValidationError extends Error {
|
|
@@ -314,8 +331,19 @@ function validateNode(value, schema, path, root, issues) {
|
|
|
314
331
|
}
|
|
315
332
|
}
|
|
316
333
|
}
|
|
317
|
-
function
|
|
334
|
+
function defaultedArkRun(value) {
|
|
335
|
+
if (!isObject(value))
|
|
336
|
+
return value;
|
|
318
337
|
return {
|
|
338
|
+
...value,
|
|
339
|
+
mode: value.mode === undefined ? 'advisory' : value.mode,
|
|
340
|
+
compositionRoots: value.compositionRoots === undefined ? [] : value.compositionRoots,
|
|
341
|
+
managedLayers: value.managedLayers === undefined ? [] : value.managedLayers,
|
|
342
|
+
requireDeclarations: value.requireDeclarations === undefined ? true : value.requireDeclarations,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function defaultedConfig(input) {
|
|
346
|
+
const result = {
|
|
319
347
|
...input,
|
|
320
348
|
$schema: input.$schema === undefined ? ARK_CONFIG_SCHEMA_URL : input.$schema,
|
|
321
349
|
schemaVersion: input.schemaVersion === undefined ? ARK_CONFIG_SCHEMA_VERSION : input.schemaVersion,
|
|
@@ -325,6 +353,57 @@ function defaultedConfig(input) {
|
|
|
325
353
|
? DEFAULT_ARK_CONFIG_RULES.map((rule) => ({ ...rule }))
|
|
326
354
|
: input.rules,
|
|
327
355
|
};
|
|
356
|
+
if (input.arkRun !== undefined)
|
|
357
|
+
result.arkRun = defaultedArkRun(input.arkRun);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function validateArkRunExtra(config, issues) {
|
|
361
|
+
const extra = config.arkRun;
|
|
362
|
+
if (extra === undefined || !isObject(extra))
|
|
363
|
+
return;
|
|
364
|
+
const layerNames = new Set();
|
|
365
|
+
if (Array.isArray(config.layers)) {
|
|
366
|
+
for (const layer of config.layers) {
|
|
367
|
+
if (isObject(layer) && typeof layer.name === 'string' && layer.name.length > 0) {
|
|
368
|
+
layerNames.add(layer.name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const managed = extra.managedLayers;
|
|
373
|
+
if (Array.isArray(managed)) {
|
|
374
|
+
managed.forEach((name, index) => {
|
|
375
|
+
if (typeof name === 'string' && name.length > 0 && !layerNames.has(name)) {
|
|
376
|
+
issues.push({
|
|
377
|
+
path: `$.arkRun.managedLayers[${index}]`,
|
|
378
|
+
message: `layer ${JSON.stringify(name)} is not declared in layers[]`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (extra.mode === 'enforced') {
|
|
384
|
+
const roots = extra.compositionRoots;
|
|
385
|
+
if (!Array.isArray(roots) || roots.length === 0) {
|
|
386
|
+
issues.push({
|
|
387
|
+
path: '$.arkRun.compositionRoots',
|
|
388
|
+
message: 'ARKRUN_MISSING_ROOT: enforced mode requires at least one composition root',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (!Array.isArray(managed) || managed.length === 0) {
|
|
392
|
+
issues.push({
|
|
393
|
+
path: '$.arkRun.managedLayers',
|
|
394
|
+
message: 'enforced mode requires at least one managed layer',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function migratedFromOf(originalVersion) {
|
|
400
|
+
if (originalVersion === ARK_CONFIG_SCHEMA_VERSION)
|
|
401
|
+
return null;
|
|
402
|
+
if (originalVersion === 'unversioned')
|
|
403
|
+
return 'unversioned';
|
|
404
|
+
if (originalVersion === '1.0' || originalVersion === '1.1')
|
|
405
|
+
return originalVersion;
|
|
406
|
+
return null;
|
|
328
407
|
}
|
|
329
408
|
function knownInputVersions() {
|
|
330
409
|
const versions = new Set([ARK_CONFIG_SCHEMA_VERSION]);
|
|
@@ -369,8 +448,8 @@ export function migrateArkConfig(input, source = 'ark.config.json') {
|
|
|
369
448
|
}
|
|
370
449
|
let version = originalVersion;
|
|
371
450
|
const working = { ...input };
|
|
372
|
-
// Walk the migration table. Each step is a pure version stamp
|
|
373
|
-
//
|
|
451
|
+
// Walk the migration table. Each step is a pure version stamp (optional extras
|
|
452
|
+
// like arkRules / arkRun need no field rewrite when absent).
|
|
374
453
|
let guard = 0;
|
|
375
454
|
while (version !== ARK_CONFIG_SCHEMA_VERSION && guard < ARK_CONFIG_MIGRATIONS.length + 1) {
|
|
376
455
|
guard += 1;
|
|
@@ -394,17 +473,13 @@ export function migrateArkConfig(input, source = 'ark.config.json') {
|
|
|
394
473
|
},
|
|
395
474
|
]);
|
|
396
475
|
}
|
|
397
|
-
|
|
398
|
-
? 'unversioned'
|
|
399
|
-
: originalVersion === '1.0'
|
|
400
|
-
? '1.0'
|
|
401
|
-
: null;
|
|
402
|
-
return { candidate: defaultedConfig(working), migratedFrom };
|
|
476
|
+
return { candidate: defaultedConfig(working), migratedFrom: migratedFromOf(originalVersion) };
|
|
403
477
|
}
|
|
404
478
|
export function loadArkConfigContract(input, source = 'ark.config.json') {
|
|
405
479
|
const { candidate, migratedFrom } = migrateArkConfig(input, source);
|
|
406
480
|
const issues = [];
|
|
407
481
|
validateNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
|
|
482
|
+
validateArkRunExtra(candidate, issues);
|
|
408
483
|
if (issues.length > 0)
|
|
409
484
|
throw new ArkConfigValidationError(source, issues);
|
|
410
485
|
return { config: candidate, migratedFrom };
|
|
@@ -56,6 +56,14 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
56
56
|
entry('ARKRULE_INVARIANT', 'arkrules', 'ArkRule invariant failed', 'Reserved / remediation-recognized code for invariant-plane failures bound to an ArkRule id (coverage path also emits INVARIANT_UNCOVERED).', 'Fix the invariant for the ArkRule declared in arkrules/<Layer>.json, then preflight again. Do not demote without acknowledgement.'),
|
|
57
57
|
entry('ARKRULE_SCOPE_EMPTY', 'arkrules', 'ArkRule appliesTo matched zero files', 'An ArkRule’s appliesTo globs matched no governed files — the rule cannot observe what it claims to protect.', 'Fix appliesTo globs so they match governed files, or remove the rule. Enforced empty scope fails; advisory empty scope warns.', { oftenAdvisory: true }),
|
|
58
58
|
entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green.'),
|
|
59
|
+
// ── ArkRun (opt-in extra; RN05 dual-depth nextAction) ────────────────────
|
|
60
|
+
entry('ARKRUN_MISSING_ROOT', 'arkrun', 'No kernel factory in composition roots', 'The ArkRun extra is on but no createArkKernel / createStrictArkKernel / createArkKernelFromConfig / createStrictArkKernelFromConfig factory was found in arkRun.compositionRoots, so agents can skip the kernel while the write gate stays green.', 'Import createStrictArkKernel from @arkgate/runtime (never a removed arkgate/runtime shim) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
|
|
61
|
+
entry('ARKRUN_KERNEL_IN_DOMAIN', 'arkrun', 'Domain-role layer imports the kernel', 'A Domain-role layer imports @arkgate/runtime or kernel types. Domain stays kernel-free; composition roots and adapters own the factory.', 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again. Never mechanical-safe.'),
|
|
62
|
+
entry('ARKRUN_DIRECT_NEW', 'arkrun', 'Managed type constructed with new', 'A managed non-Domain file constructs an admitted type with new outside an ArkRun composition-root factory, skipping kernel resolve/registration.', 'Resolve the type from the kernel instead of constructing it with new, then preflight again. Never mechanical-safe — rewiring construction is a design decision.'),
|
|
63
|
+
entry('ARKRUN_UNDECLARED_EMIT', 'arkrun', 'Emit name not in raises/sends', 'A publisher / publish / raise / send call-site literal is not listed in the file’s raises or sends declaration.', 'Add the existing call-site name to raises or sends on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new emit stays judgment.'),
|
|
64
|
+
entry('ARKRUN_UNDECLARED_HANDLE', 'arkrun', 'Handle name not in reactsTo', 'A subscribe / registerHandler call-site literal is not listed in the file’s reactsTo declaration.', 'Add the existing call-site name to reactsTo on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new handle stays judgment.'),
|
|
65
|
+
entry('ARKRUN_UNDECLARED_DEPEND', 'arkrun', 'Depend name not in uses', 'A resolve / resolveSingleton call-site literal is not listed in the file’s uses declaration.', 'Add the existing call-site name to uses on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new depend stays judgment.'),
|
|
66
|
+
entry('ARKRUN_TRANSPORT_BYPASS', 'arkrun', 'Homemade broker or emitter import', 'A managed layer imports a closed broker/queue/emitter specifier (EventEmitter, queue clients, …) instead of the ArkRun kernel transport.', 'Send through the ArkRun kernel transport instead of importing that broker or emitter, then preflight again. Never mechanical-safe — homemade buses stay judgment.'),
|
|
59
67
|
// ── atomic preflight / change set ────────────────────────────────────────
|
|
60
68
|
entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
|
|
61
69
|
entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
|
|
@@ -20,8 +20,20 @@ import { printParseHealthSection, summarizeParseHealth } from './parse-health.mj
|
|
|
20
20
|
import { detectGraphBlindSpots, printGraphBlindSection } from './graph-blind.mjs';
|
|
21
21
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
22
22
|
import { collectStewardNudge } from './team-parliament-io.mjs';
|
|
23
|
+
import { formatArkRunDoctorLines, summarizeArkRunSection } from './ark-run-doctor.mjs';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
function classificationFromCoverage(cov) {
|
|
26
|
+
return {
|
|
27
|
+
governedPercent: cov?.governed?.percent ?? null,
|
|
28
|
+
populatedLayerCount: Array.isArray(cov?.layers)
|
|
29
|
+
? cov.layers.filter((row) => (row?.files ?? 0) > 0).length
|
|
30
|
+
: null,
|
|
31
|
+
classifiedFiles: cov?.governed?.classifiedFiles ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `activeViolations` must already exclude frozen baseline keys (report residual parity). */
|
|
36
|
+
export function computeDoctorAdvisories(root, config, cov, rules, files, ts, parseHealth, facts, activeViolations) {
|
|
25
37
|
const physicalCohesion = computePhysicalCohesion(root, files);
|
|
26
38
|
const decisionMemory = computeReshapeDecisionMemory(root, files);
|
|
27
39
|
physicalCohesion.reshapeDecisions = decisionMemory.summary;
|
|
@@ -41,6 +53,27 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
|
|
|
41
53
|
})).filter((f) => f.path),
|
|
42
54
|
}
|
|
43
55
|
: undefined);
|
|
56
|
+
const classification = classificationFromCoverage(cov);
|
|
57
|
+
const rulesUnderContract = summarizeRulesUnderContract(root, config, factPaths, classification);
|
|
58
|
+
const arkRun = summarizeArkRunSection({
|
|
59
|
+
arkRun: config?.arkRun,
|
|
60
|
+
findings: activeViolations,
|
|
61
|
+
classification,
|
|
62
|
+
arkRules: {
|
|
63
|
+
active: rulesUnderContract?.active === true,
|
|
64
|
+
structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
|
|
65
|
+
structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
|
|
66
|
+
structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
|
|
67
|
+
invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
|
|
68
|
+
invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
|
|
69
|
+
invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
|
|
70
|
+
covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
|
|
71
|
+
uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (rulesUnderContract?.mergePlanes) {
|
|
75
|
+
rulesUnderContract.mergePlanes = arkRun.mergePlanes;
|
|
76
|
+
}
|
|
44
77
|
return {
|
|
45
78
|
contractHealth: computeContractHealth(root, config, cov, rules),
|
|
46
79
|
ambientState: computeAmbientState(ts, root, config, files),
|
|
@@ -51,13 +84,8 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
|
|
|
51
84
|
// AR12 — Rules under contract (honest counts; real test I/O, never empty-fileContents stub).
|
|
52
85
|
// P1M: pass classification so extraMergeTeeth cannot arm at 0% governed.
|
|
53
86
|
stewardNudge: collectStewardNudge(root, config),
|
|
54
|
-
rulesUnderContract
|
|
55
|
-
|
|
56
|
-
populatedLayerCount: Array.isArray(cov?.layers)
|
|
57
|
-
? cov.layers.filter((row) => (row?.files ?? 0) > 0).length
|
|
58
|
-
: null,
|
|
59
|
-
classifiedFiles: cov?.governed?.classifiedFiles ?? null,
|
|
60
|
-
}),
|
|
87
|
+
rulesUnderContract,
|
|
88
|
+
arkRun,
|
|
61
89
|
};
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -79,4 +107,13 @@ export function printDoctorAdvisories(advisories, io) {
|
|
|
79
107
|
io.line(io.warn, nudge.ask);
|
|
80
108
|
if (nudge.nextAction) io.line(' ', io.color.dim(`Next: ${nudge.nextAction}`));
|
|
81
109
|
}
|
|
110
|
+
const arkRun = advisories.arkRun;
|
|
111
|
+
if (arkRun && arkRun.notAScore === true) {
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(io.color.bold('ArkRun (not a score)'));
|
|
114
|
+
const mark = arkRun.active && arkRun.residual?.count > 0 ? io.warn : ' ';
|
|
115
|
+
for (const text of formatArkRunDoctorLines(arkRun)) {
|
|
116
|
+
io.line(mark, text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
82
119
|
}
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -140,6 +140,16 @@ export function printDoctorCompactHuman(view) {
|
|
|
140
140
|
line(warn, nudge.ask);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
const arkRun = doctorAdvisories.arkRun;
|
|
144
|
+
if (arkRun?.active === true && arkRun.notAScore === true) {
|
|
145
|
+
console.log('');
|
|
146
|
+
const residual = Number(arkRun.residual?.count) || 0;
|
|
147
|
+
line(
|
|
148
|
+
residual > 0 ? warn : ' ',
|
|
149
|
+
`ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
if (violations.length === 0) {
|
|
144
154
|
if (!analysisComplete) {
|
|
145
155
|
console.log('');
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -599,21 +599,22 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
599
599
|
patternBets: patternBetsForLoop,
|
|
600
600
|
designSmells,
|
|
601
601
|
});
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
602
|
+
const activeViolations = baseline.exists
|
|
603
|
+
? violations.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]))
|
|
604
|
+
: violations;
|
|
605
|
+
const doctorAdvisories = computeDoctorAdvisories(
|
|
606
606
|
root,
|
|
607
607
|
config,
|
|
608
|
+
cov,
|
|
609
|
+
rules,
|
|
610
|
+
files,
|
|
611
|
+
options.ts,
|
|
612
|
+
options.parseHealth,
|
|
608
613
|
options.facts ?? options.architectureFacts,
|
|
609
|
-
|
|
610
|
-
governedPercent: cov.governed?.percent ?? null,
|
|
611
|
-
populatedLayerCount: Array.isArray(cov.layers)
|
|
612
|
-
? cov.layers.filter((row) => (row?.files ?? 0) > 0).length
|
|
613
|
-
: null,
|
|
614
|
-
classifiedFiles: cov.governed?.classifiedFiles ?? null,
|
|
615
|
-
}
|
|
614
|
+
activeViolations
|
|
616
615
|
);
|
|
616
|
+
const rulesUnderContract = doctorAdvisories.rulesUnderContract;
|
|
617
|
+
const arkRun = doctorAdvisories.arkRun;
|
|
617
618
|
// Single residual expression (nextPilot || extractionCard) — HTML report uses the same.
|
|
618
619
|
const residualPilot = pilotLoop?.nextPilot || pilotLoop?.extractionCard || null;
|
|
619
620
|
// Evidence-backed hard only (never capabilities-from-hook-files alone).
|
|
@@ -653,9 +654,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
653
654
|
residualPilots: Boolean(residualPilot) && designFitness.designWeak === true,
|
|
654
655
|
pilotTarget: residualPilot?.pilotTarget ?? residualPilot?.pilot ?? null,
|
|
655
656
|
arkRulesMergeHonesty: rulesUnderContract?.mergePlanes
|
|
656
|
-
? {
|
|
657
|
-
|
|
658
|
-
|
|
657
|
+
? {
|
|
658
|
+
active: rulesUnderContract.active === true || arkRun?.active === true,
|
|
659
|
+
...rulesUnderContract.mergePlanes,
|
|
660
|
+
}
|
|
661
|
+
: rulesUnderContract?.active === true || arkRun?.active === true
|
|
662
|
+
? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true }
|
|
659
663
|
: null,
|
|
660
664
|
primaryNextAction:
|
|
661
665
|
adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
|
|
@@ -746,10 +750,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
746
750
|
// Dual-truth: managed CLI vs package.json pin (not a gate fail).
|
|
747
751
|
packageVersionTruth,
|
|
748
752
|
// Advisories, never a verdict: W01/U05/X04/Y03 + graph-blind spots.
|
|
749
|
-
//
|
|
753
|
+
// Re-assert after spread so mergePlanes from this scan wins.
|
|
750
754
|
...doctorAdvisories,
|
|
751
|
-
// AR12 + P1-M mergePlanes (authoritative; after advisories spread).
|
|
752
755
|
rulesUnderContract,
|
|
756
|
+
arkRun,
|
|
753
757
|
// P0-B — single anti-false-green honesty surface (never a score).
|
|
754
758
|
productHonesty,
|
|
755
759
|
governed: cov.governed,
|