arkgate 4.5.0 → 4.5.6
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 +69 -2
- package/README.md +5 -4
- package/bin/ark-check-runtime.mjs +4 -0
- package/bin/ark-mcp-runtime.mjs +49 -0
- package/bin/ark-shared.mjs +8 -29
- package/bin/ark.mjs +7 -3
- package/bin/lib/adapter-contract.mjs +5 -5
- package/bin/lib/analysis-engine.mjs +1 -1
- package/bin/lib/ci-and-commands.mjs +5 -0
- package/bin/lib/deep-module-coach.mjs +177 -0
- package/bin/lib/deepening-coach.mjs +177 -0
- package/bin/lib/doctor-plan.mjs +14 -0
- package/bin/lib/html-report-advisories.mjs +33 -0
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/managed-upgrade.mjs +215 -1
- package/bin/lib/remediation.mjs +5 -5
- package/bin/lib/rules-inventory.mjs +23 -0
- package/bin/lib/upgrade-command.mjs +132 -11
- package/bin/lib/upgrade-package-decision.mjs +241 -0
- package/bin/lib/upgrade-whats-new.mjs +135 -0
- package/dist/eslint/index.cjs +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/index.cjs +27 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +29 -29
- package/docs/README.md +4 -5
- package/docs/agent-guide.md +36 -0
- package/docs/brownfield-adoption.md +12 -0
- package/docs/package-surface.md +6 -3
- package/docs/product-voice.md +21 -0
- package/docs/use.md +3 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +2 -2
- package/templates/agent-skills/ark-adopt/SKILL.md +13 -0
- package/templates/agent-skills/ark-explore/SKILL.md +21 -0
- package/templates/agent-skills/ark-fix/SKILL.md +7 -0
- package/templates/agent-skills/ark-loop/SKILL.md +7 -0
- package/templates/agent-skills/ark-place/SKILL.md +7 -0
- package/templates/agent-skills/ark-think/SKILL.md +7 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +61 -15
- package/templates/skills/ark-adopt.md +13 -0
- package/templates/skills/ark-explore.md +21 -0
- package/templates/skills/ark-fix.md +7 -0
- package/templates/skills/ark-loop.md +7 -0
- package/templates/skills/ark-place.md +7 -0
- package/templates/skills/ark-think.md +7 -0
- package/templates/skills/ark-upgrade.md +61 -15
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/deepeningCoach.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/deepening-coach.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ARK_DEEPENING_COACH_SCHEMA_VERSION = '1.0';
|
|
12
|
+
/** Cap on listed deepening candidates (agent-legible; not a ranking score). */
|
|
13
|
+
export const DEEPENING_CANDIDATE_CAP = 5;
|
|
14
|
+
function asString(value) {
|
|
15
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
16
|
+
}
|
|
17
|
+
function firstEvidencePath(evidence) {
|
|
18
|
+
if (!Array.isArray(evidence))
|
|
19
|
+
return '';
|
|
20
|
+
for (const row of evidence) {
|
|
21
|
+
if (typeof row === 'string' && row.trim())
|
|
22
|
+
return row.trim().replace(/\\/g, '/');
|
|
23
|
+
if (row && typeof row === 'object') {
|
|
24
|
+
const path = asString(row.path)
|
|
25
|
+
|| asString(row.file);
|
|
26
|
+
if (path)
|
|
27
|
+
return path.replace(/\\/g, '/');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
function evidenceList(source, refs, detail) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const ref of refs) {
|
|
35
|
+
const r = asString(ref);
|
|
36
|
+
if (!r)
|
|
37
|
+
continue;
|
|
38
|
+
out.push(detail ? { source, ref: r, detail } : { source, ref: r });
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project deepening candidates from existing doctor-side evidence only.
|
|
44
|
+
* Empty input / no residual → empty `candidates` (honesty: no fake list).
|
|
45
|
+
*/
|
|
46
|
+
export function buildDeepeningCandidates(input = {}) {
|
|
47
|
+
const candidates = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const push = (candidate) => {
|
|
50
|
+
if (candidates.length >= DEEPENING_CANDIDATE_CAP)
|
|
51
|
+
return;
|
|
52
|
+
const key = `${candidate.target}|${candidate.friction}`.toLowerCase();
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
return;
|
|
55
|
+
if (!asString(candidate.target) || !asString(candidate.friction))
|
|
56
|
+
return;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
candidates.push(candidate);
|
|
59
|
+
};
|
|
60
|
+
const pilot = input.pilotLoop?.active === true && input.pilotLoop.nextPilot
|
|
61
|
+
? input.pilotLoop.nextPilot
|
|
62
|
+
: null;
|
|
63
|
+
if (pilot) {
|
|
64
|
+
// Require a real pilot identity — empty shells are not evidence.
|
|
65
|
+
const target = asString(pilot.pilotTarget) || asString(pilot.pilot) || asString(pilot.smellId);
|
|
66
|
+
if (target) {
|
|
67
|
+
push({
|
|
68
|
+
target,
|
|
69
|
+
friction: asString(pilot.smellId)
|
|
70
|
+
? `Shape pilot residual (${pilot.smellId}) — one extraction at a time`
|
|
71
|
+
: 'Shape pilot residual — one extraction at a time',
|
|
72
|
+
intent: asString(pilot.move) ||
|
|
73
|
+
'Deepen the public seam; hide implementation behind a small interface',
|
|
74
|
+
benefit: asString(pilot.successSignal) ||
|
|
75
|
+
'Locality: change and tests concentrate at the public interface',
|
|
76
|
+
evidence: evidenceList('pilotLoop', [target], 'nextPilot'),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const smells = Array.isArray(input.designSmells) ? input.designSmells : [];
|
|
81
|
+
for (const smell of smells) {
|
|
82
|
+
if (!smell || typeof smell !== 'object')
|
|
83
|
+
continue;
|
|
84
|
+
const id = asString(smell.id);
|
|
85
|
+
const path = firstEvidencePath(smell.evidence);
|
|
86
|
+
// Require non-empty smell id or evidence path — whitespace shells are not evidence.
|
|
87
|
+
if (!id && !path)
|
|
88
|
+
continue;
|
|
89
|
+
const target = path || id;
|
|
90
|
+
const friction = asString(smell.outcome) || asString(smell.message) || `Design residual (${id || target})`;
|
|
91
|
+
push({
|
|
92
|
+
target,
|
|
93
|
+
friction,
|
|
94
|
+
intent: asString(smell.fix) ||
|
|
95
|
+
'Prefer a deep module at a named seam; apply the deletion test before pass-through extracts',
|
|
96
|
+
benefit: 'Leverage: callers learn a smaller interface; locality of change improves',
|
|
97
|
+
evidence: evidenceList('designSmells', path ? [path, id].filter(Boolean) : [id], id || undefined),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const pc = input.physicalCohesion;
|
|
101
|
+
const reshape = pc?.reshapePilot?.nextPilot;
|
|
102
|
+
if (reshape) {
|
|
103
|
+
const target = asString(reshape.pilotTarget) || asString(reshape.pilot);
|
|
104
|
+
if (target) {
|
|
105
|
+
push({
|
|
106
|
+
target,
|
|
107
|
+
friction: 'Physical cohesion residual — concept concentration across anchors',
|
|
108
|
+
intent: asString(reshape.move) ||
|
|
109
|
+
'One reshape pilot toward locality; never mechanical-safe multi-file batch',
|
|
110
|
+
benefit: asString(reshape.successSignal) ||
|
|
111
|
+
'Related behavior co-located; public seams stay small',
|
|
112
|
+
evidence: evidenceList('physicalCohesion', [target], 'reshapePilot'),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const findings = Array.isArray(pc?.findings) ? pc.findings : [];
|
|
117
|
+
for (const finding of findings) {
|
|
118
|
+
if (!finding || typeof finding !== 'object')
|
|
119
|
+
continue;
|
|
120
|
+
const concept = asString(finding.concept);
|
|
121
|
+
const anchors = Array.isArray(finding.anchors)
|
|
122
|
+
? finding.anchors.map((a) => asString(a)).filter(Boolean)
|
|
123
|
+
: [];
|
|
124
|
+
const target = anchors[0] || concept;
|
|
125
|
+
if (!target)
|
|
126
|
+
continue;
|
|
127
|
+
push({
|
|
128
|
+
target,
|
|
129
|
+
friction: asString(finding.message) ||
|
|
130
|
+
(concept
|
|
131
|
+
? `Concept "${concept}" concentrated across physical anchors`
|
|
132
|
+
: 'Physical cohesion residual'),
|
|
133
|
+
intent: 'Deepen by colocating behavior behind one public interface per concern',
|
|
134
|
+
benefit: 'Locality of change; fewer cross-anchor edits for one concept',
|
|
135
|
+
evidence: evidenceList('physicalCohesion', anchors.length > 0 ? anchors.slice(0, 3) : [target], concept || undefined),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const compass = input.improvementCompass;
|
|
139
|
+
if (compass && compass.notAScore === true) {
|
|
140
|
+
const residualIds = Array.isArray(compass.topResidual)
|
|
141
|
+
? compass.topResidual.map((id) => asString(id)).filter(Boolean)
|
|
142
|
+
: [];
|
|
143
|
+
const lensById = new Map();
|
|
144
|
+
if (Array.isArray(compass.lenses)) {
|
|
145
|
+
for (const lens of compass.lenses) {
|
|
146
|
+
if (lens && typeof lens === 'object' && asString(lens.id)) {
|
|
147
|
+
lensById.set(asString(lens.id), { summary: asString(lens.summary) || undefined });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const id of residualIds) {
|
|
152
|
+
// Each residual lens id is independent evidence. Cap + target|friction dedupe may
|
|
153
|
+
// still list both a smell card and lens:<id> — no cross-source suppression.
|
|
154
|
+
const summary = lensById.get(id)?.summary;
|
|
155
|
+
push({
|
|
156
|
+
target: `lens:${id}`,
|
|
157
|
+
friction: summary
|
|
158
|
+
? `Residual lens ${id}: ${summary}`
|
|
159
|
+
: `Residual architecture lens ${id} (not a score)`,
|
|
160
|
+
intent: 'Process judgment: deepen modules / name seams that clear this lens residual',
|
|
161
|
+
benefit: 'Clear residual without inventing a depth score or gate fail',
|
|
162
|
+
evidence: evidenceList('improvementCompass', [id], 'topResidual'),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
schemaVersion: ARK_DEEPENING_COACH_SCHEMA_VERSION,
|
|
168
|
+
notAScore: true,
|
|
169
|
+
candidates,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* True when the pure projection would list zero candidates (honesty helper for tests).
|
|
174
|
+
*/
|
|
175
|
+
export function hasNoDeepeningEvidence(input) {
|
|
176
|
+
return buildDeepeningCandidates(input).candidates.length === 0;
|
|
177
|
+
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -66,6 +66,10 @@ import {
|
|
|
66
66
|
buildDoctorImprovementCompass,
|
|
67
67
|
printImprovementCompassSection,
|
|
68
68
|
} from './improvement-compass-doctor.mjs';
|
|
69
|
+
import {
|
|
70
|
+
buildDeepModuleCoachAdvisory,
|
|
71
|
+
printDeepModuleCoachSection,
|
|
72
|
+
} from './deep-module-coach.mjs';
|
|
69
73
|
|
|
70
74
|
const color = {
|
|
71
75
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
@@ -660,6 +664,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
660
664
|
goldenPatternPresent: goldenPattern.present === true,
|
|
661
665
|
arkRulesLoaded: rulesUnderContract?.active === true,
|
|
662
666
|
});
|
|
667
|
+
// Deep-module coach: hot paths + deepening candidates — advisory only (notAScore).
|
|
668
|
+
const deepModuleCoach = buildDeepModuleCoachAdvisory(root, {
|
|
669
|
+
designSmells,
|
|
670
|
+
physicalCohesion: doctorAdvisories.physicalCohesion,
|
|
671
|
+
improvementCompass,
|
|
672
|
+
pilotLoop,
|
|
673
|
+
});
|
|
663
674
|
|
|
664
675
|
if (asJson) {
|
|
665
676
|
(options.writeJson ?? console.log)(
|
|
@@ -677,6 +688,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
677
688
|
designSmells,
|
|
678
689
|
// Improvement compass (lenses; notAScore; never a gate input).
|
|
679
690
|
improvementCompass,
|
|
691
|
+
// Deep-module coach (hot paths + deepening; notAScore; never a gate input).
|
|
692
|
+
deepModuleCoach,
|
|
680
693
|
...(options.designDelta ? { designDelta: options.designDelta } : {}),
|
|
681
694
|
// Q01: primary next action when Shape residual dominates (null if not design-weak).
|
|
682
695
|
postGreenPath,
|
|
@@ -870,6 +883,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
870
883
|
}
|
|
871
884
|
|
|
872
885
|
printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
|
|
886
|
+
printDeepModuleCoachSection(deepModuleCoach, { line, warn, ok, color });
|
|
873
887
|
|
|
874
888
|
console.log('');
|
|
875
889
|
console.log(color.bold('Design fitness'));
|
|
@@ -279,11 +279,44 @@ function improvementCompassHtml(compass) {
|
|
|
279
279
|
</section>`;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/** Deep-module coach — hot paths + deepening candidates; never a gate input. */
|
|
283
|
+
function deepModuleCoachHtml(coach) {
|
|
284
|
+
if (!coach || coach.notAScore !== true) return '';
|
|
285
|
+
const hot = coach.hotPaths;
|
|
286
|
+
const candidates = Array.isArray(coach.deepeningCandidates) ? coach.deepeningCandidates : [];
|
|
287
|
+
let hotBlock = '';
|
|
288
|
+
if (hot?.status === 'unavailable') {
|
|
289
|
+
hotBlock = `<p class="muted">Hot paths: unavailable — ${esc(hot.reason || 'no git history')}; never invented.</p>`;
|
|
290
|
+
} else if (hot?.available === true && Array.isArray(hot.paths) && hot.paths.length > 0) {
|
|
291
|
+
hotBlock = `<p><span class="tag warn">hot paths</span> ${hot.paths
|
|
292
|
+
.slice(0, 8)
|
|
293
|
+
.map((p) => `<code>${esc(p.path)}</code> (${esc(String(p.changeCount))})`)
|
|
294
|
+
.join(' · ')}</p>`;
|
|
295
|
+
} else {
|
|
296
|
+
hotBlock = '<p class="muted">Hot paths: none above churn threshold (advisory).</p>';
|
|
297
|
+
}
|
|
298
|
+
const deepenBlock =
|
|
299
|
+
candidates.length === 0
|
|
300
|
+
? '<p class="muted">Deepening candidates: none from existing evidence (not a score).</p>'
|
|
301
|
+
: `<p><span class="tag warn">deepening</span> ${candidates
|
|
302
|
+
.slice(0, 5)
|
|
303
|
+
.map((c) => `<code>${esc(c.target)}</code> — ${esc(c.friction)}`)
|
|
304
|
+
.join('<br/>')}</p>`;
|
|
305
|
+
return `
|
|
306
|
+
<section class="section card" data-advisory="deepModuleCoach">
|
|
307
|
+
<h2>Deep-module coach <span class="muted">(advisory — never changes the verdict)</span></h2>
|
|
308
|
+
${hotBlock}
|
|
309
|
+
${deepenBlock}
|
|
310
|
+
<p class="muted">Prefer deep modules; name seams; test at the public interface. Always <code>notAScore</code>.</p>
|
|
311
|
+
</section>`;
|
|
312
|
+
}
|
|
313
|
+
|
|
282
314
|
export function renderAdvisorySections(advisories, escape) {
|
|
283
315
|
if (!advisories || typeof advisories !== 'object') return '';
|
|
284
316
|
if (typeof escape === 'function') esc = escape;
|
|
285
317
|
return [
|
|
286
318
|
improvementCompassHtml(advisories.improvementCompass),
|
|
319
|
+
deepModuleCoachHtml(advisories.deepModuleCoach),
|
|
287
320
|
contractHealthHtml(advisories.contractHealth),
|
|
288
321
|
ambientStateHtml(advisories.ambientState),
|
|
289
322
|
physicalCohesionHtml(advisories.physicalCohesion),
|
|
@@ -22,6 +22,7 @@ import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
|
22
22
|
import { readBaseline, baselineOccurrenceKeys } from './violations.mjs';
|
|
23
23
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
24
24
|
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
25
|
+
import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
25
26
|
import { computePhysicalCohesion } from './physical-cohesion.mjs';
|
|
26
27
|
|
|
27
28
|
function esc(value) {
|
|
@@ -178,6 +179,13 @@ export function buildReportDepthPayload(
|
|
|
178
179
|
goldenPatternPresent: goldenPattern.present === true,
|
|
179
180
|
arkRulesLoaded: rulesUnderContract?.active === true,
|
|
180
181
|
});
|
|
182
|
+
// Deep-module coach — same advisory as doctor; never a gate input.
|
|
183
|
+
const deepModuleCoach = buildDeepModuleCoachAdvisory(root, {
|
|
184
|
+
designSmells,
|
|
185
|
+
physicalCohesion,
|
|
186
|
+
improvementCompass,
|
|
187
|
+
pilotLoop,
|
|
188
|
+
});
|
|
181
189
|
return {
|
|
182
190
|
adoption,
|
|
183
191
|
designDepth: {
|
|
@@ -190,6 +198,7 @@ export function buildReportDepthPayload(
|
|
|
190
198
|
productHonesty,
|
|
191
199
|
mergePlanes: rulesUnderContract?.mergePlanes ?? null,
|
|
192
200
|
improvementCompass,
|
|
201
|
+
deepModuleCoach,
|
|
193
202
|
},
|
|
194
203
|
};
|
|
195
204
|
}
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
formatManagedUpgradeSelfServiceHonesty,
|
|
9
9
|
projectManagedUpgradeSelfServiceHonesty,
|
|
10
10
|
} from './managed-upgrade-honesty.mjs';
|
|
11
|
+
import {
|
|
12
|
+
buildUpgradeWhatsNewSuggestions,
|
|
13
|
+
formatUpgradeWhatsNewSuggestions,
|
|
14
|
+
} from './upgrade-whats-new.mjs';
|
|
11
15
|
import {
|
|
12
16
|
KNOWN_TOOLS,
|
|
13
17
|
arkPackageVersion,
|
|
@@ -21,6 +25,11 @@ export {
|
|
|
21
25
|
projectHostWritePathActivation,
|
|
22
26
|
projectManagedUpgradeSelfServiceHonesty,
|
|
23
27
|
} from './managed-upgrade-honesty.mjs';
|
|
28
|
+
export {
|
|
29
|
+
buildUpgradeWhatsNewSuggestions,
|
|
30
|
+
formatUpgradeWhatsNewSuggestions,
|
|
31
|
+
UPGRADE_WHATS_NEW_SCHEMA_VERSION,
|
|
32
|
+
} from './upgrade-whats-new.mjs';
|
|
24
33
|
|
|
25
34
|
export const MANAGED_MANIFEST_PATH = 'ark.managed.json';
|
|
26
35
|
const MANIFEST_VERSION = '1.0';
|
|
@@ -456,11 +465,19 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
456
465
|
kind: catalogAsset.kind,
|
|
457
466
|
});
|
|
458
467
|
const accepted = options.acceptConflicts === true;
|
|
468
|
+
// FX04: --refresh-skills opt-in rewrites customized *skill* assets to package
|
|
469
|
+
// templates. Conflicted still needs --accept-conflicts. Never silent default.
|
|
470
|
+
const refreshSkills = options.refreshSkills === true;
|
|
471
|
+
const skillRefresh =
|
|
472
|
+
refreshSkills &&
|
|
473
|
+
catalogAsset.kind === 'skill' &&
|
|
474
|
+
classified.state === 'customized';
|
|
459
475
|
const canApply =
|
|
460
476
|
classified.state === 'stale' ||
|
|
477
|
+
skillRefresh ||
|
|
461
478
|
(classified.state === 'missing' && (!recorded || accepted)) ||
|
|
462
479
|
(classified.state === 'conflicted' && accepted);
|
|
463
|
-
const blocked = classified.requiresConsent && !accepted;
|
|
480
|
+
const blocked = classified.requiresConsent && !accepted && !skillRefresh;
|
|
464
481
|
const desiredFile = afterFileContent(catalogAsset, currentFile, desiredScoped);
|
|
465
482
|
const asset = {
|
|
466
483
|
path: catalogAsset.relativePath,
|
|
@@ -533,6 +550,7 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
533
550
|
profile: selection.profile,
|
|
534
551
|
hosts: selection.hosts,
|
|
535
552
|
acceptConflicts: options.acceptConflicts === true,
|
|
553
|
+
refreshSkills: options.refreshSkills === true,
|
|
536
554
|
assets,
|
|
537
555
|
summary,
|
|
538
556
|
nextManifest,
|
|
@@ -543,6 +561,183 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
543
561
|
return plan;
|
|
544
562
|
}
|
|
545
563
|
|
|
564
|
+
/**
|
|
565
|
+
* FX03 — skill content drift honesty (counts by state + sample paths).
|
|
566
|
+
* Skills only; never claims "skills upgraded" when only package pin moved.
|
|
567
|
+
*/
|
|
568
|
+
export function buildSkillDriftSummary(plan) {
|
|
569
|
+
const assets = Array.isArray(plan?.assets) ? plan.assets : [];
|
|
570
|
+
const skills = assets.filter((a) => a?.kind === 'skill');
|
|
571
|
+
const byState = {};
|
|
572
|
+
for (const skill of skills) {
|
|
573
|
+
const state = typeof skill.state === 'string' ? skill.state : 'unknown';
|
|
574
|
+
byState[state] = (byState[state] ?? 0) + 1;
|
|
575
|
+
}
|
|
576
|
+
const sample = (state, limit = 5) =>
|
|
577
|
+
skills
|
|
578
|
+
.filter((s) => s.state === state)
|
|
579
|
+
.map((s) => s.path)
|
|
580
|
+
.sort()
|
|
581
|
+
.slice(0, limit);
|
|
582
|
+
const customized = byState.customized ?? 0;
|
|
583
|
+
const stale = byState.stale ?? 0;
|
|
584
|
+
const missing = byState.missing ?? 0;
|
|
585
|
+
const current = byState.current ?? 0;
|
|
586
|
+
const wouldRefresh = skills.filter((s) => s.willApply === true).length;
|
|
587
|
+
return {
|
|
588
|
+
schemaVersion: '1.0',
|
|
589
|
+
notAScore: true,
|
|
590
|
+
skillCount: skills.length,
|
|
591
|
+
byState,
|
|
592
|
+
stale,
|
|
593
|
+
customized,
|
|
594
|
+
missing,
|
|
595
|
+
current,
|
|
596
|
+
wouldRefresh,
|
|
597
|
+
samplePaths: {
|
|
598
|
+
stale: sample('stale'),
|
|
599
|
+
customized: sample('customized'),
|
|
600
|
+
missing: sample('missing'),
|
|
601
|
+
},
|
|
602
|
+
note:
|
|
603
|
+
customized > 0 && wouldRefresh === 0
|
|
604
|
+
? 'Skills on disk differ from package templates (customized preserved). Use --refresh-skills to opt in to rewrite customized skills; never silent overwrite.'
|
|
605
|
+
: stale > 0
|
|
606
|
+
? 'Some skills are stale vs package templates and will refresh on apply.'
|
|
607
|
+
: skills.length === 0
|
|
608
|
+
? 'No managed skill assets in this upgrade selection.'
|
|
609
|
+
: 'Skill content matches package templates or is scheduled for write.',
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* FX07 — active host vs managed --tools / manifest hosts.
|
|
615
|
+
*/
|
|
616
|
+
export function buildHostSelectionHonesty(plan) {
|
|
617
|
+
const hosts = Array.isArray(plan?.hosts) ? plan.hosts.map((h) => String(h).toLowerCase()) : [];
|
|
618
|
+
let active = null;
|
|
619
|
+
try {
|
|
620
|
+
active = detectActiveAgentHost();
|
|
621
|
+
} catch {
|
|
622
|
+
active = null;
|
|
623
|
+
}
|
|
624
|
+
const activeNorm =
|
|
625
|
+
typeof active === 'string' && active.trim() ? active.trim().toLowerCase() : null;
|
|
626
|
+
const known = activeNorm && KNOWN_TOOLS.includes(activeNorm);
|
|
627
|
+
const inSelection = Boolean(activeNorm && hosts.includes(activeNorm));
|
|
628
|
+
const note =
|
|
629
|
+
known && !inSelection
|
|
630
|
+
? `Detected host "${activeNorm}" is not in managed tools [${hosts.join(', ') || 'none'}]. Re-run with --tools ${[...new Set([...hosts, activeNorm])].sort().join(',')} so that host's skills/hooks are in the plan.`
|
|
631
|
+
: known && inSelection
|
|
632
|
+
? `Detected host "${activeNorm}" is in the managed selection.`
|
|
633
|
+
: activeNorm
|
|
634
|
+
? `Detected host "${activeNorm}" is outside the known managed tool set.`
|
|
635
|
+
: 'No active agent host detected for this process.';
|
|
636
|
+
return {
|
|
637
|
+
schemaVersion: '1.0',
|
|
638
|
+
notAScore: true,
|
|
639
|
+
activeHost: activeNorm,
|
|
640
|
+
managedHosts: hosts,
|
|
641
|
+
activeInSelection: inSelection,
|
|
642
|
+
suggestTools:
|
|
643
|
+
known && !inSelection
|
|
644
|
+
? [...new Set([...hosts, activeNorm])].sort().join(',')
|
|
645
|
+
: null,
|
|
646
|
+
note,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* FX05 — post-upgrade verification block (advisory sensors only).
|
|
652
|
+
*/
|
|
653
|
+
export function buildPostUpgradeChecks(root, options = {}) {
|
|
654
|
+
const resolvedRoot = path.resolve(root);
|
|
655
|
+
const checks = [];
|
|
656
|
+
let projectVersion = null;
|
|
657
|
+
try {
|
|
658
|
+
const pkgPath = path.join(resolvedRoot, 'node_modules', 'arkgate', 'package.json');
|
|
659
|
+
if (fs.existsSync(pkgPath)) {
|
|
660
|
+
projectVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
projectVersion = null;
|
|
664
|
+
}
|
|
665
|
+
const cli = typeof options.cliVersion === 'string' ? options.cliVersion : arkPackageVersion();
|
|
666
|
+
const pinOk =
|
|
667
|
+
projectVersion != null && cli != null ? projectVersion === cli : null;
|
|
668
|
+
checks.push({
|
|
669
|
+
id: 'package-pin-cli',
|
|
670
|
+
ok: pinOk,
|
|
671
|
+
detail:
|
|
672
|
+
pinOk === true
|
|
673
|
+
? `Installed arkgate@${projectVersion} matches CLI ${cli}.`
|
|
674
|
+
: pinOk === false
|
|
675
|
+
? `Installed arkgate@${projectVersion} ≠ CLI ${cli}; re-run install or restart using project-local CLI.`
|
|
676
|
+
: `Could not compare pin (installed=${projectVersion ?? 'missing'}, cli=${cli ?? 'unknown'}).`,
|
|
677
|
+
});
|
|
678
|
+
checks.push({
|
|
679
|
+
id: 'architecture-verification',
|
|
680
|
+
ok:
|
|
681
|
+
options.verification?.mode === 'skipped'
|
|
682
|
+
? null
|
|
683
|
+
: options.verification?.exitCode === 0,
|
|
684
|
+
detail:
|
|
685
|
+
options.verification?.mode === 'skipped'
|
|
686
|
+
? 'Strict architecture verification was skipped (--no-strict).'
|
|
687
|
+
: options.verification?.exitCode === 0
|
|
688
|
+
? 'Strict-merge architecture verification passed.'
|
|
689
|
+
: `Architecture verification exit ${options.verification?.exitCode ?? 'unknown'}.`,
|
|
690
|
+
});
|
|
691
|
+
checks.push({
|
|
692
|
+
id: 'package-version-truth',
|
|
693
|
+
ok: options.dualTruth?.dualTruth === true ? false : options.dualTruth ? true : null,
|
|
694
|
+
detail:
|
|
695
|
+
options.dualTruth?.dualTruth === true
|
|
696
|
+
? options.dualTruth.note || 'Package pin dual-truth detected.'
|
|
697
|
+
: options.dualTruth
|
|
698
|
+
? 'Package pin truth is consistent for this apply.'
|
|
699
|
+
: 'Package version truth not evaluated.',
|
|
700
|
+
});
|
|
701
|
+
checks.push({
|
|
702
|
+
id: 'doctor-compass-coach',
|
|
703
|
+
ok: null,
|
|
704
|
+
detail:
|
|
705
|
+
'Run `npx arkgate-check --doctor --json` and confirm doctor.improvementCompass + doctor.deepModuleCoach (notAScore).',
|
|
706
|
+
});
|
|
707
|
+
checks.push({
|
|
708
|
+
id: 'agents-md-projection',
|
|
709
|
+
ok: null,
|
|
710
|
+
detail: 'Run `npx arkgate agents-md --check` (or --write) so AGENTS.md matches the package projection.',
|
|
711
|
+
});
|
|
712
|
+
checks.push({
|
|
713
|
+
id: 'status-mode',
|
|
714
|
+
ok: null,
|
|
715
|
+
detail: 'Run `npx arkgate status --json` and read honesty mode; incomplete facts never invent green residual.',
|
|
716
|
+
});
|
|
717
|
+
return {
|
|
718
|
+
schemaVersion: '1.0',
|
|
719
|
+
notAScore: true,
|
|
720
|
+
neverGateInput: true,
|
|
721
|
+
checks,
|
|
722
|
+
mcpNote:
|
|
723
|
+
'If you used Ark MCP this session: restart/retarget MCP after package bump so process arkgateVersion matches project install; always pass project.expectedRoot + expectedProjectId (WI01). Prefer project-local CLI until identity matched and versions align.',
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function formatSkillDriftHuman(skillDrift) {
|
|
728
|
+
if (!skillDrift) return [];
|
|
729
|
+
const lines = [
|
|
730
|
+
`Skill drift: ${skillDrift.skillCount} skill(s) — current ${skillDrift.current}, stale ${skillDrift.stale}, customized ${skillDrift.customized}, missing ${skillDrift.missing}, would refresh ${skillDrift.wouldRefresh}.`,
|
|
731
|
+
];
|
|
732
|
+
if (skillDrift.note) lines.push(` ${skillDrift.note}`);
|
|
733
|
+
return lines;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function formatHostSelectionHuman(hostSelection) {
|
|
737
|
+
if (!hostSelection?.note) return [];
|
|
738
|
+
return [`Host selection: ${hostSelection.note}`];
|
|
739
|
+
}
|
|
740
|
+
|
|
546
741
|
function publicPlan(plan, overrides = {}) {
|
|
547
742
|
const assets = plan.assets.map(
|
|
548
743
|
({ containerBeforeHash: _container, [AFTER_CONTENT]: _content, ...asset }) => asset
|
|
@@ -567,11 +762,15 @@ function publicPlan(plan, overrides = {}) {
|
|
|
567
762
|
assets,
|
|
568
763
|
summary: base.summary,
|
|
569
764
|
});
|
|
765
|
+
// Suggested improvements / what’s new — product capabilities to try after install/upgrade.
|
|
766
|
+
// Always notAScore; never a gate input; not part of planDigest.
|
|
767
|
+
const whatsNew = buildUpgradeWhatsNewSuggestions();
|
|
570
768
|
return {
|
|
571
769
|
...base,
|
|
572
770
|
...overrides,
|
|
573
771
|
// DF05 projection always present unless an override supplies a replacement.
|
|
574
772
|
selfService: overrides.selfService ?? selfService,
|
|
773
|
+
whatsNew: overrides.whatsNew ?? whatsNew,
|
|
575
774
|
};
|
|
576
775
|
}
|
|
577
776
|
|
|
@@ -771,6 +970,21 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
771
970
|
for (const line of formatManagedUpgradeSelfServiceHonesty(honesty)) {
|
|
772
971
|
console.log(line);
|
|
773
972
|
}
|
|
973
|
+
const skillDrift =
|
|
974
|
+
options.skillDrift ?? plan.skillDrift ?? buildSkillDriftSummary(plan);
|
|
975
|
+
for (const line of formatSkillDriftHuman(skillDrift)) {
|
|
976
|
+
console.log(line);
|
|
977
|
+
}
|
|
978
|
+
const hostSelection =
|
|
979
|
+
options.hostSelection ?? plan.hostSelection ?? buildHostSelectionHonesty(plan);
|
|
980
|
+
for (const line of formatHostSelectionHuman(hostSelection)) {
|
|
981
|
+
console.log(line);
|
|
982
|
+
}
|
|
983
|
+
// FX08: whatsNew always on preview/apply human path (including nothing-to-apply).
|
|
984
|
+
const whatsNew = plan.whatsNew ?? buildUpgradeWhatsNewSuggestions();
|
|
985
|
+
for (const line of formatUpgradeWhatsNewSuggestions(whatsNew)) {
|
|
986
|
+
console.log(line);
|
|
987
|
+
}
|
|
774
988
|
if (plan.applied) {
|
|
775
989
|
console.log(
|
|
776
990
|
`Applied ${wouldWrite} content write(s)` +
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -49,15 +49,15 @@ export function deterministicNextAction(violation) {
|
|
|
49
49
|
return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
|
|
50
50
|
}
|
|
51
51
|
if (violation.peerIsolation) {
|
|
52
|
-
return 'Extract the shared dependency to a shared layer, then preflight again.';
|
|
52
|
+
return 'Extract the shared dependency to a shared layer, test at the public interface, then preflight again.';
|
|
53
53
|
}
|
|
54
|
-
return `Define a port in ${violation.fromLayer ?? 'the source layer'}, inject the ${violation.toLayer ?? 'outer-layer'} implementation, then preflight again.`;
|
|
54
|
+
return `Define a port in ${violation.fromLayer ?? 'the source layer'}, inject the ${violation.toLayer ?? 'outer-layer'} implementation, test at the public interface, then preflight again.`;
|
|
55
55
|
case 'FORBIDDEN_GLOBAL':
|
|
56
|
-
return `Inject ${violation.target ?? 'the capability'} through a port, then preflight again.`;
|
|
56
|
+
return `Inject ${violation.target ?? 'the capability'} through a port, test at the public interface, then preflight again.`;
|
|
57
57
|
case 'CAPABILITY_VIOLATION':
|
|
58
|
-
return `Define a ${String(violation.capability ?? 'capability')} port in ${violation.fromLayer ?? 'the walled layer'}, bind the implementation outside it, then preflight again.`;
|
|
58
|
+
return `Define a ${String(violation.capability ?? 'capability')} port in ${violation.fromLayer ?? 'the walled layer'}, bind the implementation outside it, test at the public interface, then preflight again.`;
|
|
59
59
|
case 'CIRCULAR_DEPENDENCY':
|
|
60
|
-
return 'Extract the shared dependency into a third module, then preflight again.';
|
|
60
|
+
return 'Extract the shared dependency into a third module, test at the public interface, then preflight again.';
|
|
61
61
|
case 'RAW_EVENT_PUBLISH':
|
|
62
62
|
return 'Publish through a registered intent creator, then run Ark again.';
|
|
63
63
|
case 'PUBLISH_MISSING_SOURCE':
|
|
@@ -136,8 +136,29 @@ export function buildRulesInventory(input) {
|
|
|
136
136
|
/_(?:OID|OIDS)$/i.test(name) ||
|
|
137
137
|
/^(?:INT2|INT4|INT8|FLOAT4|FLOAT8|NUMERIC|DATE|TIME|TIMESTAMP|TIMESTAMPTZ|JSON|JSONB|UUID)OID$/i.test(name) ||
|
|
138
138
|
/(?:^|_)(?:SCHEMA|PROTOCOL|RESOLVER|FORMAT)_(?:URL|URI|VERSION|ID|IDENTITY)$/i.test(name);
|
|
139
|
+
/**
|
|
140
|
+
* FX09 — pure UX copy / error-message string constants crowd inventory pilots.
|
|
141
|
+
* Downrank (skip) sentence-like strings and message-named identifiers; keep
|
|
142
|
+
* numeric thresholds and domain status tokens for adopt/contract pilots.
|
|
143
|
+
*/
|
|
144
|
+
const isUxMessageConstant = (name, rawValue) => {
|
|
145
|
+
if (/^(?:ERROR|SUCCESS|WARNING|INFO|HINT|HELP|EMPTY|TOAST|SNACK|ALERT|BANNER|DIALOG|MODAL|TOOLTIP|CAPTION|SUBTITLE|HEADLINE|USER|UI|DISPLAY|FEEDBACK)_(?:MSG|MESSAGE|TEXT|COPY|LABEL|TITLE|BODY|DESC|DESCRIPTION|HINT|HELP)?/i.test(name) ||
|
|
146
|
+
/_(?:MSG|MESSAGE|TEXT|COPY|TOAST|SNACK|ALERT|BANNER|CAPTION|HINT|HELP_TEXT|ERROR_TEXT|EMPTY_TEXT|PLACEHOLDER_TEXT|USER_MESSAGE|FEEDBACK)$/i.test(name)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const unquoted = rawValue.replace(/^['"]|['"]$/g, '');
|
|
150
|
+
// Sentence-like string values (spaces or terminal punctuation) are UX copy,
|
|
151
|
+
// not behavioral business limits — unless the name is a clear domain status seed.
|
|
152
|
+
if (/^['"]/.test(rawValue) &&
|
|
153
|
+
(/\s/.test(unquoted) || /[.!?…]$/.test(unquoted)) &&
|
|
154
|
+
!/^(?:STATUS|STATE|PHASE|ROLE|TYPE|KIND|ORDER|PAYMENT|CART|INVOICE|POLICY)_[A-Z0-9_]+$/i.test(name)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
139
159
|
while ((magic = magicRe.exec(content)) !== null) {
|
|
140
160
|
const name = magic[2];
|
|
161
|
+
const rawValue = magic[3] ?? '';
|
|
141
162
|
// With governed layer evidence, generic Tooling/Kernel constants are not
|
|
142
163
|
// business-rule candidates. Controller-shaped boundaries stay eligible
|
|
143
164
|
// because business policy can leak into them.
|
|
@@ -145,6 +166,8 @@ export function buildRulesInventory(input) {
|
|
|
145
166
|
continue;
|
|
146
167
|
if (isInfraMagicName(name))
|
|
147
168
|
continue;
|
|
169
|
+
if (isUxMessageConstant(name, rawValue))
|
|
170
|
+
continue;
|
|
148
171
|
// P2-N: skip remaining ALL_CAPS noise only on clear UI chrome (not all of app/).
|
|
149
172
|
if (isUiChrome && !isDomain)
|
|
150
173
|
continue;
|