arkgate 4.4.0 → 4.5.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 +47 -2
- package/README.md +5 -4
- package/bin/ark-check-runtime.mjs +34 -13
- package/bin/ark-layer-match.mjs +25 -12
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/html-report.mjs +8 -1
- package/bin/lib/improvement-compass-map.mjs +507 -0
- package/bin/lib/improvement-compass-types.mjs +85 -0
- package/bin/lib/improvement-compass.mjs +10 -561
- package/bin/lib/managed-upgrade-honesty.mjs +201 -0
- package/bin/lib/managed-upgrade.mjs +37 -4
- package/bin/lib/status-command.mjs +127 -2
- package/bin/lib/status-manifest.mjs +163 -14
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +33 -33
- package/dist/index.d.ts +126 -21
- package/dist/index.js +33 -33
- package/docs/README.md +5 -5
- package/docs/agent-guide.md +35 -8
- package/docs/develop.md +3 -1
- package/docs/package-surface.md +7 -6
- package/docs/product-voice.md +4 -1
- package/docs/use.md +30 -0
- package/package.json +1 -1
- package/schemas/ark.status-manifest.schema.json +28 -1
- package/server.json +2 -2
|
@@ -8,571 +8,20 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/improvement-compass.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'soc',
|
|
15
|
-
'cohesion',
|
|
16
|
-
'coupling',
|
|
17
|
-
'srp',
|
|
18
|
-
'dip',
|
|
19
|
-
'ocp',
|
|
20
|
-
'encapsulation',
|
|
21
|
-
'modularity',
|
|
22
|
-
'scalability',
|
|
23
|
-
'resilience',
|
|
24
|
-
'security',
|
|
25
|
-
'maintainability',
|
|
26
|
-
'testability',
|
|
27
|
-
'domain',
|
|
28
|
-
'stack',
|
|
29
|
-
];
|
|
30
|
-
/** Cap for topResidual — short, agent-legible list (not a ranking score). */
|
|
31
|
-
export const IMPROVEMENT_COMPASS_TOP_RESIDUAL_CAP = 5;
|
|
32
|
-
/** Locked out-of-scope — never become residual from missing sensors. */
|
|
33
|
-
export const IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES = [
|
|
34
|
-
'scalability',
|
|
35
|
-
'resilience',
|
|
36
|
-
'security',
|
|
37
|
-
];
|
|
38
|
-
const OUT_OF_SCOPE_SET = new Set(IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES);
|
|
39
|
-
/**
|
|
40
|
-
* Residual sort priority (lower = earlier in topResidual). Product relevance,
|
|
41
|
-
* not a health score. Ties break by id.
|
|
42
|
-
*/
|
|
43
|
-
const RESIDUAL_SORT_PRIORITY = {
|
|
44
|
-
soc: 10,
|
|
45
|
-
coupling: 20,
|
|
46
|
-
dip: 30,
|
|
47
|
-
domain: 40,
|
|
48
|
-
srp: 50,
|
|
49
|
-
cohesion: 60,
|
|
50
|
-
encapsulation: 70,
|
|
51
|
-
modularity: 80,
|
|
52
|
-
testability: 90,
|
|
53
|
-
maintainability: 100,
|
|
54
|
-
ocp: 110,
|
|
55
|
-
stack: 120,
|
|
56
|
-
scalability: 200,
|
|
57
|
-
resilience: 200,
|
|
58
|
-
security: 200,
|
|
59
|
-
};
|
|
60
|
-
const LENS_LABELS = {
|
|
61
|
-
soc: 'Separation of concerns',
|
|
62
|
-
cohesion: 'High cohesion',
|
|
63
|
-
coupling: 'Low coupling',
|
|
64
|
-
srp: 'Single responsibility (architecture)',
|
|
65
|
-
dip: 'Dependency inversion',
|
|
66
|
-
ocp: 'Open/closed',
|
|
67
|
-
encapsulation: 'Encapsulation',
|
|
68
|
-
modularity: 'Modularity',
|
|
69
|
-
scalability: 'Scalability / performance',
|
|
70
|
-
resilience: 'Resilience / fault tolerance',
|
|
71
|
-
security: 'Security by design',
|
|
72
|
-
maintainability: 'Maintainability',
|
|
73
|
-
testability: 'Testability',
|
|
74
|
-
domain: 'Domain alignment',
|
|
75
|
-
stack: 'Stack-specific practices',
|
|
76
|
-
};
|
|
77
|
-
const OUT_OF_SCOPE_SUMMARIES = {
|
|
78
|
-
scalability: 'ArkGate does not measure performance or horizontal scale. Use load tests and APM outside Ark.',
|
|
79
|
-
resilience: 'ArkGate does not measure app resilience or chaos readiness. Structural boundaries and optional experimental runtime are not a resilience score.',
|
|
80
|
-
security: 'ArkGate does not run SAST or app-security tooling. Structural least-privilege of effects is partial only — not a security rating.',
|
|
81
|
-
};
|
|
82
|
-
function smellIdOf(smell) {
|
|
83
|
-
const raw = smell.id ?? smell.smellId ?? '';
|
|
84
|
-
return typeof raw === 'string' ? raw.trim() : '';
|
|
85
|
-
}
|
|
86
|
-
function violationRuleId(v) {
|
|
87
|
-
const raw = v.ruleId ?? v.code ?? '';
|
|
88
|
-
return typeof raw === 'string' ? raw.trim() : '';
|
|
89
|
-
}
|
|
90
|
-
function pushEvidence(lens, source, ref, detail) {
|
|
91
|
-
if (!ref)
|
|
92
|
-
return;
|
|
93
|
-
// Dedup by source+ref for deterministic stability.
|
|
94
|
-
if (lens.evidence.some((e) => e.source === source && e.ref === ref))
|
|
95
|
-
return;
|
|
96
|
-
const entry = { source, ref };
|
|
97
|
-
if (detail && detail.trim())
|
|
98
|
-
entry.detail = detail.trim().slice(0, 240);
|
|
99
|
-
lens.evidence.push(entry);
|
|
100
|
-
}
|
|
101
|
-
function markResidual(lens, summary, nextAction) {
|
|
102
|
-
if (OUT_OF_SCOPE_SET.has(lens.id))
|
|
103
|
-
return;
|
|
104
|
-
lens.status = 'residual';
|
|
105
|
-
lens.summary = summary;
|
|
106
|
-
if (nextAction)
|
|
107
|
-
lens.nextAction = nextAction;
|
|
108
|
-
}
|
|
109
|
-
function defaultOkSummary(id) {
|
|
110
|
-
switch (id) {
|
|
111
|
-
case 'soc':
|
|
112
|
-
return 'No separation-of-concerns residual detected from current sensors.';
|
|
113
|
-
case 'cohesion':
|
|
114
|
-
return 'No cohesion residual (god-module / physical cohesion) from current sensors.';
|
|
115
|
-
case 'coupling':
|
|
116
|
-
return 'No coupling residual (import edges, cycles, peer isolation) from current sensors.';
|
|
117
|
-
case 'srp':
|
|
118
|
-
return 'No single-responsibility residual from current sensors.';
|
|
119
|
-
case 'dip':
|
|
120
|
-
return 'No dependency-inversion residual (pure / capability / forbidden walls) from current sensors.';
|
|
121
|
-
case 'ocp':
|
|
122
|
-
return 'Open/closed is not strongly instrumented — no switch-chain sensor.';
|
|
123
|
-
case 'encapsulation':
|
|
124
|
-
return 'No encapsulation residual from ArkRules structure sensors.';
|
|
125
|
-
case 'modularity':
|
|
126
|
-
return 'No modularity / placement residual from current sensors.';
|
|
127
|
-
case 'maintainability':
|
|
128
|
-
return 'No maintainability residual (design-weak / baseline honesty) from current sensors.';
|
|
129
|
-
case 'testability':
|
|
130
|
-
return 'No testability residual (impure domain / capability walls) from current sensors.';
|
|
131
|
-
case 'domain':
|
|
132
|
-
return 'No domain-alignment residual from current sensors.';
|
|
133
|
-
case 'stack':
|
|
134
|
-
return 'Stack practices are only partially instrumented (TypeScript / host / Ark idioms).';
|
|
135
|
-
default:
|
|
136
|
-
return `${LENS_LABELS[id]} — no residual from current sensors.`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function initLenses() {
|
|
140
|
-
return IMPROVEMENT_LENS_IDS.map((id) => {
|
|
141
|
-
if (OUT_OF_SCOPE_SET.has(id)) {
|
|
142
|
-
const key = id;
|
|
143
|
-
return {
|
|
144
|
-
id,
|
|
145
|
-
status: 'out-of-scope',
|
|
146
|
-
summary: OUT_OF_SCOPE_SUMMARIES[key],
|
|
147
|
-
evidence: [],
|
|
148
|
-
nextAction: {
|
|
149
|
-
kind: 'docs',
|
|
150
|
-
ref: 'docs/use.md#improvement-compass',
|
|
151
|
-
summary: 'Out of scope for ArkGate — use dedicated tooling outside the gate.',
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
if (id === 'ocp') {
|
|
156
|
-
return {
|
|
157
|
-
id,
|
|
158
|
-
status: 'not-instrumented',
|
|
159
|
-
summary: defaultOkSummary(id),
|
|
160
|
-
evidence: [],
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
id,
|
|
165
|
-
status: 'ok',
|
|
166
|
-
summary: defaultOkSummary(id),
|
|
167
|
-
evidence: [],
|
|
168
|
-
};
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
function mapDesignSmells(byId, smells) {
|
|
172
|
-
for (const smell of smells) {
|
|
173
|
-
const id = smellIdOf(smell);
|
|
174
|
-
if (!id)
|
|
175
|
-
continue;
|
|
176
|
-
const detail = smell.outcome || smell.message || undefined;
|
|
177
|
-
const evidencePaths = Array.isArray(smell.evidence) ? smell.evidence : [];
|
|
178
|
-
const pathHint = evidencePaths[0];
|
|
179
|
-
const attach = (lensId, summary, action) => {
|
|
180
|
-
const lens = byId.get(lensId);
|
|
181
|
-
if (!lens || OUT_OF_SCOPE_SET.has(lensId))
|
|
182
|
-
return;
|
|
183
|
-
pushEvidence(lens, 'designSmells', id, detail);
|
|
184
|
-
if (pathHint)
|
|
185
|
-
pushEvidence(lens, 'designSmells', pathHint, id);
|
|
186
|
-
markResidual(lens, summary, action);
|
|
187
|
-
};
|
|
188
|
-
const shapeAction = {
|
|
189
|
-
kind: 'skill',
|
|
190
|
-
ref: '/ark-explore',
|
|
191
|
-
summary: 'Map Shape residual (shape-focus), then one extraction pilot with user OK.',
|
|
192
|
-
};
|
|
193
|
-
const dipAction = {
|
|
194
|
-
kind: 'skill',
|
|
195
|
-
ref: '/ark-fix',
|
|
196
|
-
summary: 'Inject a port/adapter for I/O; keep domain pure.',
|
|
197
|
-
};
|
|
198
|
-
switch (id) {
|
|
199
|
-
case 'domain-logic-in-ui':
|
|
200
|
-
attach('soc', 'Business rules still mix with UI or presentation surfaces.', shapeAction);
|
|
201
|
-
attach('domain', 'Domain logic lives outside Domain — align rules with Domain ownership.', shapeAction);
|
|
202
|
-
break;
|
|
203
|
-
case 'facade-sql-in-routes':
|
|
204
|
-
attach('soc', 'Routes/controllers own SQL or ORM access — concerns are mixed.', shapeAction);
|
|
205
|
-
attach('dip', 'Transport depends on concrete persistence instead of a port.', dipAction);
|
|
206
|
-
break;
|
|
207
|
-
case 'io-under-application':
|
|
208
|
-
attach('soc', 'Application/business code reaches I/O directly — separation is weak.', shapeAction);
|
|
209
|
-
attach('dip', 'I/O is not inverted behind ports/adapters.', dipAction);
|
|
210
|
-
attach('testability', 'Direct I/O under application code hurts pure unit testing.', dipAction);
|
|
211
|
-
break;
|
|
212
|
-
case 'handler-in-persistence':
|
|
213
|
-
attach('soc', 'HTTP/transport handlers live under persistence folders.', shapeAction);
|
|
214
|
-
break;
|
|
215
|
-
case 'god-module':
|
|
216
|
-
attach('cohesion', 'Large multi-responsibility modules reduce cohesion.', shapeAction);
|
|
217
|
-
attach('srp', 'God modules own too many responsibilities — split by concern (one pilot).', {
|
|
218
|
-
kind: 'skill',
|
|
219
|
-
ref: '/ark-autopilot',
|
|
220
|
-
summary: 'One Shape pilot with user OK — never multi-pilot batch.',
|
|
221
|
-
});
|
|
222
|
-
break;
|
|
223
|
-
case 'mixed-pattern-cluster':
|
|
224
|
-
attach('modularity', 'Multiple layout styles coexist — placement is unclear for the next AI turn.', {
|
|
225
|
-
kind: 'skill',
|
|
226
|
-
ref: '/ark-explore',
|
|
227
|
-
summary: 'Pick a golden pattern and migrate one pilot cluster on touch.',
|
|
228
|
-
});
|
|
229
|
-
attach('cohesion', 'Mixed layout styles scatter the same concern across patterns.', shapeAction);
|
|
230
|
-
break;
|
|
231
|
-
case 'soft-contract':
|
|
232
|
-
attach('maintainability', 'Soft contract walls (layers without deny rules) hide maintainability debt.', {
|
|
233
|
-
kind: 'skill',
|
|
234
|
-
ref: '/ark-contract',
|
|
235
|
-
summary: 'Add real layer rules so the AI has hard walls.',
|
|
236
|
-
});
|
|
237
|
-
attach('coupling', 'Layers with files but almost no deny rules allow free peer coupling.', {
|
|
238
|
-
kind: 'skill',
|
|
239
|
-
ref: '/ark-contract',
|
|
240
|
-
summary: 'Tighten inter-layer allows/denies without weakening enforcement.',
|
|
241
|
-
});
|
|
242
|
-
break;
|
|
243
|
-
default:
|
|
244
|
-
// Unknown smell ids still feed maintainability residual (honest residual,
|
|
245
|
-
// not out-of-scope invention).
|
|
246
|
-
attach('maintainability', 'Design residual remains under an unrecognized smell id — review evidence.', shapeAction);
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function isTypeOnlyPlacementDebt(v) {
|
|
252
|
-
// Product voice: type-only edges are placement debt (failsStrict:false), not runtime coupling.
|
|
253
|
-
if (v.failsStrict === false)
|
|
254
|
-
return true;
|
|
255
|
-
if (v.typeOnly === true)
|
|
256
|
-
return true;
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
function mapViolations(byId, violations) {
|
|
260
|
-
for (const v of violations) {
|
|
261
|
-
const ruleId = violationRuleId(v);
|
|
262
|
-
if (!ruleId)
|
|
263
|
-
continue;
|
|
264
|
-
const detail = v.message;
|
|
265
|
-
const upper = ruleId.toUpperCase();
|
|
266
|
-
const attach = (lensId, summary, action) => {
|
|
267
|
-
const lens = byId.get(lensId);
|
|
268
|
-
if (!lens || OUT_OF_SCOPE_SET.has(lensId))
|
|
269
|
-
return;
|
|
270
|
-
pushEvidence(lens, 'violations', ruleId, detail);
|
|
271
|
-
if (v.file)
|
|
272
|
-
pushEvidence(lens, 'violations', v.file, ruleId);
|
|
273
|
-
markResidual(lens, summary, action);
|
|
274
|
-
};
|
|
275
|
-
// Type-only / non-blocking placement debt → modularity only (never coupling / DIP residual).
|
|
276
|
-
if (isTypeOnlyPlacementDebt(v)) {
|
|
277
|
-
attach('modularity', 'Type-only placement debt remains — prefer SharedTypes / owning layer (not runtime coupling).', {
|
|
278
|
-
kind: 'skill',
|
|
279
|
-
ref: '/ark-place',
|
|
280
|
-
summary: 'Place shared types in a layer both sides may import; type-only debt is not a value edge.',
|
|
281
|
-
});
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
const edgeAction = {
|
|
285
|
-
kind: 'skill',
|
|
286
|
-
ref: '/ark-fix',
|
|
287
|
-
summary: 'Clear the active edge residual, then re-doctor.',
|
|
288
|
-
};
|
|
289
|
-
if (upper === 'LAYER_IMPORT_VIOLATION' ||
|
|
290
|
-
upper.includes('LAYER_IMPORT') ||
|
|
291
|
-
upper === 'DYNAMIC_IMPORT_VIOLATION') {
|
|
292
|
-
attach('coupling', 'Import graph edges violate the layer contract.', edgeAction);
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
if (upper.includes('CYCLE') || upper === 'CIRCULAR_DEPENDENCY') {
|
|
296
|
-
attach('coupling', 'Import cycles couple modules tightly.', edgeAction);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (upper.includes('PEER_ISOLATION') || upper === 'PEER_ISOLATION_VIOLATION') {
|
|
300
|
-
attach('coupling', 'Peer isolation residual — slices import each other freely.', {
|
|
301
|
-
kind: 'skill',
|
|
302
|
-
ref: '/ark-loop',
|
|
303
|
-
summary: 'Peer isolation fixes are judgment-class — one cluster at a time.',
|
|
304
|
-
});
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (upper === 'FORBIDDEN_GLOBAL' || upper.startsWith('FORBIDDEN_')) {
|
|
308
|
-
attach('dip', 'Forbidden globals / effect surfaces break dependency inversion.', {
|
|
309
|
-
kind: 'skill',
|
|
310
|
-
ref: '/ark-fix',
|
|
311
|
-
summary: 'Inject a port instead of the forbidden global.',
|
|
312
|
-
});
|
|
313
|
-
attach('testability', 'Forbidden ambient effects reduce pure-domain testability.', {
|
|
314
|
-
kind: 'skill',
|
|
315
|
-
ref: '/ark-fix',
|
|
316
|
-
summary: 'Replace ambient effects with injectable ports.',
|
|
317
|
-
});
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (upper === 'CAPABILITY_VIOLATION') {
|
|
321
|
-
attach('dip', 'Denied capability use — invert through an allowed adapter/port.', {
|
|
322
|
-
kind: 'skill',
|
|
323
|
-
ref: '/ark-fix',
|
|
324
|
-
summary: 'Capability walls require port injection (judgment, not mechanical-safe).',
|
|
325
|
-
});
|
|
326
|
-
attach('testability', 'Capability violations couple domain code to I/O — harder to unit-test.', {
|
|
327
|
-
kind: 'skill',
|
|
328
|
-
ref: '/ark-fix',
|
|
329
|
-
summary: 'Keep pure layers free of denied capabilities.',
|
|
330
|
-
});
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (upper.startsWith('ARKRULE_') || upper === 'INVARIANT_UNCOVERED') {
|
|
334
|
-
attach('encapsulation', 'ArkRules structure / invariant residual inside a layer.', {
|
|
335
|
-
kind: 'skill',
|
|
336
|
-
ref: '/ark-fix',
|
|
337
|
-
summary: 'Label [ArkRules]; structure fixes are judgment — never invent mechanical-safe.',
|
|
338
|
-
});
|
|
339
|
-
attach('domain', 'Intra-layer domain structure or invariant coverage residual.', {
|
|
340
|
-
kind: 'skill',
|
|
341
|
-
ref: '/ark-explore',
|
|
342
|
-
summary: 'Inventory candidates → one ArkRules pilot with coverage evidence.',
|
|
343
|
-
});
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
function mapCountsAndFlags(byId, facts) {
|
|
349
|
-
const cycleCount = Number(facts.cycleCount) || 0;
|
|
350
|
-
if (cycleCount > 0) {
|
|
351
|
-
const lens = byId.get('coupling');
|
|
352
|
-
pushEvidence(lens, 'cycles', `count:${cycleCount}`);
|
|
353
|
-
markResidual(lens, 'Import cycles couple modules tightly.', {
|
|
354
|
-
kind: 'skill',
|
|
355
|
-
ref: '/ark-fix',
|
|
356
|
-
summary: 'Break cycles with a judgment extraction — one pilot.',
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
const peer = typeof facts.peerIsolationCount === 'boolean'
|
|
360
|
-
? facts.peerIsolationCount
|
|
361
|
-
? 1
|
|
362
|
-
: 0
|
|
363
|
-
: Number(facts.peerIsolationCount) || 0;
|
|
364
|
-
if (peer > 0) {
|
|
365
|
-
const lens = byId.get('coupling');
|
|
366
|
-
pushEvidence(lens, 'peerIsolation', `count:${peer}`);
|
|
367
|
-
markResidual(lens, 'Peer isolation residual remains.', {
|
|
368
|
-
kind: 'skill',
|
|
369
|
-
ref: '/ark-loop',
|
|
370
|
-
summary: 'Peer isolation is judgment-class residual.',
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
const pc = Number(facts.physicalCohesionFindingCount) || 0;
|
|
374
|
-
if (pc > 0) {
|
|
375
|
-
const cohesion = byId.get('cohesion');
|
|
376
|
-
pushEvidence(cohesion, 'physicalCohesion', `findings:${pc}`);
|
|
377
|
-
markResidual(cohesion, 'Physical cohesion residual — mirrored concept clusters across anchors.', {
|
|
378
|
-
kind: 'skill',
|
|
379
|
-
ref: '/ark-explore',
|
|
380
|
-
summary: 'Review reshape pilot; one decision-aware pilot at a time.',
|
|
381
|
-
});
|
|
382
|
-
const srp = byId.get('srp');
|
|
383
|
-
pushEvidence(srp, 'physicalCohesion', `findings:${pc}`);
|
|
384
|
-
markResidual(srp, 'Mirrored clusters suggest split-by-concern residual (architecture SRP).', {
|
|
385
|
-
kind: 'skill',
|
|
386
|
-
ref: '/ark-autopilot',
|
|
387
|
-
summary: 'One reshape/extraction pilot with user OK.',
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
const pureN = Number(facts.pureOrCapabilityResidual) || 0;
|
|
391
|
-
const fgN = Number(facts.forbiddenGlobalResidual) || 0;
|
|
392
|
-
if (pureN > 0 || fgN > 0) {
|
|
393
|
-
const dip = byId.get('dip');
|
|
394
|
-
if (pureN > 0)
|
|
395
|
-
pushEvidence(dip, 'capability', `residual:${pureN}`);
|
|
396
|
-
if (fgN > 0)
|
|
397
|
-
pushEvidence(dip, 'forbiddenGlobals', `residual:${fgN}`);
|
|
398
|
-
markResidual(dip, 'Pure / capability / forbidden residual weakens dependency inversion.', {
|
|
399
|
-
kind: 'skill',
|
|
400
|
-
ref: '/ark-fix',
|
|
401
|
-
summary: 'Inject ports; keep pure layers free of effects.',
|
|
402
|
-
});
|
|
403
|
-
const test = byId.get('testability');
|
|
404
|
-
if (pureN > 0)
|
|
405
|
-
pushEvidence(test, 'capability', `residual:${pureN}`);
|
|
406
|
-
if (fgN > 0)
|
|
407
|
-
pushEvidence(test, 'forbiddenGlobals', `residual:${fgN}`);
|
|
408
|
-
markResidual(test, 'Impure domain or capability residual reduces testability.', {
|
|
409
|
-
kind: 'skill',
|
|
410
|
-
ref: '/ark-fix',
|
|
411
|
-
summary: 'Prefer ports over concrete I/O in pure/domain modules.',
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
const arkN = Number(facts.arkRulesStructureResidual) || 0;
|
|
415
|
-
if (arkN > 0) {
|
|
416
|
-
const enc = byId.get('encapsulation');
|
|
417
|
-
pushEvidence(enc, 'arkRules', `structureResidual:${arkN}`);
|
|
418
|
-
markResidual(enc, 'ArkRules structure residual — encapsulation inside the layer.', {
|
|
419
|
-
kind: 'skill',
|
|
420
|
-
ref: '/ark-fix',
|
|
421
|
-
summary: 'Fix structure sensors under [ArkRules] without inventing mechanical-safe.',
|
|
422
|
-
});
|
|
423
|
-
const domain = byId.get('domain');
|
|
424
|
-
pushEvidence(domain, 'arkRules', `structureResidual:${arkN}`);
|
|
425
|
-
markResidual(domain, 'ArkRules residual may mean domain shape is not yet under contract.', {
|
|
426
|
-
kind: 'skill',
|
|
427
|
-
ref: '/ark-explore',
|
|
428
|
-
summary: 'Map inventory candidates; one pilot rule at a time.',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
else if (facts.arkRulesLoaded === false || facts.arkRulesLoaded == null) {
|
|
432
|
-
// No ArkRules → encapsulation stays ok (absence is valid), not residual.
|
|
433
|
-
// Domain remains ok unless other evidence marked it.
|
|
434
|
-
}
|
|
435
|
-
if (facts.designWeak === true) {
|
|
436
|
-
const m = byId.get('maintainability');
|
|
437
|
-
pushEvidence(m, 'designFitness', 'design-weak');
|
|
438
|
-
markResidual(m, 'Design-weak: checked edges may be clean, but design residual remains — not finished.', {
|
|
439
|
-
kind: 'skill',
|
|
440
|
-
ref: '/ark-explore',
|
|
441
|
-
summary: 'Shape door: explore shape-focus → dual-plan B → one pilot with OK.',
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
if (facts.dirtyBaselineRisk === true || (Number(facts.baselineStale) || 0) > 0) {
|
|
445
|
-
const m = byId.get('maintainability');
|
|
446
|
-
if (facts.dirtyBaselineRisk === true) {
|
|
447
|
-
pushEvidence(m, 'baseline', 'dirty-freeze-risk');
|
|
448
|
-
}
|
|
449
|
-
if ((Number(facts.baselineStale) || 0) > 0) {
|
|
450
|
-
pushEvidence(m, 'baseline', `stale:${facts.baselineStale}`);
|
|
451
|
-
}
|
|
452
|
-
markResidual(m, 'Baseline honesty residual — frozen debt or stale keys need review.', {
|
|
453
|
-
kind: 'command',
|
|
454
|
-
ref: 'ark-check --doctor',
|
|
455
|
-
summary: 'Review baseline freeze honesty; do not freeze new wrong debt.',
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
// Large frozen residual (baseline exists with many freezes) is maintainability debt —
|
|
459
|
-
// only when there is already a residual signal or a substantial freeze surface.
|
|
460
|
-
const frozenN = Number(facts.frozenResidual) || 0;
|
|
461
|
-
if (facts.baselineExists === true && frozenN >= 10 && byId.get('maintainability').status !== 'residual') {
|
|
462
|
-
const m = byId.get('maintainability');
|
|
463
|
-
pushEvidence(m, 'baseline', `frozen:${frozenN}`);
|
|
464
|
-
markResidual(m, 'Substantial frozen residual remains under the baseline — review debt honestly.', {
|
|
465
|
-
kind: 'command',
|
|
466
|
-
ref: 'ark-check --doctor',
|
|
467
|
-
summary: 'Review freezes; do not freeze new wrong debt to clear residual.',
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
const ungov = Number(facts.ungovernedDirCount) || 0;
|
|
471
|
-
const emptyL = Number(facts.emptyLayerCount) || 0;
|
|
472
|
-
if (ungov > 0 || emptyL > 0) {
|
|
473
|
-
const mod = byId.get('modularity');
|
|
474
|
-
if (ungov > 0)
|
|
475
|
-
pushEvidence(mod, 'coverage', `ungovernedDirs:${ungov}`);
|
|
476
|
-
if (emptyL > 0)
|
|
477
|
-
pushEvidence(mod, 'coverage', `emptyLayers:${emptyL}`);
|
|
478
|
-
markResidual(mod, 'Placement / modularity residual — ungoverned dirs or empty layer globs.', {
|
|
479
|
-
kind: 'skill',
|
|
480
|
-
ref: '/ark-contract',
|
|
481
|
-
summary: 'Classify ungoverned paths; fix empty layer patterns.',
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
// Missing golden pattern under design-weak → modularity residual (AI placement cue).
|
|
485
|
-
if (facts.designWeak === true && facts.goldenPatternPresent === false) {
|
|
486
|
-
const mod = byId.get('modularity');
|
|
487
|
-
pushEvidence(mod, 'goldenPattern', 'absent');
|
|
488
|
-
markResidual(mod, 'Design-weak without a golden pattern — new code lacks a placement norm for the AI.', {
|
|
489
|
-
kind: 'skill',
|
|
490
|
-
ref: '/ark-place',
|
|
491
|
-
summary: 'Record an advisory golden pattern for new code (does not clear design-weak).',
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
// Stack: TypeScript host partially instrumented; unknown → not-instrumented.
|
|
495
|
-
const stack = byId.get('stack');
|
|
496
|
-
const kind = facts.stackKind ?? null;
|
|
497
|
-
if (kind === 'typescript') {
|
|
498
|
-
// Partial instrumentation is still honest `ok` when no residual evidence.
|
|
499
|
-
if (stack.status === 'ok') {
|
|
500
|
-
stack.summary =
|
|
501
|
-
'Stack practices are partially instrumented for TypeScript / host / Ark idioms only — not a full framework checklist.';
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
stack.status = 'not-instrumented';
|
|
506
|
-
stack.summary =
|
|
507
|
-
'Stack-specific best practices outside TypeScript/host/Ark idioms are not instrumented.';
|
|
508
|
-
stack.evidence = [];
|
|
509
|
-
stack.nextAction = {
|
|
510
|
-
kind: 'docs',
|
|
511
|
-
ref: 'docs/use.md#improvement-compass',
|
|
512
|
-
summary: 'Ark does not score non-TS stack idioms.',
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
function finalizeTopResidual(lenses) {
|
|
517
|
-
const residual = lenses
|
|
518
|
-
.filter((l) => l.status === 'residual' && !OUT_OF_SCOPE_SET.has(l.id))
|
|
519
|
-
.slice()
|
|
520
|
-
.sort((a, b) => {
|
|
521
|
-
const pa = RESIDUAL_SORT_PRIORITY[a.id] ?? 150;
|
|
522
|
-
const pb = RESIDUAL_SORT_PRIORITY[b.id] ?? 150;
|
|
523
|
-
if (pa !== pb)
|
|
524
|
-
return pa - pb;
|
|
525
|
-
return a.id.localeCompare(b.id);
|
|
526
|
-
});
|
|
527
|
-
return residual.slice(0, IMPROVEMENT_COMPASS_TOP_RESIDUAL_CAP).map((l) => l.id);
|
|
528
|
-
}
|
|
529
|
-
function humanLabel(id) {
|
|
530
|
-
return LENS_LABELS[id] ?? id;
|
|
531
|
-
}
|
|
11
|
+
export { ARK_IMPROVEMENT_COMPASS_SCHEMA_VERSION, IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES, IMPROVEMENT_COMPASS_TOP_RESIDUAL_CAP, IMPROVEMENT_LENS_IDS, } from './improvement-compass-types.mjs';
|
|
12
|
+
import { ARK_IMPROVEMENT_COMPASS_SCHEMA_VERSION, IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES, improvementCompassHumanLabel, } from './improvement-compass-types.mjs';
|
|
13
|
+
import { createInitialImprovementCompassLenses, finalizeImprovementCompassTopResidual, lockImprovementCompassOutOfScope, projectImprovementCompassFacts, sortImprovementCompassEvidence, } from './improvement-compass-map.mjs';
|
|
532
14
|
/**
|
|
533
15
|
* Build a deterministic improvement compass from supplied doctor-side facts.
|
|
534
16
|
* Always returns all 15 lenses; always `notAScore: true`.
|
|
535
17
|
*/
|
|
536
18
|
export function buildImprovementCompass(facts = {}) {
|
|
537
|
-
const lenses =
|
|
19
|
+
const lenses = createInitialImprovementCompassLenses();
|
|
538
20
|
const byId = new Map(lenses.map((l) => [l.id, l]));
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
if (Array.isArray(facts.violations) && facts.violations.length > 0) {
|
|
545
|
-
const violations = [...facts.violations].sort((a, b) => {
|
|
546
|
-
const ra = violationRuleId(a).localeCompare(violationRuleId(b));
|
|
547
|
-
if (ra !== 0)
|
|
548
|
-
return ra;
|
|
549
|
-
return String(a.file ?? '').localeCompare(String(b.file ?? ''));
|
|
550
|
-
});
|
|
551
|
-
mapViolations(byId, violations);
|
|
552
|
-
}
|
|
553
|
-
mapCountsAndFlags(byId, facts);
|
|
554
|
-
// Hard lock: out-of-scope can never be residual, even if bad facts arrive.
|
|
555
|
-
for (const id of IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES) {
|
|
556
|
-
const lens = byId.get(id);
|
|
557
|
-
lens.status = 'out-of-scope';
|
|
558
|
-
lens.summary = OUT_OF_SCOPE_SUMMARIES[id];
|
|
559
|
-
lens.evidence = [];
|
|
560
|
-
lens.nextAction = {
|
|
561
|
-
kind: 'docs',
|
|
562
|
-
ref: 'docs/use.md#improvement-compass',
|
|
563
|
-
summary: 'Out of scope for ArkGate — use dedicated tooling outside the gate.',
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
// Stable evidence order per lens (source then ref).
|
|
567
|
-
for (const lens of lenses) {
|
|
568
|
-
lens.evidence.sort((a, b) => {
|
|
569
|
-
const s = a.source.localeCompare(b.source);
|
|
570
|
-
if (s !== 0)
|
|
571
|
-
return s;
|
|
572
|
-
return a.ref.localeCompare(b.ref);
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
const topResidual = finalizeTopResidual(lenses);
|
|
21
|
+
projectImprovementCompassFacts(byId, facts);
|
|
22
|
+
lockImprovementCompassOutOfScope(byId);
|
|
23
|
+
sortImprovementCompassEvidence(lenses);
|
|
24
|
+
const topResidual = finalizeImprovementCompassTopResidual(lenses);
|
|
576
25
|
return {
|
|
577
26
|
schemaVersion: ARK_IMPROVEMENT_COMPASS_SCHEMA_VERSION,
|
|
578
27
|
notAScore: true,
|
|
@@ -595,7 +44,7 @@ export function buildImprovementCompass(facts = {}) {
|
|
|
595
44
|
* Plain residual lens names for human doctor / compact router (never a score).
|
|
596
45
|
*/
|
|
597
46
|
export function formatImprovementCompassResidualLabels(compass) {
|
|
598
|
-
return compass.topResidual.map((id) =>
|
|
47
|
+
return compass.topResidual.map((id) => improvementCompassHumanLabel(id));
|
|
599
48
|
}
|
|
600
49
|
/**
|
|
601
50
|
* Primary next action from the first residual lens that carries one.
|
|
@@ -613,7 +62,7 @@ export function primaryImprovementCompassNextAction(compass) {
|
|
|
613
62
|
*/
|
|
614
63
|
export function formatImprovementCompassDoctorLines(compass) {
|
|
615
64
|
const residual = formatImprovementCompassResidualLabels(compass);
|
|
616
|
-
const outOfScope = IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES.map((id) =>
|
|
65
|
+
const outOfScope = IMPROVEMENT_COMPASS_OUT_OF_SCOPE_LENSES.map((id) => improvementCompassHumanLabel(id));
|
|
617
66
|
const next = primaryImprovementCompassNextAction(compass);
|
|
618
67
|
const lines = [];
|
|
619
68
|
if (residual.length > 0) {
|