arkgate 3.0.3 → 3.0.4

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 CHANGED
@@ -4,7 +4,40 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
- No changes are scheduled after 3.0.3.
7
+ No changes are scheduled after 3.0.4.
8
+
9
+ ## 3.0.4 — 2026-07-14
10
+
11
+ Report honesty + showcase depth patch. **No breaking** CLI or `ark.config.json` changes.
12
+ **No gate weaken.**
13
+
14
+ ### Fixed
15
+
16
+ - **HTML report false ADAPT:** `computeReportFitness` counted *any* `optional: true` layer
17
+ with files as `coreOptionalWithFiles`, so doctor could report **ENFORCE** while
18
+ `ark-report.html` / `latest.json` mode stayed **ADAPT** (secondary layers like
19
+ SharedKernel / Integration / Workflow). Report now uses the same `CORE_LAYER_NAMES`
20
+ filter as doctor adoption (`DomainModel`, `ApplicationOrchestration`,
21
+ `PresentationAdapters`, `PersistenceAdapters`).
22
+ - **False adoption gap `write-path-none` on report/CI:** when `activeHost` is `unknown`
23
+ (plain `npx ark-check --report` outside an agent session) but the repo inventory already
24
+ has hard-write hooks or advisory MCP for Claude/Grok/Cursor/Codex, doctor/report no longer
25
+ open a `write-path-none` adoption gap. Session projection still reports `mode: none` for
26
+ honesty (other hosts' hooks are not a guarantee for this process). `detectActiveAgentHost`
27
+ also recognizes `GROK_AGENT`.
28
+
29
+ ### Added
30
+
31
+ - **Report metric hints:** HTML showcase KPIs (hero, adoption, contract density, debt) show
32
+ plain-language micro-copy under each tile plus native tooltips; PASS/mode badges and score
33
+ parts (Coverage/Clean/Gates/Rules) explain what they mean for newcomers.
34
+ - **Report design-depth strip:** `ark-check --report` includes doctor-parity Shape residual
35
+ (design-weak badge, smell outcomes, one next pilot, post-green door, optional golden pattern).
36
+ Clean ENFORCE with no smells shows a short “Design depth · OK” note (only when sensors ran).
37
+ - **Report adoption extras:** write-path line (active host · mode · inventory on disk) and a
38
+ fixed baseline-policy legend (`keep-empty` / `active-ratchet` / `absent`).
39
+
40
+ Release note: `docs/releases/3.0.4.md`.
8
41
 
9
42
  ## 3.0.3 — 2026-07-13
10
43
 
package/bin/ark-check.mjs CHANGED
@@ -58,6 +58,7 @@ import {
58
58
  reportsDir,
59
59
  readJsonSafe,
60
60
  } from './lib/html-report.mjs';
61
+ import { buildReportDepthPayload } from './lib/html-report-depth.mjs';
61
62
  import { shouldOpenHtmlReport, openHtmlInBrowser } from './lib/open-html.mjs';
62
63
  import {
63
64
  computeCoverage,
@@ -1266,6 +1267,13 @@ async function main() {
1266
1267
  const existingOrigin = args.resetOrigin
1267
1268
  ? null
1268
1269
  : readJsonSafe(path.join(reportsDir(root), 'origin.json'));
1270
+ const { adoption: adoptionForReport, designDepth } = buildReportDepthPayload(
1271
+ root,
1272
+ config,
1273
+ files,
1274
+ coverage,
1275
+ activeViolations
1276
+ );
1269
1277
  const reportPayload = {
1270
1278
  root,
1271
1279
  config,
@@ -1282,6 +1290,8 @@ async function main() {
1282
1290
  originSnapshot: existingOrigin,
1283
1291
  currentSnapshot,
1284
1292
  originJustCreated: !existingOrigin,
1293
+ adoption: adoptionForReport,
1294
+ designDepth,
1285
1295
  };
1286
1296
  const html = args.beginner
1287
1297
  ? renderBeginnerHtmlReport(reportPayload)
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Design-depth + adoption extras for the HTML showcase report.
3
+ * Kept separate from html-report.mjs so the main renderer stays under LOC budget.
4
+ * Does not import html-report.mjs (avoids a cycle).
5
+ */
6
+ import {
7
+ detectDesignSmells,
8
+ summarizeDesignFitness,
9
+ buildPatternBetsFromSmells,
10
+ } from './design-smells.mjs';
11
+ import { summarizePilotLoop } from './pilot-loop.mjs';
12
+ import { buildPostGreenNextAction } from './post-green-path.mjs';
13
+ import { loadGoldenPattern, summarizeGoldenPattern } from './golden-pattern.mjs';
14
+ import { collectAdoptionGaps } from './mcp-adoption.mjs';
15
+
16
+ function esc(value) {
17
+ return String(value)
18
+ .replace(/&/g, '&')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;');
22
+ }
23
+
24
+ /**
25
+ * Doctor-parity design depth + adoption for ark-check --report.
26
+ * @param {string} root
27
+ * @param {object} config
28
+ * @param {string[]} files
29
+ * @param {object} coverage
30
+ * @param {object[]} activeViolations
31
+ */
32
+ export function buildReportDepthPayload(root, config, files, coverage, activeViolations = []) {
33
+ const designSmells = detectDesignSmells(root, config, files, coverage);
34
+ const designFitness = summarizeDesignFitness(designSmells, {
35
+ activeViolations: activeViolations.length,
36
+ governedPercent: coverage?.governed?.percent,
37
+ totalFiles: coverage?.governed?.totalFiles,
38
+ });
39
+ const postGreenPath = buildPostGreenNextAction(designFitness);
40
+ const patternBets = buildPatternBetsFromSmells(designSmells);
41
+ const pilotLoop = summarizePilotLoop({
42
+ designWeak: designFitness.designWeak,
43
+ patternBets,
44
+ designSmells,
45
+ });
46
+ const goldenPattern = summarizeGoldenPattern(loadGoldenPattern(root));
47
+ const adoption = collectAdoptionGaps(root, config, coverage);
48
+ return {
49
+ adoption,
50
+ designDepth: {
51
+ designFitness,
52
+ designSmells,
53
+ pilotLoop,
54
+ postGreenPath,
55
+ goldenPattern,
56
+ },
57
+ };
58
+ }
59
+
60
+ /** Write-path mode → human meaning (active host projection). */
61
+ export function writePathModeHint(mode) {
62
+ switch (String(mode || '')) {
63
+ case 'repair':
64
+ return 'Hard write hook with repair payload — best co-pilot path for the active host.';
65
+ case 'reject-only':
66
+ return 'Hard write boundary without repair payload; edits can be blocked without guided re-entry.';
67
+ case 'mcp-only':
68
+ return 'Advisory MCP only — prepare-write/autoPatch available, no hard PreToolUse for this host.';
69
+ case 'none':
70
+ return 'No hard write boundary or advisory MCP for the active host (or host is unknown in this process).';
71
+ default:
72
+ return 'Session write-path capability for the active agent host.';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Configured hosts from writePath inventory (hard or advisory evidence on disk).
78
+ * @param {object|null|undefined} writePath
79
+ * @returns {string[]}
80
+ */
81
+ export function inventoryConfiguredHosts(writePath) {
82
+ const hosts = writePath?.inventory?.hosts;
83
+ if (!hosts || typeof hosts !== 'object') return [];
84
+ return Object.entries(hosts)
85
+ .filter(([, rec]) => rec && rec.configured)
86
+ .map(([name]) => name);
87
+ }
88
+
89
+ /**
90
+ * Compact write-path line for the Adoption card.
91
+ * @param {object|null|undefined} writePath
92
+ */
93
+ export function renderWritePathAdoptionBlock(writePath) {
94
+ if (!writePath || typeof writePath !== 'object') return '';
95
+ const mode = writePath.mode || 'none';
96
+ const host = writePath.activeHost || 'unknown';
97
+ const inv = inventoryConfiguredHosts(writePath);
98
+ const invLine =
99
+ inv.length > 0
100
+ ? `Inventory on disk: ${inv.map((h) => esc(h)).join(', ')}.`
101
+ : 'No host write gates found on disk yet.';
102
+ const unknownNote =
103
+ host === 'unknown' && inv.length > 0
104
+ ? ' Session host unknown (shell/CI) — inventory is still real; set ARK_ACTIVE_HOST or run from an agent for session-accurate mode.'
105
+ : '';
106
+ const gapNote = writePath.gap
107
+ ? ` Gap: <b>${esc(writePath.gap.id)}</b> — ${esc(writePath.gap.message || '')}`
108
+ : '';
109
+ // invLine / gapNote already include escaped user content; only plain strings go through esc().
110
+ return `<div class="write-path-block" title="${esc(writePathModeHint(mode))}">
111
+ <p class="dim" style="margin:.65rem 0 .2rem;font-size:.84rem">
112
+ <b>Write path</b> · active host <code>${esc(host)}</code>
113
+ · mode <code>${esc(mode)}</code>
114
+ ${writePath.hookRepair ? '· repair ✓' : writePath.hookPresent ? '· reject-only' : ''}
115
+ ${writePath.mcpPresent ? '· MCP ✓' : ''}
116
+ </p>
117
+ <p class="kpi-hint" style="max-width:none;margin:0">
118
+ ${esc(writePathModeHint(mode))} ${invLine}${esc(unknownNote)}${gapNote}
119
+ </p>
120
+ </div>`;
121
+ }
122
+
123
+ /** Fixed legend for baseline policy signals. */
124
+ export function renderBaselineSignalLegend() {
125
+ return `<details class="baseline-legend" style="margin-top:.75rem">
126
+ <summary>Baseline policy signals (legend)</summary>
127
+ <ul class="senior-list" style="margin-top:.4rem">
128
+ <li><b>keep-empty</b> — ${esc(baselineLegendBody('keep-empty'))}</li>
129
+ <li><b>active-ratchet</b> — ${esc(baselineLegendBody('active-ratchet'))}</li>
130
+ <li><b>absent</b> — ${esc(baselineLegendBody('absent'))}</li>
131
+ </ul>
132
+ </details>`;
133
+ }
134
+
135
+ function baselineLegendBody(signal) {
136
+ switch (signal) {
137
+ case 'keep-empty':
138
+ return '`.ark-baseline.json` exists with 0 frozen keys; every violation is active (honest green).';
139
+ case 'active-ratchet':
140
+ return 'Known debt keys are frozen; new distinct violations still fail the check.';
141
+ case 'absent':
142
+ return 'No baseline file — all findings are active (or freeze not adopted).';
143
+ default:
144
+ return '';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Design-weak / Shape residual strip for the showcase report.
150
+ * Null HTML when there is nothing useful to show.
151
+ *
152
+ * @param {{
153
+ * designFitness?: object|null,
154
+ * designSmells?: object[],
155
+ * pilotLoop?: object|null,
156
+ * postGreenPath?: object|null,
157
+ * goldenPattern?: object|null,
158
+ * mode?: string,
159
+ * }} depth
160
+ */
161
+ export function renderDesignDepthStrip(depth = {}) {
162
+ const fitness = depth.designFitness;
163
+ const smells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
164
+ const designWeak = fitness?.designWeak === true;
165
+ if (!designWeak && smells.length === 0) return '';
166
+
167
+ const mode = String(depth.mode || '').toLowerCase();
168
+ const title = designWeak
169
+ ? mode === 'enforce'
170
+ ? 'ENFORCE · design-weak'
171
+ : `${(mode || 'edges').toUpperCase()} · design-weak`
172
+ : 'Design smells (edges still open)';
173
+ const lede = designWeak
174
+ ? 'Contract edges are clean, but lived design residual remains. This does not fail PASS — it blocks “healthy finished” until Shape work lands.'
175
+ : 'Design smells exist alongside open edge debt. Fix edges first; treat smells as Shape residual after green.';
176
+
177
+ const smellItems = smells
178
+ .slice(0, 6)
179
+ .map((s) => {
180
+ const outcome = s.outcome || s.message || s.id;
181
+ const evidence = (s.evidence || [])
182
+ .filter((e) => typeof e === 'string' && !e.startsWith('layer:') && !e.startsWith('layout:'))
183
+ .slice(0, 3);
184
+ const ev =
185
+ evidence.length > 0
186
+ ? ` <span class="dim">· ${evidence.map((e) => `<code>${esc(e)}</code>`).join(' ')}</span>`
187
+ : '';
188
+ return `<li><b>${esc(s.id)}</b> — ${esc(outcome)}${ev}</li>`;
189
+ })
190
+ .join('');
191
+
192
+ const pilot = depth.pilotLoop?.active && depth.pilotLoop?.nextPilot ? depth.pilotLoop.nextPilot : null;
193
+ const pilotHtml = pilot
194
+ ? `<div class="pilot-card">
195
+ <h3 style="margin-top:.85rem">Next pilot (one at a time)</h3>
196
+ <p class="dim" style="margin:.15rem 0 .4rem;font-size:.86rem">
197
+ Judgment only — never mechanical-safe · never multi-pilot batch
198
+ </p>
199
+ <ul class="senior-list">
200
+ <li><b>Smell</b> · <code>${esc(pilot.smellId || pilot.id || '—')}</code></li>
201
+ <li><b>Target</b> · <code>${esc(pilot.pilotTarget || pilot.pilot || '—')}</code></li>
202
+ ${
203
+ pilot.move || pilot.fix
204
+ ? `<li><b>Move</b> · ${esc(pilot.move || pilot.fix)}</li>`
205
+ : ''
206
+ }
207
+ ${
208
+ pilot.successSignal
209
+ ? `<li><b>Success</b> · ${esc(pilot.successSignal)}</li>`
210
+ : ''
211
+ }
212
+ ${
213
+ pilot.killSwitch
214
+ ? `<li><b>Kill-switch</b> · ${esc(pilot.killSwitch)}</li>`
215
+ : ''
216
+ }
217
+ </ul>
218
+ </div>`
219
+ : '';
220
+
221
+ const next =
222
+ depth.postGreenPath?.short ||
223
+ depth.postGreenPath?.action ||
224
+ (designWeak
225
+ ? '/ark-explore shape-focus → dual-plan B, then /ark-autopilot only with OK'
226
+ : null);
227
+ const nextHtml = next
228
+ ? `<p class="meta" style="margin-top:.75rem"><b>Primary next</b> · ${esc(next)}</p>`
229
+ : '';
230
+
231
+ const golden = depth.goldenPattern;
232
+ const goldenHtml =
233
+ golden && golden.present !== false && (golden.name || golden.norm)
234
+ ? `<p class="dim" style="margin-top:.5rem;font-size:.84rem">
235
+ Golden pattern (advisory for <b>new</b> code only):
236
+ <code>${esc(golden.name || 'pattern')}</code>
237
+ ${golden.norm ? ` — ${esc(golden.norm)}` : ''}
238
+ ${golden.examplePath ? ` · e.g. <code>${esc(golden.examplePath)}</code>` : ''}
239
+ </p>`
240
+ : designWeak
241
+ ? `<p class="dim" style="margin-top:.5rem;font-size:.84rem">
242
+ No <code>.ark/golden-pattern.json</code> yet — optional; helps agents place <b>new</b> code only.
243
+ </p>`
244
+ : '';
245
+
246
+ return `<div class="section card design-strip ${designWeak ? 'is-weak' : 'has-smells'}" id="design-depth">
247
+ <div class="design-head">
248
+ <span class="badge design" title="Shape residual — separate from PASS/FAIL edge honesty">${esc(title)}</span>
249
+ <span class="dim" style="font-size:.86rem">${designWeak ? 'Edges clean · residual remains' : 'Smells + open edges'}</span>
250
+ </div>
251
+ <p class="dim" style="margin:.55rem 0 .5rem;font-size:.9rem">${esc(lede)}</p>
252
+ ${smellItems ? `<ul class="senior-list">${smellItems}</ul>` : ''}
253
+ ${pilotHtml}
254
+ ${nextHtml}
255
+ ${goldenHtml}
256
+ </div>`;
257
+ }
258
+
259
+ /**
260
+ * Optional clean-depth note when edges + design are both healthy.
261
+ * Requires designFitness from a real sensor run (object). Null/undefined means
262
+ * depth was not computed — do not claim “OK” from missing data.
263
+ * @param {{ designFitness?: object|null, ok?: boolean, mode?: string }} depth
264
+ */
265
+ export function renderDesignCleanNote(depth = {}) {
266
+ if (!depth.ok) return '';
267
+ // Sensors never ran (callers that omit designDepth) → no strip.
268
+ if (depth.designFitness == null || typeof depth.designFitness !== 'object') return '';
269
+ if (depth.designFitness.designWeak) return '';
270
+ if ((depth.designFitness.smellCount ?? 0) > 0) return '';
271
+ if (String(depth.mode || '').toLowerCase() !== 'enforce') return '';
272
+ return `<div class="section card design-strip is-clean" id="design-depth">
273
+ <div class="design-head">
274
+ <span class="badge design-ok" title="No deterministic design smells with clean edges">Design depth · OK</span>
275
+ <span class="dim" style="font-size:.86rem">No design-weak residual detected</span>
276
+ </div>
277
+ <p class="dim" style="margin:.45rem 0 0;font-size:.88rem">
278
+ Edges and deterministic design sensors agree. Keep placing new code on the golden path;
279
+ re-run doctor after large refactors.
280
+ </p>
281
+ </div>`;
282
+ }
@@ -11,6 +11,13 @@ import {
11
11
  resolveOperatingMode,
12
12
  } from '../ark-shared.mjs';
13
13
  import { collectAdoptionGaps, arkCheckCommand } from './agent-gates.mjs';
14
+ import { CORE_LAYER_NAMES } from './core-layers.mjs';
15
+ import {
16
+ renderBaselineSignalLegend,
17
+ renderDesignCleanNote,
18
+ renderDesignDepthStrip,
19
+ renderWritePathAdoptionBlock,
20
+ } from './html-report-depth.mjs';
14
21
  import { FIX_HINTS } from './violations.mjs';
15
22
 
16
23
  export function detectEnforcement(root) {
@@ -58,6 +65,53 @@ export function htmlEscape(value) {
58
65
  .replace(/"/g, '&quot;');
59
66
  }
60
67
 
68
+ /**
69
+ * KPI tile with plain-language hint (visible micro-copy + native tooltip).
70
+ * Helps newcomers read the showcase without memorizing Ark jargon.
71
+ *
72
+ * @param {string|number} value
73
+ * @param {string} label short metric name
74
+ * @param {string} hint one-sentence meaning
75
+ */
76
+ export function metricKpi(value, label, hint) {
77
+ const v = htmlEscape(String(value));
78
+ const l = htmlEscape(label);
79
+ const h = htmlEscape(hint);
80
+ return `<div class="kpi" title="${h}" aria-label="${l}: ${v}. ${h}">
81
+ <b>${v}</b>
82
+ <span>${l}</span>
83
+ <em class="kpi-hint">${h}</em>
84
+ </div>`;
85
+ }
86
+
87
+ /** Baseline policy signal → human meaning (adoption card). */
88
+ export function baselineSignalHint(signal) {
89
+ switch (String(signal || '')) {
90
+ case 'keep-empty':
91
+ return 'Baseline file exists and freezes 0 keys — every violation is active (honest green).';
92
+ case 'active-ratchet':
93
+ return 'Baseline freezes known debt keys; new distinct violations still fail the check.';
94
+ case 'absent':
95
+ return 'No .ark-baseline.json — all findings are active (or you have not adopted a freeze file).';
96
+ default:
97
+ return 'How frozen debt is handled relative to active architecture violations.';
98
+ }
99
+ }
100
+
101
+ /** Operating mode badge tooltip. */
102
+ export function modeBadgeHint(mode) {
103
+ switch (String(mode || '').toLowerCase()) {
104
+ case 'enforce':
105
+ return 'Contract matches the tree: cores are required where populated, coverage is honest, gates can hold the line.';
106
+ case 'adapt':
107
+ return 'Contract is live but still aligning (optional cores with files, empty cores, or presentation-bag false green).';
108
+ case 'suggest':
109
+ return 'Starter shape — expand layers and raise governed coverage as the codebase grows.';
110
+ default:
111
+ return 'Operating mode for co-pilot surfaces (suggest · adapt · enforce).';
112
+ }
113
+ }
114
+
61
115
  /** Directory for origin / latest / history architecture report snapshots. */
62
116
  const ARK_REPORTS_DIR = path.join('.ark', 'reports');
63
117
  const ARK_REPORT_HISTORY_MAX = 20;
@@ -159,7 +213,10 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
159
213
  const presentationRow = (coverage?.layers ?? []).find(
160
214
  (r) => r.name === 'PresentationAdapters'
161
215
  );
216
+ // Same honesty gate as doctor (`mcp-adoption` coreOptional): only the four cores
217
+ // matter. Secondary optional layers with files must not force ADAPT on the HTML report.
162
218
  const coreOptionalWithFiles = (config?.layers ?? []).filter((layer) => {
219
+ if (!CORE_LAYER_NAMES.has(layer.name)) return false;
163
220
  if (layer.optional !== true) return false;
164
221
  const row = (coverage?.layers ?? []).find((r) => r.name === layer.name);
165
222
  return (row?.files ?? 0) > 0;
@@ -396,6 +453,8 @@ export function renderHtmlReport({
396
453
  currentSnapshot = null,
397
454
  originJustCreated = false,
398
455
  adoption = null,
456
+ /** Optional design-depth (doctor parity): designFitness, designSmells, pilotLoop, postGreenPath, goldenPattern */
457
+ designDepth = null,
399
458
  }) {
400
459
  const layers = Array.isArray(config.layers) ? config.layers : [];
401
460
  const rules = Array.isArray(config.rules) ? config.rules : [];
@@ -448,6 +507,29 @@ export function renderHtmlReport({
448
507
  } = fitness;
449
508
 
450
509
  const adoptionView = adoption || collectAdoptionGaps(root, config, coverage);
510
+ const depth = designDepth && typeof designDepth === 'object' ? designDepth : {};
511
+ const designFitness = depth.designFitness ?? null;
512
+ const designSmells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
513
+ const designWeakBadge =
514
+ designFitness?.designWeak === true
515
+ ? ` <span class="badge design" title="Edges can be green while lived design residual remains (Shape). Not a FAIL.">design-weak</span>`
516
+ : '';
517
+ const designStripHtml =
518
+ renderDesignDepthStrip({
519
+ designFitness,
520
+ designSmells,
521
+ pilotLoop: depth.pilotLoop,
522
+ postGreenPath: depth.postGreenPath,
523
+ goldenPattern: depth.goldenPattern,
524
+ mode,
525
+ }) ||
526
+ renderDesignCleanNote({
527
+ designFitness,
528
+ ok,
529
+ mode,
530
+ });
531
+ const writePathHtml = renderWritePathAdoptionBlock(adoptionView.writePath);
532
+ const baselineLegendHtml = renderBaselineSignalLegend();
451
533
 
452
534
  // ── Senior diagnostics (coupling, purity, contract density) ──────────────
453
535
  const layerNames = ordered.map((l) => l.name);
@@ -818,9 +900,33 @@ export function renderHtmlReport({
818
900
  .score-cap { color: var(--dim); font-size: .85rem; margin: 0; }
819
901
  .kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: .65rem; margin: 1rem 0 0; }
820
902
  @media (max-width: 720px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
821
- .kpi { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: .7rem .8rem; }
903
+ .kpi { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: .7rem .8rem; cursor: help; }
822
904
  .kpi b { display: block; font-size: 1.25rem; letter-spacing: -0.02em; }
823
905
  .kpi span { color: var(--dim); font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; }
906
+ .kpi-hint {
907
+ display: block; margin-top: .4rem; color: var(--dim); font-size: .68rem; font-style: normal;
908
+ font-weight: 450; line-height: 1.35; letter-spacing: 0; text-transform: none; max-width: 16rem;
909
+ }
910
+ .score-parts span { cursor: help; border-bottom: 1px dotted color-mix(in srgb, var(--dim) 55%, transparent); }
911
+ .badge[title] { cursor: help; }
912
+ .badge.design {
913
+ background: color-mix(in srgb, var(--gold) 18%, transparent); color: var(--gold);
914
+ border-color: color-mix(in srgb, var(--gold) 40%, transparent);
915
+ }
916
+ .badge.design-ok {
917
+ background: color-mix(in srgb, var(--green) 16%, transparent); color: var(--green);
918
+ border-color: color-mix(in srgb, var(--green) 35%, transparent);
919
+ }
920
+ .design-strip { border-left: 3px solid var(--gold); }
921
+ .design-strip.is-clean { border-left-color: var(--green); }
922
+ .design-strip.has-smells { border-left-color: var(--gold); }
923
+ .design-head { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
924
+ .pilot-card {
925
+ margin-top: .35rem; padding: .75rem .9rem; border-radius: 12px;
926
+ background: var(--panel2); border: 1px solid var(--line);
927
+ }
928
+ .write-path-block { margin-top: .15rem; }
929
+ .baseline-legend summary { cursor: pointer; color: var(--dim); font-size: .84rem; }
824
930
  .section { margin-top: 1.35rem; }
825
931
  .grid-2 { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 1rem; }
826
932
  @media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
@@ -936,34 +1042,79 @@ export function renderHtmlReport({
936
1042
  <div class="hero">
937
1043
  <div class="card">
938
1044
  <div class="brand"><i></i> Ark architecture report</div>
939
- <h1>${esc(project)} <span class="badge ${status}">${status}</span> <span class="badge mode">${esc(modeLabel)}</span></h1>
940
- <p class="lede">${esc(modeBlurb)} One machine-readable contract · write gate · CI · optional runtime.</p>
1045
+ <h1>${esc(project)} <span class="badge ${status}" title="${status === 'PASS' ? 'Architecture check is green: 0 active violations against the contract.' : 'Architecture check failed: active violations remain (or the scan could not complete cleanly).'}">${status}</span> <span class="badge mode" title="${esc(modeBadgeHint(mode))}">${esc(modeLabel)}</span>${designWeakBadge}</h1>
1046
+ <p class="lede">${esc(modeBlurb)}${designFitness?.designWeak ? ' Design residual remains (see strip below) — not a FAIL.' : ''} One machine-readable contract · write gate · CI · optional runtime.</p>
941
1047
  <div class="kpis">
942
- <div class="kpi"><b>${esc(govLabel)}</b><span>Governed</span></div>
943
- <div class="kpi"><b>${layers.length}</b><span>Layers</span></div>
944
- <div class="kpi"><b>${gatesOn}/${enforcement.length}</b><span>Gates live</span></div>
945
- <div class="kpi"><b>${violations.length}${suppressed ? ` · ${suppressed}Δ` : ''}</b><span>Violations${suppressed ? ' · frozen' : ''}</span></div>
1048
+ ${metricKpi(
1049
+ govLabel,
1050
+ 'Governed',
1051
+ 'Share of scanned files assigned to a contract layer. 100% means every in-scope file has a home.'
1052
+ )}
1053
+ ${metricKpi(
1054
+ layers.length,
1055
+ 'Layers',
1056
+ 'How many architecture layers the contract defines (cores + optional product layers).'
1057
+ )}
1058
+ ${metricKpi(
1059
+ `${gatesOn}/${enforcement.length}`,
1060
+ 'Gates live',
1061
+ 'Write hook, CI workflow, ESLint plugin, and baseline file — how many enforcement points are actually present.'
1062
+ )}
1063
+ ${metricKpi(
1064
+ `${violations.length}${suppressed ? ` · ${suppressed}Δ` : ''}`,
1065
+ `Violations${suppressed ? ' · frozen' : ''}`,
1066
+ suppressed
1067
+ ? 'Active contract breaks right now; Δ = keys frozen in baseline (not failing until ratchet).'
1068
+ : 'Active contract breaks (layer imports, purity, etc.). Zero means edges match the rules.'
1069
+ )}
946
1070
  </div>
947
1071
  <p class="meta">${meta}</p>
948
1072
  ${skillsNote}
949
1073
  </div>
950
- <div class="card score-card">
1074
+ <div class="card score-card" title="Human fitness signal only — not a CI gate. Weighted blend of coverage, cleanliness, live gates, and rule density.">
951
1075
  <div class="score-ring ${scoreTone}"><div><div class="score-n">${score}</div><div class="dim" style="font-size:.72rem;letter-spacing:.08em;text-transform:uppercase">Ark score</div></div></div>
952
1076
  <p class="score-cap">${esc(scoreCaption)}</p>
953
- <p class="meta" style="margin-top:.65rem">Coverage ${scoreCoverage} · Clean ${scoreClean} · Gates ${scoreGates} · Rules ${scoreRules}</p>
1077
+ <p class="meta score-parts" style="margin-top:.65rem">
1078
+ <span title="0.4 weight — governed file percent (or 50 if coverage unknown).">${esc(`Coverage ${scoreCoverage}`)}</span>
1079
+ · <span title="0.3 weight — 100 with zero active violations; drops as violations pile up.">${esc(`Clean ${scoreClean}`)}</span>
1080
+ · <span title="0.2 weight — share of enforcement points that are present on disk (hook, CI, ESLint, baseline).">${esc(`Gates ${scoreGates}`)}</span>
1081
+ · <span title="0.1 weight — how dense the deny matrix is relative to layer pairs (stricter inward architecture scores higher).">${esc(`Rules ${scoreRules}`)}</span>
1082
+ </p>
954
1083
  </div>
955
1084
  </div>
956
1085
 
1086
+ ${designStripHtml}
1087
+
957
1088
  <div class="section card" id="adoption">
958
1089
  <h2>Adoption</h2>
959
1090
  <p class="dim" style="margin:.15rem 0 .75rem;font-size:.88rem">
960
1091
  Co-pilot completeness — separate from the 0–100 fitness score above. Hosts, MCP health, origin snapshot, core optionality, baseline policy.
961
1092
  </p>
962
1093
  <div class="kpis" style="margin-bottom:.75rem">
963
- <div class="kpi"><b>${adoptionView.gaps.length === 0 ? 'OK' : adoptionView.gaps.length}</b><span>${adoptionView.gaps.length === 0 ? 'No adoption gaps' : 'Adoption gap(s)'}</span></div>
964
- <div class="kpi"><b>${adoptionView.originReport.present ? 'yes' : 'no'}</b><span>Origin report</span></div>
965
- <div class="kpi"><b>${esc(adoptionView.baseline.signal)}</b><span>Baseline policy</span></div>
966
- <div class="kpi"><b>${adoptionView.mcp.ok ? 'ok' : 'fix'}</b><span>Repo MCP argv</span></div>
1094
+ ${metricKpi(
1095
+ adoptionView.gaps.length === 0 ? 'OK' : adoptionView.gaps.length,
1096
+ adoptionView.gaps.length === 0 ? 'No adoption gaps' : 'Adoption gap(s)',
1097
+ adoptionView.gaps.length === 0
1098
+ ? 'Hosts, MCP argv, origin snapshot, and core optionality look complete for co-pilot use.'
1099
+ : 'Install or fix the listed gaps so agents get write gates, MCP, and honest cores.'
1100
+ )}
1101
+ ${metricKpi(
1102
+ adoptionView.originReport.present ? 'yes' : 'no',
1103
+ 'Origin report',
1104
+ 'First architecture snapshot under .ark/reports/origin.* — future reports show evolution deltas against it.'
1105
+ )}
1106
+ ${metricKpi(
1107
+ adoptionView.baseline.signal,
1108
+ 'Baseline policy',
1109
+ baselineSignalHint(adoptionView.baseline.signal)
1110
+ )}
1111
+ ${metricKpi(
1112
+ adoptionView.mcp.ok ? 'ok' : 'fix',
1113
+ 'Repo MCP argv',
1114
+ adoptionView.mcp.ok
1115
+ ? 'Repo MCP config points at a single ark/arkgate MCP bin (no dual-bin conflict).'
1116
+ : 'Broken MCP argv: more than one of ark-mcp/arkgate-mcp — migrate with --install-agent-gates --migrate-commands.'
1117
+ )}
967
1118
  </div>
968
1119
  ${
969
1120
  adoptionView.gaps.length
@@ -991,6 +1142,8 @@ export function renderHtmlReport({
991
1142
  .join(' · ')}</p>`
992
1143
  : ''
993
1144
  }
1145
+ ${writePathHtml}
1146
+ ${baselineLegendHtml}
994
1147
  </div>
995
1148
 
996
1149
  <div class="section grid-2">
@@ -1138,10 +1291,26 @@ export function renderHtmlReport({
1138
1291
 
1139
1292
  <h3>Contract density</h3>
1140
1293
  <div class="kpis" style="margin-top:.35rem">
1141
- <div class="kpi"><b>${denyRatio}%</b><span>Edges denied</span></div>
1142
- <div class="kpi"><b>${deniedCount}</b><span>Deny rules</span></div>
1143
- <div class="kpi"><b>${allowedCount}</b><span>Explicit allows</span></div>
1144
- <div class="kpi"><b>${pairCount}</b><span>Directed pairs</span></div>
1294
+ ${metricKpi(
1295
+ `${denyRatio}%`,
1296
+ 'Edges denied',
1297
+ 'Denied directed layer pairs ÷ all possible pairs. Higher = stricter inward dependency rules.'
1298
+ )}
1299
+ ${metricKpi(
1300
+ deniedCount,
1301
+ 'Deny rules',
1302
+ 'Explicit allowed:false rules in ark.config.json (row may not import column).'
1303
+ )}
1304
+ ${metricKpi(
1305
+ allowedCount,
1306
+ 'Explicit allows',
1307
+ 'Explicit allowed:true edges. Most opens are implicit (no rule) unless you document them.'
1308
+ )}
1309
+ ${metricKpi(
1310
+ pairCount,
1311
+ 'Directed pairs',
1312
+ 'layers × (layers − 1) — every ordered from→to pair the matrix can constrain.'
1313
+ )}
1145
1314
  </div>
1146
1315
  <p class="dim" style="margin:.55rem 0 0;font-size:.84rem">
1147
1316
  Deny ratio = denied ÷ (layers × (layers−1)). High ratio = strict inward architecture.
@@ -1270,10 +1439,26 @@ export function renderHtmlReport({
1270
1439
 
1271
1440
  <h3>Debt &amp; violation taxonomy</h3>
1272
1441
  <div class="kpis" style="margin-top:.35rem">
1273
- <div class="kpi"><b>${violations.length}</b><span>Active</span></div>
1274
- <div class="kpi"><b>${valueN}</b><span>Value edges</span></div>
1275
- <div class="kpi"><b>${typeOnlyN}</b><span>Type-only</span></div>
1276
- <div class="kpi"><b>${suppressed || baselineKeys}</b><span>Baseline keys</span></div>
1442
+ ${metricKpi(
1443
+ violations.length,
1444
+ 'Active',
1445
+ 'Violations that fail the check right now (not frozen by baseline).'
1446
+ )}
1447
+ ${metricKpi(
1448
+ valueN,
1449
+ 'Value edges',
1450
+ 'Runtime import edges that cross a deny rule (stronger debt than type-only).'
1451
+ )}
1452
+ ${metricKpi(
1453
+ typeOnlyN,
1454
+ 'Type-only',
1455
+ 'Type-only imports across a deny edge — often mechanical-safe to rewrite as import type.'
1456
+ )}
1457
+ ${metricKpi(
1458
+ suppressed || baselineKeys,
1459
+ 'Baseline keys',
1460
+ 'Distinct frozen debt keys in .ark-baseline.json (or suppressed count for this run).'
1461
+ )}
1277
1462
  </div>
1278
1463
  ${
1279
1464
  topEdges.length
@@ -1291,6 +1476,14 @@ export function renderHtmlReport({
1291
1476
  Coverage=${scoreCoverage}, clean=${scoreClean}, gates=${scoreGates}, rules=${scoreRules} → <b>${score}</b>.
1292
1477
  This is a fitness signal for humans, not a CI gate.
1293
1478
  </p>
1479
+ <ul class="senior-list" style="margin-top:.45rem">
1480
+ <li><b>Coverage</b> — % of in-scope files that match a layer pattern.</li>
1481
+ <li><b>Clean</b> — 100 with zero active violations; falls as breaks accumulate.</li>
1482
+ <li><b>Gates</b> — share of write / CI / ESLint / baseline enforcement points present.</li>
1483
+ <li><b>Rules</b> — deny-matrix density (more inward denies → higher component).</li>
1484
+ <li><b>PASS / FAIL</b> — binary edge honesty (active violations), independent of the 0–100 score.</li>
1485
+ <li><b>SUGGEST / ADAPT / ENFORCE</b> — whether the contract is honest enough to protect the tree (not a skill grade).</li>
1486
+ </ul>
1294
1487
  </details>
1295
1488
  </div>
1296
1489
 
@@ -43,10 +43,11 @@ export function detectActiveAgentHost(env = process.env) {
43
43
  .toLowerCase();
44
44
  if (explicit) return explicit;
45
45
 
46
- // Grok / xAI Build
46
+ // Grok / xAI Build (include GROK_AGENT — common session signal missing in older detect)
47
47
  if (
48
48
  envTruthy(env.GROK_BUILD) ||
49
49
  envTruthy(env.XAI_GROK) ||
50
+ envTruthy(env.GROK_AGENT) ||
50
51
  env.GROK_WORKSPACE_ROOT ||
51
52
  env.GROK_SESSION_ID
52
53
  ) {
@@ -30,21 +30,32 @@ export function detectWritePathCapabilities(root, explicitHost) {
30
30
 
31
31
  const tools = installToolsForHost(activeHost);
32
32
  let gap = null;
33
+ // Repo inventory (any host) can show hard/advisory write while activeHost is
34
+ // unknown (plain shell / `npx ark-check --report` outside an agent session).
35
+ // Session projection stays mode=none (other hosts' hooks are not a guarantee for
36
+ // this process) — but do not open an adoption gap: gates exist on disk.
37
+ const inventoryHasWriteBoundary =
38
+ Boolean(inventory?.capabilities?.['hard-write']) ||
39
+ Boolean(inventory?.capabilities?.['advisory-write']);
33
40
  if (mode === 'none') {
34
- gap = {
35
- id: 'write-path-none',
36
- severity: 'warn',
37
- message:
38
- `Active host ${activeHost} has no hard write boundary or advisory Ark MCP. ` +
39
- (capabilities['merge-gate']
40
- ? 'The CI check remains separate and does not block local writes.'
41
- : 'No Ark CI check was detected either.'),
42
- fix: arkCommand(
43
- root,
44
- 'ark-check',
45
- `--install-agent-gates --tools ${tools}`
46
- ),
47
- };
41
+ if (activeHost === 'unknown' && inventoryHasWriteBoundary) {
42
+ gap = null;
43
+ } else {
44
+ gap = {
45
+ id: 'write-path-none',
46
+ severity: 'warn',
47
+ message:
48
+ `Active host ${activeHost} has no hard write boundary or advisory Ark MCP. ` +
49
+ (capabilities['merge-gate']
50
+ ? 'The CI check remains separate and does not block local writes.'
51
+ : 'No Ark CI check was detected either.'),
52
+ fix: arkCommand(
53
+ root,
54
+ 'ark-check',
55
+ `--install-agent-gates --tools ${tools}`
56
+ ),
57
+ };
58
+ }
48
59
  } else if (mode === 'reject-only') {
49
60
  gap = {
50
61
  id: 'write-path-reject-only',
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ __export(gate_exports, {
50
50
  module.exports = __toCommonJS(gate_exports);
51
51
 
52
52
  // src/version.ts
53
- var version = "3.0.3";
53
+ var version = "3.0.4";
54
54
 
55
55
  // src/domain/adapterContract.ts
56
56
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.cjs';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.3";
5
+ declare const version = "3.0.4";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.js';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.3";
5
+ declare const version = "3.0.4";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var version = "3.0.3";
2
+ var version = "3.0.4";
3
3
 
4
4
  // src/domain/adapterContract.ts
5
5
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkgate",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "ArkGate — architecture co-pilot for AI TypeScript (write gate, CI gate, plan/loop)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/pedroknigge/arkgate",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.3",
9
+ "version": "3.0.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "arkgate",
14
- "version": "3.0.3",
14
+ "version": "3.0.4",
15
15
  "runtimeHint": "npx",
16
16
  "transport": {
17
17
  "type": "stdio"