arkgate 4.4.0 → 4.5.5
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 +71 -2
- package/README.md +7 -4
- package/bin/ark-check-runtime.mjs +38 -13
- package/bin/ark-layer-match.mjs +25 -12
- package/bin/lib/adapter-contract.mjs +5 -5
- package/bin/lib/analysis-engine.mjs +5 -5
- 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/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 +54 -4
- package/bin/lib/remediation.mjs +5 -5
- package/bin/lib/status-command.mjs +127 -2
- package/bin/lib/status-manifest.mjs +163 -14
- package/bin/lib/upgrade-whats-new.mjs +110 -0
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +28 -28
- package/dist/index.d.ts +126 -21
- package/dist/index.js +28 -28
- package/docs/README.md +5 -5
- package/docs/agent-guide.md +50 -8
- package/docs/brownfield-adoption.md +12 -0
- package/docs/develop.md +3 -1
- package/docs/package-surface.md +8 -6
- package/docs/product-voice.md +25 -1
- package/docs/use.md +33 -0
- package/package.json +1 -1
- package/schemas/ark.status-manifest.schema.json +28 -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 +14 -0
- 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 +14 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-module coach advisory (Tooling) — hot paths + deepening candidates.
|
|
3
|
+
*
|
|
4
|
+
* Advisory only (`notAScore`). Never feeds valid / strict-merge / goal.met /
|
|
5
|
+
* completeness green. Hot paths use a bounded git log heuristic; incomplete or
|
|
6
|
+
* missing history → status `unavailable`, empty paths (never invent).
|
|
7
|
+
* Deepening candidates come only from existing smells / cohesion / compass /
|
|
8
|
+
* pilot evidence via Domain pure projection.
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import {
|
|
12
|
+
buildDeepeningCandidates,
|
|
13
|
+
ARK_DEEPENING_COACH_SCHEMA_VERSION,
|
|
14
|
+
DEEPENING_CANDIDATE_CAP,
|
|
15
|
+
} from './deepening-coach.mjs';
|
|
16
|
+
|
|
17
|
+
export { buildDeepeningCandidates, ARK_DEEPENING_COACH_SCHEMA_VERSION, DEEPENING_CANDIDATE_CAP };
|
|
18
|
+
|
|
19
|
+
/** Recent-window commit cap for hot-path heuristic (budget). */
|
|
20
|
+
export const HOT_PATH_COMMIT_LIMIT = 200;
|
|
21
|
+
/** Max listed hot paths. */
|
|
22
|
+
export const HOT_PATH_LIST_CAP = 8;
|
|
23
|
+
/** Minimum change hits before a path is “elevated”. */
|
|
24
|
+
export const HOT_PATH_MIN_HITS = 3;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Best-effort recent-churn paths from git history.
|
|
28
|
+
* @param {string} root
|
|
29
|
+
* @param {{ runGit?: Function, commitLimit?: number, minHits?: number, listCap?: number }} [opts]
|
|
30
|
+
*/
|
|
31
|
+
export function computeHotPathAdvisory(root, opts = {}) {
|
|
32
|
+
const commitLimit = Number(opts.commitLimit) > 0 ? Number(opts.commitLimit) : HOT_PATH_COMMIT_LIMIT;
|
|
33
|
+
const minHits = Number(opts.minHits) > 0 ? Number(opts.minHits) : HOT_PATH_MIN_HITS;
|
|
34
|
+
const listCap = Number(opts.listCap) > 0 ? Number(opts.listCap) : HOT_PATH_LIST_CAP;
|
|
35
|
+
const runGit =
|
|
36
|
+
typeof opts.runGit === 'function'
|
|
37
|
+
? opts.runGit
|
|
38
|
+
: (args) =>
|
|
39
|
+
spawnSync('git', ['-C', root, ...args], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const emptyUnavailable = (reason) => ({
|
|
46
|
+
available: false,
|
|
47
|
+
status: 'unavailable',
|
|
48
|
+
reason,
|
|
49
|
+
paths: [],
|
|
50
|
+
notAScore: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const head = runGit(['rev-parse', '--verify', 'HEAD']);
|
|
55
|
+
if (!head || head.status !== 0) {
|
|
56
|
+
return emptyUnavailable('git history incomplete or missing (no HEAD)');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const log = runGit([
|
|
60
|
+
'log',
|
|
61
|
+
'-n',
|
|
62
|
+
String(commitLimit),
|
|
63
|
+
'--name-only',
|
|
64
|
+
'--pretty=format:',
|
|
65
|
+
'--diff-filter=AMR',
|
|
66
|
+
]);
|
|
67
|
+
if (!log || log.status !== 0) {
|
|
68
|
+
return emptyUnavailable('git log unavailable for hot-path heuristic');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const counts = new Map();
|
|
72
|
+
const text = typeof log.stdout === 'string' ? log.stdout : '';
|
|
73
|
+
for (const line of text.split('\n')) {
|
|
74
|
+
const raw = line.trim().replace(/\\/g, '/');
|
|
75
|
+
if (!raw || raw.startsWith('.git/')) continue;
|
|
76
|
+
// Prefer product source; still allow other paths if they dominate.
|
|
77
|
+
counts.set(raw, (counts.get(raw) || 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (counts.size === 0) {
|
|
81
|
+
return {
|
|
82
|
+
available: true,
|
|
83
|
+
status: 'ok',
|
|
84
|
+
reason: null,
|
|
85
|
+
paths: [],
|
|
86
|
+
notAScore: true,
|
|
87
|
+
window: { commitLimit, minHits },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ranked = [...counts.entries()]
|
|
92
|
+
.filter(([, n]) => n >= minHits)
|
|
93
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
94
|
+
.slice(0, listCap)
|
|
95
|
+
.map(([path, changeCount]) => ({ path, changeCount }));
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
available: true,
|
|
99
|
+
status: 'ok',
|
|
100
|
+
reason: null,
|
|
101
|
+
paths: ranked,
|
|
102
|
+
notAScore: true,
|
|
103
|
+
window: { commitLimit, minHits },
|
|
104
|
+
};
|
|
105
|
+
} catch {
|
|
106
|
+
return emptyUnavailable('git history incomplete or missing');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Full deep-module coach advisory for doctor / report.
|
|
112
|
+
* @param {string} root
|
|
113
|
+
* @param {{
|
|
114
|
+
* designSmells?: object[],
|
|
115
|
+
* physicalCohesion?: object | null,
|
|
116
|
+
* improvementCompass?: object | null,
|
|
117
|
+
* pilotLoop?: object | null,
|
|
118
|
+
* runGit?: Function,
|
|
119
|
+
* }} [input]
|
|
120
|
+
*/
|
|
121
|
+
export function buildDeepModuleCoachAdvisory(root, input = {}) {
|
|
122
|
+
const deepening = buildDeepeningCandidates({
|
|
123
|
+
designSmells: input.designSmells,
|
|
124
|
+
physicalCohesion: input.physicalCohesion,
|
|
125
|
+
improvementCompass: input.improvementCompass,
|
|
126
|
+
pilotLoop: input.pilotLoop,
|
|
127
|
+
});
|
|
128
|
+
const hotPaths = computeHotPathAdvisory(root, {
|
|
129
|
+
runGit: input.runGit,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: ARK_DEEPENING_COACH_SCHEMA_VERSION,
|
|
134
|
+
notAScore: true,
|
|
135
|
+
hotPaths,
|
|
136
|
+
deepeningCandidates: deepening.candidates,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Human doctor section (never a score bar). Always one short block when coach is present.
|
|
142
|
+
* @param {ReturnType<typeof buildDeepModuleCoachAdvisory>} coach
|
|
143
|
+
* @param {{ line: Function, warn: string, ok: string, color: { bold: Function, dim: Function } }} io
|
|
144
|
+
*/
|
|
145
|
+
export function printDeepModuleCoachSection(coach, io) {
|
|
146
|
+
if (!coach || coach.notAScore !== true) return;
|
|
147
|
+
const { line, warn, ok, color } = io;
|
|
148
|
+
const hot = coach.hotPaths || { status: 'unavailable', paths: [], reason: 'missing' };
|
|
149
|
+
const candidates = Array.isArray(coach.deepeningCandidates) ? coach.deepeningCandidates : [];
|
|
150
|
+
const paths = Array.isArray(hot.paths) ? hot.paths : [];
|
|
151
|
+
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(color.bold('Deep-module coach (advisory — not a score)'));
|
|
154
|
+
|
|
155
|
+
if (hot.status === 'unavailable') {
|
|
156
|
+
line(ok, color.dim(`Hot paths: unavailable — ${hot.reason || 'no git history'}; never invented.`));
|
|
157
|
+
} else if (paths.length > 0) {
|
|
158
|
+
line(warn, `Hot paths (recent churn heuristic, top ${paths.length}):`);
|
|
159
|
+
for (const row of paths.slice(0, HOT_PATH_LIST_CAP)) {
|
|
160
|
+
line(' ', color.dim(`${row.path} · ${row.changeCount} recent change(s)`));
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
line(ok, color.dim('Hot paths: none above churn threshold (advisory only).'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (candidates.length === 0) {
|
|
167
|
+
line(ok, color.dim('Deepening candidates: none from existing evidence.'));
|
|
168
|
+
} else {
|
|
169
|
+
line(warn, `Deepening candidates (${candidates.length}, from existing residual only):`);
|
|
170
|
+
for (const c of candidates.slice(0, DEEPENING_CANDIDATE_CAP)) {
|
|
171
|
+
line(' ', `${c.target} — ${c.friction}`);
|
|
172
|
+
if (c.intent) line(' ', color.dim(`intent: ${c.intent}`));
|
|
173
|
+
if (c.benefit) line(' ', color.dim(`benefit: ${c.benefit}`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
line(ok, color.dim('Never changes gate verdicts. Prefer deep modules; test at the public interface.'));
|
|
177
|
+
}
|
|
@@ -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
|
}
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -143,6 +143,8 @@ export function buildReportSnapshot({
|
|
|
143
143
|
enforcement,
|
|
144
144
|
score,
|
|
145
145
|
mode,
|
|
146
|
+
/** DF02 — thin status compass slice (mode + residual ids, notAScore). */
|
|
147
|
+
improvementCompass = null,
|
|
146
148
|
}) {
|
|
147
149
|
const layers = Array.isArray(config?.layers) ? config.layers : [];
|
|
148
150
|
const rules = Array.isArray(config?.rules) ? config.rules : [];
|
|
@@ -151,7 +153,7 @@ export function buildReportSnapshot({
|
|
|
151
153
|
for (const [name, n] of fileCountByLayer) counts[name] = n;
|
|
152
154
|
}
|
|
153
155
|
const gatesOn = (enforcement || []).filter((e) => e.on).length;
|
|
154
|
-
|
|
156
|
+
const snapshot = {
|
|
155
157
|
version: 1,
|
|
156
158
|
kind: 'ark-architecture-snapshot',
|
|
157
159
|
generatedAt: new Date().toISOString(),
|
|
@@ -186,6 +188,11 @@ export function buildReportSnapshot({
|
|
|
186
188
|
gatesTotal: (enforcement || []).length,
|
|
187
189
|
layerFiles: counts,
|
|
188
190
|
};
|
|
191
|
+
// DF02 — store thin status compass so `ark status` can project residual honestly.
|
|
192
|
+
if (improvementCompass && typeof improvementCompass === 'object') {
|
|
193
|
+
snapshot.improvementCompass = improvementCompass;
|
|
194
|
+
}
|
|
195
|
+
return snapshot;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
export function readJsonSafe(file) {
|