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
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
|
}
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
8
8
|
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
9
9
|
import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
|
|
10
|
+
import {
|
|
11
|
+
EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
|
|
12
|
+
composeMergePlanesHonesty,
|
|
13
|
+
demoteExtraPlaneTeethUnderClassificationFloor,
|
|
14
|
+
} from './extra-merge-teeth.mjs';
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Cap long catalogs in doctor JSON (and HTML, which consumes the same summary).
|
|
@@ -16,12 +21,11 @@ const COVERED_SAMPLE_MAX = 24;
|
|
|
16
21
|
const STRUCTURE_CATALOG_MAX = 40;
|
|
17
22
|
const UNCOVERED_CATALOG_MAX = 30;
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
|
|
24
|
+
export { EXTRA_MERGE_TEETH_GOVERNED_FLOOR };
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* P1M / extraMergeTeeth: under the classification floor, demote enforced ArkRules
|
|
24
|
-
*
|
|
28
|
+
* and ArkRun findings so merge matches doctor stamp (layer graph only).
|
|
25
29
|
* Unknown classification (null/null) → do not demote (contract-only callers).
|
|
26
30
|
*
|
|
27
31
|
* @param {object[]} violations
|
|
@@ -29,28 +33,7 @@ export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
|
|
|
29
33
|
* @returns {object[]}
|
|
30
34
|
*/
|
|
31
35
|
export function demoteArkRuleTeethUnderClassificationFloor(violations, classification = {}) {
|
|
32
|
-
|
|
33
|
-
const governed =
|
|
34
|
-
typeof classification.governedPercent === 'number' ? classification.governedPercent : null;
|
|
35
|
-
const populated =
|
|
36
|
-
typeof classification.populatedLayerCount === 'number'
|
|
37
|
-
? classification.populatedLayerCount
|
|
38
|
-
: null;
|
|
39
|
-
if (governed == null && populated == null) return violations;
|
|
40
|
-
const allowsTeeth =
|
|
41
|
-
(governed ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR && (populated ?? 0) >= 1;
|
|
42
|
-
if (allowsTeeth) return violations;
|
|
43
|
-
for (const v of violations) {
|
|
44
|
-
const isArkRule =
|
|
45
|
-
v?.arkruleId != null ||
|
|
46
|
-
(typeof v?.ruleId === 'string' &&
|
|
47
|
-
(v.ruleId.startsWith('ARKRULE') || v.ruleId.startsWith('arkrule')));
|
|
48
|
-
if (isArkRule && v.failsStrict !== false) {
|
|
49
|
-
v.failsStrict = false;
|
|
50
|
-
if (v.severity === 'error') v.severity = 'warning';
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return violations;
|
|
36
|
+
return demoteExtraPlaneTeethUnderClassificationFloor(violations, classification);
|
|
54
37
|
}
|
|
55
38
|
|
|
56
39
|
/**
|
|
@@ -63,6 +46,18 @@ export function demoteArkRuleTeethUnderClassificationFloor(violations, classific
|
|
|
63
46
|
* classifiedFiles?: number | null,
|
|
64
47
|
* }} [classification] layer-plane coverage (when known)
|
|
65
48
|
*/
|
|
49
|
+
function arkRunMergeInput(config, residualCount = 0) {
|
|
50
|
+
const extra = config?.arkRun;
|
|
51
|
+
if (!extra || typeof extra !== 'object') {
|
|
52
|
+
return { present: false, mode: null, residualCount: 0 };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
present: true,
|
|
56
|
+
mode: extra.mode === 'enforced' || extra.mode === 'advisory' ? extra.mode : null,
|
|
57
|
+
residualCount: Number(residualCount) || 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
66
61
|
export function summarizeRulesUnderContract(root, config, facts, classification) {
|
|
67
62
|
if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
|
|
68
63
|
return {
|
|
@@ -71,6 +66,11 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
71
66
|
invariants: 0,
|
|
72
67
|
coveredInvariants: 0,
|
|
73
68
|
uncoveredInvariants: 0,
|
|
69
|
+
mergePlanes: composeMergePlanesHonesty({
|
|
70
|
+
classification,
|
|
71
|
+
arkRules: { active: false },
|
|
72
|
+
arkRun: arkRunMergeInput(config),
|
|
73
|
+
}),
|
|
74
74
|
notAScore: true,
|
|
75
75
|
note: 'No arkRules map — intra-layer ArkRules are opt-in.',
|
|
76
76
|
};
|
|
@@ -162,73 +162,21 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
162
162
|
const invariantAdvisory = invariants - invariantEnforced;
|
|
163
163
|
const coveredInvariants = coverage.coverage.filter((c) => c.covered).length;
|
|
164
164
|
const uncoveredInvariants = coverage.coverage.filter((c) => !c.covered).length;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
classification && typeof classification.populatedLayerCount === 'number'
|
|
176
|
-
? classification.populatedLayerCount
|
|
177
|
-
: classification && typeof classification.classifiedFiles === 'number'
|
|
178
|
-
? classification.classifiedFiles > 0
|
|
179
|
-
? 1
|
|
180
|
-
: 0
|
|
181
|
-
: null;
|
|
182
|
-
const classificationKnown = governedPercent != null || populatedLayerCount != null;
|
|
183
|
-
const classificationAllowsTeeth = !classificationKnown
|
|
184
|
-
? true
|
|
185
|
-
: (governedPercent ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR &&
|
|
186
|
-
(populatedLayerCount ?? 0) >= 1;
|
|
187
|
-
const extraMergeTeeth = hasEnforcedTeeth && classificationAllowsTeeth;
|
|
188
|
-
const teethDeferredForClassification =
|
|
189
|
-
hasEnforcedTeeth && classificationKnown && !classificationAllowsTeeth;
|
|
190
|
-
// P1-M — which plane can fail merge (layers vs enforced structure vs invariants).
|
|
191
|
-
const mergePlanes = {
|
|
192
|
-
layers: {
|
|
193
|
-
role: 'inter-layer-edges',
|
|
194
|
-
alwaysOnGate: true,
|
|
195
|
-
note: 'Import/export layer graph — the default merge plane. Absent arkRules changes nothing here.',
|
|
196
|
-
},
|
|
197
|
-
structureSensors: {
|
|
198
|
-
role: 'intra-layer-heuristics',
|
|
199
|
-
total: structureRules,
|
|
200
|
-
enforced: structureEnforced,
|
|
201
|
-
advisory: structureAdvisory,
|
|
202
|
-
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).',
|
|
203
|
-
},
|
|
204
|
-
invariants: {
|
|
205
|
-
role: 'catalog-plus-coverage',
|
|
206
|
-
total: invariants,
|
|
207
|
-
enforced: invariantEnforced,
|
|
208
|
-
advisory: invariantAdvisory,
|
|
165
|
+
const mergePlanes = composeMergePlanesHonesty({
|
|
166
|
+
classification,
|
|
167
|
+
arkRules: {
|
|
168
|
+
active: true,
|
|
169
|
+
structureEnforced,
|
|
170
|
+
structureTotal: structureRules,
|
|
171
|
+
structureAdvisory,
|
|
172
|
+
invariantEnforced,
|
|
173
|
+
invariantTotal: invariants,
|
|
174
|
+
invariantAdvisory,
|
|
209
175
|
covered: coveredInvariants,
|
|
210
176
|
uncovered: uncoveredInvariants,
|
|
211
|
-
note: 'Invariants are catalog + coverage evidence, not a business runtime. Enforced + proven-uncovered fails merge; absence of enforced rules adds no extra teeth.',
|
|
212
177
|
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
extraMergeTeeth,
|
|
216
|
-
...(classificationKnown
|
|
217
|
-
? {
|
|
218
|
-
classificationGate: {
|
|
219
|
-
governedPercent: governedPercent ?? null,
|
|
220
|
-
populatedLayerCount: populatedLayerCount ?? null,
|
|
221
|
-
floorPercent: EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
|
|
222
|
-
allowsTeeth: classificationAllowsTeeth,
|
|
223
|
-
},
|
|
224
|
-
}
|
|
225
|
-
: {}),
|
|
226
|
-
failMergeWhen: extraMergeTeeth
|
|
227
|
-
? 'Layer graph failures plus enforced structure/invariant findings (advisory sensors never fail merge alone).'
|
|
228
|
-
: teethDeferredForClassification
|
|
229
|
-
? `Layer graph only — enforced ArkRules structure/invariant 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.`
|
|
230
|
-
: 'Layer graph only — no enforced ArkRules structure/invariant teeth on this tree. Advisory packs do not arm merge teeth.',
|
|
231
|
-
};
|
|
178
|
+
arkRun: arkRunMergeInput(config),
|
|
179
|
+
});
|
|
232
180
|
|
|
233
181
|
return {
|
|
234
182
|
active: true,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Fail-closed completeness evidence for one proposed source snippet. */
|
|
2
|
+
import { layerForRelativePath } from '../ark-layer-match.mjs';
|
|
2
3
|
import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
|
|
4
|
+
import { evaluateArkRunEditorSensorsFromSource } from './ark-run-sensors.mjs';
|
|
3
5
|
|
|
4
6
|
export function flattenTsParseDiagnostics(ts, diagnostics, sourceFile) {
|
|
5
7
|
if (!Array.isArray(diagnostics) || !ts) return [];
|
|
@@ -31,11 +33,50 @@ function finding(ruleId, message, file, nextAction) {
|
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function arkRunSnippetViolations(source, context = {}) {
|
|
37
|
+
const extra = context.arkRun;
|
|
38
|
+
const layers = context.layers;
|
|
39
|
+
const file = context.relFile || context.filePath;
|
|
40
|
+
if (!extra || !Array.isArray(layers) || typeof file !== 'string' || file.length === 0) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const layerForFile = (pathValue) => {
|
|
44
|
+
if (pathValue === file && typeof context.layer === 'string') return context.layer;
|
|
45
|
+
return layerForRelativePath(pathValue, layers);
|
|
46
|
+
};
|
|
47
|
+
const { findings } = evaluateArkRunEditorSensorsFromSource({
|
|
48
|
+
arkRun: extra,
|
|
49
|
+
layers,
|
|
50
|
+
file,
|
|
51
|
+
source,
|
|
52
|
+
layerForFile,
|
|
53
|
+
classification: context.classification,
|
|
54
|
+
});
|
|
55
|
+
return findings
|
|
56
|
+
.filter((finding) => finding.failsStrict)
|
|
57
|
+
.map((finding) => ({
|
|
58
|
+
ruleId: finding.ruleId,
|
|
59
|
+
code: finding.ruleId,
|
|
60
|
+
message: finding.message,
|
|
61
|
+
file: finding.file,
|
|
62
|
+
line: finding.line,
|
|
63
|
+
fromLayer: finding.fromLayer,
|
|
64
|
+
target: finding.target,
|
|
65
|
+
nextAction: finding.nextAction,
|
|
66
|
+
failsStrict: true,
|
|
67
|
+
severity: 'error',
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
34
71
|
export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
|
|
35
72
|
const observed = gate.validate(source, context);
|
|
73
|
+
const arkRunViolations = arkRunSnippetViolations(source, context);
|
|
36
74
|
const base = {
|
|
37
|
-
valid: Boolean(observed.lexicalValid ?? observed.valid),
|
|
38
|
-
violations:
|
|
75
|
+
valid: Boolean(observed.lexicalValid ?? observed.valid) && arkRunViolations.length === 0,
|
|
76
|
+
violations: [
|
|
77
|
+
...(Array.isArray(observed.violations) ? observed.violations : []),
|
|
78
|
+
...arkRunViolations,
|
|
79
|
+
],
|
|
39
80
|
};
|
|
40
81
|
const file = context.filePath;
|
|
41
82
|
|
|
@@ -25,6 +25,7 @@ import { detectActiveAgentHost } from './skill-install.mjs';
|
|
|
25
25
|
import { readBaseline } from './violations.mjs';
|
|
26
26
|
import { reportsDir, readJsonSafe } from './html-report.mjs';
|
|
27
27
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
28
|
+
import { projectStatusArkRun } from './ark-run-doctor.mjs';
|
|
28
29
|
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
29
30
|
import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
|
|
30
31
|
|
|
@@ -340,6 +341,19 @@ export function collectStatusFacts(options = {}) {
|
|
|
340
341
|
|
|
341
342
|
const arkruleFrozenFallback = countArkruleFrozenKeys(baseline);
|
|
342
343
|
|
|
344
|
+
const arkRun = (() => {
|
|
345
|
+
const extra = config?.arkRun;
|
|
346
|
+
if (!extra || typeof extra !== 'object') {
|
|
347
|
+
return projectStatusArkRun({ present: false, residual: 0 });
|
|
348
|
+
}
|
|
349
|
+
const snap = latest?.arkRun && typeof latest.arkRun === 'object' ? latest.arkRun : null;
|
|
350
|
+
const mode = extra.mode === 'enforced' || extra.mode === 'advisory' ? extra.mode : null;
|
|
351
|
+
const extraMergeTeeth =
|
|
352
|
+
snap && typeof snap.extraMergeTeeth === 'boolean' ? snap.extraMergeTeeth === true : false;
|
|
353
|
+
const residual = typeof snap?.residual === 'number' ? snap.residual : null;
|
|
354
|
+
return projectStatusArkRun({ present: true, mode, extraMergeTeeth, residual });
|
|
355
|
+
})();
|
|
356
|
+
|
|
343
357
|
// DF02 — always project compass with honesty mode (never invent green residual).
|
|
344
358
|
// Prefer explicit override (tests/MCP inject doctor-facts); else report snapshot.
|
|
345
359
|
let improvementCompass = null;
|
|
@@ -388,6 +402,7 @@ export function collectStatusFacts(options = {}) {
|
|
|
388
402
|
latest?.leftoverDesignWork === true ||
|
|
389
403
|
latest?.designFitness?.designWeak === true ||
|
|
390
404
|
latest?.doctor?.designFitness?.designWeak === true,
|
|
405
|
+
arkRun: options.arkRun ?? arkRun,
|
|
391
406
|
adopted:
|
|
392
407
|
options.adopted ??
|
|
393
408
|
classifyAdopted({
|
|
@@ -506,6 +521,19 @@ export function runStatusCommand(args = {}) {
|
|
|
506
521
|
);
|
|
507
522
|
}
|
|
508
523
|
if (manifest.vsBase?.line) write(` ${manifest.vsBase.line}`);
|
|
524
|
+
const arkRunLine = manifest.arkRun;
|
|
525
|
+
if (arkRunLine && arkRunLine.notAScore === true) {
|
|
526
|
+
const residual =
|
|
527
|
+
arkRunLine.residual == null ? 'unknown' : String(arkRunLine.residual);
|
|
528
|
+
write(
|
|
529
|
+
` arkRun: ${arkRunLine.present ? arkRunLine.mode || 'on' : 'absent'}` +
|
|
530
|
+
` · residual=${residual}` +
|
|
531
|
+
(arkRunLine.present
|
|
532
|
+
? ` · extraMergeTeeth=${arkRunLine.extraMergeTeeth === true}`
|
|
533
|
+
: '') +
|
|
534
|
+
' · not a score'
|
|
535
|
+
);
|
|
536
|
+
}
|
|
509
537
|
write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
|
|
510
538
|
}
|
|
511
539
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/status-manifest.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { projectStatusArkRun } from './ark-run-doctor.mjs';
|
|
11
12
|
export const ARK_STATUS_MANIFEST_SCHEMA_VERSION = '1.0';
|
|
12
13
|
export const ARK_STATUS_MANIFEST_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.status-manifest.schema.json';
|
|
13
14
|
/**
|
|
@@ -242,6 +243,12 @@ export function resolveStatusNextAction(facts, binding, activation, lastCheck, r
|
|
|
242
243
|
summary: 'ArkRules residual remains frozen — review inventory debt without claiming a score.',
|
|
243
244
|
};
|
|
244
245
|
}
|
|
246
|
+
if (facts.arkRun?.present === true && (facts.arkRun.residual ?? 0) > 0) {
|
|
247
|
+
return {
|
|
248
|
+
id: 'review-arkrun-residual',
|
|
249
|
+
summary: 'ArkRun residual remains — wire kernel usage or declarations through @arkgate/runtime. Not a score.',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
245
252
|
if (facts.adopted === 'required-merge' || facts.adopted === 'advisory-only-acked') {
|
|
246
253
|
return {
|
|
247
254
|
id: 'stay-enforced',
|
|
@@ -326,6 +333,9 @@ export function buildStatusManifest(facts) {
|
|
|
326
333
|
if (facts.vsBase && typeof facts.vsBase.baseRef === 'string' && facts.vsBase.baseRef.length > 0) {
|
|
327
334
|
status.vsBase = facts.vsBase;
|
|
328
335
|
}
|
|
336
|
+
if (facts.arkRun && typeof facts.arkRun === 'object') {
|
|
337
|
+
status.arkRun = projectStatusArkRun(facts.arkRun);
|
|
338
|
+
}
|
|
329
339
|
return status;
|
|
330
340
|
}
|
|
331
341
|
const STATUS_COMPASS_MODE_SET = new Set(STATUS_COMPASS_MODES);
|
|
@@ -605,5 +615,18 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
605
615
|
baselineGrew: { type: 'boolean' },
|
|
606
616
|
},
|
|
607
617
|
},
|
|
618
|
+
arkRun: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
description: 'ArkRun extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
|
|
621
|
+
additionalProperties: false,
|
|
622
|
+
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
623
|
+
properties: {
|
|
624
|
+
notAScore: { const: true },
|
|
625
|
+
present: { type: 'boolean' },
|
|
626
|
+
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
627
|
+
extraMergeTeeth: { type: 'boolean' },
|
|
628
|
+
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
629
|
+
},
|
|
630
|
+
},
|
|
608
631
|
},
|
|
609
632
|
};
|
|
@@ -34,10 +34,14 @@ export {
|
|
|
34
34
|
suggestStewards,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/** Kill hung git instead of stalling CI. */
|
|
38
|
+
export const SPAWN_TIMEOUT_MS = 8000;
|
|
39
|
+
|
|
37
40
|
function runGit(cwd, args) {
|
|
38
41
|
return spawnSync('git', ['-C', cwd, ...args], {
|
|
39
42
|
encoding: 'utf8',
|
|
40
43
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
47
|
|