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.
Files changed (48) hide show
  1. package/CHANGELOG.md +69 -2
  2. package/README.md +5 -4
  3. package/bin/ark-check-runtime.mjs +4 -0
  4. package/bin/ark-mcp-runtime.mjs +49 -0
  5. package/bin/ark-shared.mjs +8 -29
  6. package/bin/ark.mjs +7 -3
  7. package/bin/lib/adapter-contract.mjs +5 -5
  8. package/bin/lib/analysis-engine.mjs +1 -1
  9. package/bin/lib/ci-and-commands.mjs +5 -0
  10. package/bin/lib/deep-module-coach.mjs +177 -0
  11. package/bin/lib/deepening-coach.mjs +177 -0
  12. package/bin/lib/doctor-plan.mjs +14 -0
  13. package/bin/lib/html-report-advisories.mjs +33 -0
  14. package/bin/lib/html-report-depth.mjs +9 -0
  15. package/bin/lib/managed-upgrade.mjs +215 -1
  16. package/bin/lib/remediation.mjs +5 -5
  17. package/bin/lib/rules-inventory.mjs +23 -0
  18. package/bin/lib/upgrade-command.mjs +132 -11
  19. package/bin/lib/upgrade-package-decision.mjs +241 -0
  20. package/bin/lib/upgrade-whats-new.mjs +135 -0
  21. package/dist/eslint/index.cjs +1 -1
  22. package/dist/eslint/index.js +1 -1
  23. package/dist/index.cjs +27 -27
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +29 -29
  26. package/docs/README.md +4 -5
  27. package/docs/agent-guide.md +36 -0
  28. package/docs/brownfield-adoption.md +12 -0
  29. package/docs/package-surface.md +6 -3
  30. package/docs/product-voice.md +21 -0
  31. package/docs/use.md +3 -0
  32. package/package.json +1 -1
  33. package/server.json +2 -2
  34. package/templates/agent-skills/README.md +2 -2
  35. package/templates/agent-skills/ark-adopt/SKILL.md +13 -0
  36. package/templates/agent-skills/ark-explore/SKILL.md +21 -0
  37. package/templates/agent-skills/ark-fix/SKILL.md +7 -0
  38. package/templates/agent-skills/ark-loop/SKILL.md +7 -0
  39. package/templates/agent-skills/ark-place/SKILL.md +7 -0
  40. package/templates/agent-skills/ark-think/SKILL.md +7 -0
  41. package/templates/agent-skills/ark-upgrade/SKILL.md +61 -15
  42. package/templates/skills/ark-adopt.md +13 -0
  43. package/templates/skills/ark-explore.md +21 -0
  44. package/templates/skills/ark-fix.md +7 -0
  45. package/templates/skills/ark-loop.md +7 -0
  46. package/templates/skills/ark-place.md +7 -0
  47. package/templates/skills/ark-think.md +7 -0
  48. package/templates/skills/ark-upgrade.md +61 -15
@@ -4,7 +4,11 @@ import fs from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
6
 
7
- import { arkCommand } from '../ark-shared.mjs';
7
+ import {
8
+ arkCommand,
9
+ buildPackageInstallSkipPayload,
10
+ formatPackageInstallDecisionHuman,
11
+ } from '../ark-shared.mjs';
8
12
  import { describePackageVersionDualTruth } from './field-install.mjs';
9
13
  import { __packageRoot } from './gate-files.mjs';
10
14
  import {
@@ -12,6 +16,9 @@ import {
12
16
  managedUpgradeJson,
13
17
  planManagedUpgrade,
14
18
  renderManagedUpgrade,
19
+ buildSkillDriftSummary,
20
+ buildPostUpgradeChecks,
21
+ buildHostSelectionHonesty,
15
22
  } from './managed-upgrade.mjs';
16
23
 
17
24
  function installedCli(root) {
@@ -277,6 +284,7 @@ function previewArgs(args) {
277
284
  const next = ['upgrade', '--root', args.root, '--no-install'];
278
285
  if (args.tools) next.push('--tools', args.tools);
279
286
  if (args.acceptConflicts) next.push('--accept-conflicts');
287
+ if (args.refreshSkills) next.push('--refresh-skills');
280
288
  if (!args.strict) next.push('--no-strict');
281
289
  if (args.json) next.push('--json');
282
290
  return next;
@@ -299,6 +307,7 @@ export function buildUpgradeNextCommand(args, planDigest) {
299
307
  if (!args.install && planDigest) flagParts.push('--plan-digest', planDigest);
300
308
  if (args.tools) flagParts.push('--tools', args.tools);
301
309
  if (args.acceptConflicts) flagParts.push('--accept-conflicts');
310
+ if (args.refreshSkills) flagParts.push('--refresh-skills');
302
311
  if (!args.strict) flagParts.push('--no-strict');
303
312
  if (args.json) flagParts.push('--json');
304
313
  const argsStr = flagParts.map(quote).join(' ');
@@ -357,19 +366,91 @@ export function runUpgradeCommand(args, dependencies) {
357
366
  }
358
367
 
359
368
  if (args.apply && args.install) {
360
- const skip =
369
+ // FX01–FX02: registry-aware skip + structured skip truth (injectable probe for tests).
370
+ const skipOptions = {
371
+ ...(dependencies.registryLatest !== undefined
372
+ ? { registryLatest: dependencies.registryLatest }
373
+ : {}),
374
+ ...(typeof dependencies.getRegistryLatest === 'function'
375
+ ? { getRegistryLatest: dependencies.getRegistryLatest }
376
+ : {}),
377
+ ...(dependencies.skipRegistryProbe === true ? { skipRegistryProbe: true } : {}),
378
+ };
379
+ const decisionRaw =
361
380
  typeof dependencies.shouldSkipArkgateInstall === 'function'
362
- ? dependencies.shouldSkipArkgateInstall(root, dependencies.cliVersion)
363
- : { skip: false };
364
- if (skip.skip) {
381
+ ? dependencies.shouldSkipArkgateInstall(root, dependencies.cliVersion, skipOptions)
382
+ : {
383
+ skip: false,
384
+ reasonCode: 'NOT_INSTALLED',
385
+ installedVersion: null,
386
+ registryLatest: null,
387
+ cliVersion: dependencies.cliVersion ?? null,
388
+ reason: 'not-installed',
389
+ };
390
+ // Normalize injectable mocks that only return { skip, installedVersion }.
391
+ const decision = {
392
+ ...decisionRaw,
393
+ reasonCode:
394
+ decisionRaw.reasonCode ||
395
+ (decisionRaw.skip
396
+ ? 'ALREADY_CURRENT'
397
+ : decisionRaw.installedVersion
398
+ ? 'VERSION_DIFFERS'
399
+ : 'NOT_INSTALLED'),
400
+ reason:
401
+ decisionRaw.reason ||
402
+ (decisionRaw.skip ? 'already-current' : 'version-differs'),
403
+ cliVersion: decisionRaw.cliVersion ?? dependencies.cliVersion ?? null,
404
+ registryLatest: decisionRaw.registryLatest ?? null,
405
+ };
406
+ const installArgv =
407
+ typeof dependencies.packageInstallArgv === 'function'
408
+ ? dependencies.packageInstallArgv
409
+ : null;
410
+ const targetSpec =
411
+ decision.registryLatest && decision.reasonCode === 'BEHIND_REGISTRY'
412
+ ? decision.registryLatest
413
+ : 'latest';
414
+ function fallbackPayload(spec = targetSpec, skipped = decision.skip === true) {
415
+ return {
416
+ schemaVersion: '1.0',
417
+ notAScore: true,
418
+ packageInstallSkipped: skipped,
419
+ reasonCode: decision.reasonCode || 'UNKNOWN',
420
+ reason: decision.reason || null,
421
+ installedVersion: decision.installedVersion,
422
+ cliVersion: decision.cliVersion,
423
+ registryLatest: decision.registryLatest,
424
+ suggestedInstallCmd: `npm install -D arkgate@${spec}`,
425
+ };
426
+ }
427
+ if (decision.skip) {
428
+ // Skip path must not call packageInstallArgv or spawn install (legacy contract).
429
+ // Recovery command uses a portable default; agents can still read reasonCode.
365
430
  if (!args.json) {
366
- console.log(
367
- `Package already at arkgate@${skip.installedVersion}; skipping install and recomputing managed preview.`
368
- );
431
+ for (const line of formatPackageInstallDecisionHuman(fallbackPayload())) {
432
+ console.log(line);
433
+ }
369
434
  }
370
435
  } else {
371
- const [command, commandArgs] = dependencies.packageInstallArgv(root);
372
- if (!args.json) console.log(`Updating ArkGate: ${command} ${commandArgs.join(' ')}`);
436
+ const [command, commandArgs] = installArgv
437
+ ? installArgv(root, targetSpec)
438
+ : ['npm', ['install', '-D', `arkgate@${targetSpec}`]];
439
+ const payload = installArgv
440
+ ? {
441
+ ...buildPackageInstallSkipPayload(decision, root, installArgv),
442
+ packageInstallSkipped: false,
443
+ suggestedInstallCmd: `${command} ${commandArgs.join(' ')}`,
444
+ }
445
+ : {
446
+ ...fallbackPayload(targetSpec, false),
447
+ suggestedInstallCmd: `${command} ${commandArgs.join(' ')}`,
448
+ };
449
+ if (!args.json) {
450
+ for (const line of formatPackageInstallDecisionHuman(payload)) {
451
+ console.log(line);
452
+ }
453
+ }
373
454
  const install = spawnSync(command, commandArgs, {
374
455
  cwd: root,
375
456
  stdio: args.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
@@ -378,6 +459,19 @@ export function runUpgradeCommand(args, dependencies) {
378
459
  const exitCode = install.status ?? 1;
379
460
  if (exitCode !== 0) {
380
461
  if (args.json && install.stderr) console.error(install.stderr.trim());
462
+ if (args.json) {
463
+ console.log(
464
+ JSON.stringify(
465
+ {
466
+ packageInstallFailed: true,
467
+ exitCode,
468
+ ...payload,
469
+ },
470
+ null,
471
+ 2
472
+ )
473
+ );
474
+ }
381
475
  const recovery = `${command} ${commandArgs.join(' ')}`;
382
476
  const rePreview = arkCommand(
383
477
  root,
@@ -420,18 +514,24 @@ export function runUpgradeCommand(args, dependencies) {
420
514
  const plan = planManagedUpgrade(root, {
421
515
  tools: args.tools,
422
516
  acceptConflicts: args.acceptConflicts,
517
+ refreshSkills: args.refreshSkills === true,
423
518
  });
424
519
  if (!args.apply) {
425
520
  const wouldWrite = plan.summary?.wouldWrite ?? 0;
426
521
  const blocked = plan.summary?.blocked ?? 0;
427
522
  const needsApply = wouldWrite > 0 || blocked > 0;
428
523
  const command = buildUpgradeNextCommand(args, plan.planDigest);
524
+ const skillDrift = buildSkillDriftSummary(plan);
525
+ const hostHonesty = buildHostSelectionHonesty(plan);
429
526
  if (args.json) {
430
527
  // Always expose nextCommand for digest-bound content/manifest apply;
431
528
  // nothingToApply flags when content writes are zero so UIs do not urge apply.
529
+ // FX03 skillDrift + FX07 hostSelection + FX08 whatsNew always on preview.
432
530
  console.log(
433
531
  managedUpgradeJson(plan, {
434
532
  nextCommand: command,
533
+ skillDrift,
534
+ hostSelection: hostHonesty,
435
535
  ...(needsApply ? {} : { nothingToApply: true }),
436
536
  // Surface dual-truth when managed assets refresh without a package pin bump.
437
537
  ...(args.install === false
@@ -449,6 +549,8 @@ export function runUpgradeCommand(args, dependencies) {
449
549
  ? `Update the package and recompute this preview with: ${command}`
450
550
  : `Apply the exact preview with: ${command}`
451
551
  : undefined,
552
+ skillDrift,
553
+ hostSelection: hostHonesty,
452
554
  });
453
555
  if (args.install === false) {
454
556
  console.log(
@@ -486,12 +588,23 @@ export function runUpgradeCommand(args, dependencies) {
486
588
  ? { mode: 'strict-merge', ...verify(root, args.json, dependencies.arkCheck, dependencies.runArkCheck) }
487
589
  : { mode: 'skipped', exitCode: 0 };
488
590
  const dualTruth = describePackageVersionDualTruth(root);
591
+ const skillDrift = buildSkillDriftSummary(applied);
592
+ const hostHonesty = buildHostSelectionHonesty(applied);
593
+ // FX05: post-upgrade verification block (advisory, notAScore).
594
+ const postUpgradeChecks = buildPostUpgradeChecks(root, {
595
+ cliVersion: dependencies.cliVersion,
596
+ verification,
597
+ dualTruth,
598
+ });
489
599
  if (args.json) {
490
600
  console.log(
491
601
  JSON.stringify(
492
602
  {
493
603
  ...applied,
494
604
  verification,
605
+ skillDrift,
606
+ hostSelection: hostHonesty,
607
+ postUpgradeChecks,
495
608
  ...(args.install === false || dualTruth.dualTruth
496
609
  ? {
497
610
  packageInstallSkipped: args.install === false,
@@ -513,7 +626,7 @@ export function runUpgradeCommand(args, dependencies) {
513
626
  )
514
627
  );
515
628
  } else {
516
- renderManagedUpgrade(applied);
629
+ renderManagedUpgrade(applied, { skillDrift, hostSelection: hostHonesty });
517
630
  if (!args.strict) console.log('Architecture verification skipped (--no-strict).');
518
631
  if (args.install === false || dualTruth.dualTruth) {
519
632
  console.log(
@@ -522,6 +635,14 @@ export function runUpgradeCommand(args, dependencies) {
522
635
  : 'Note: --no-install left package.json arkgate pin unchanged. Managed assets match this CLI; bump the pin (or re-run without --no-install) so CI resolves the same version.'
523
636
  );
524
637
  }
638
+ console.log('Post-upgrade checks (advisory — not a score; never flips the gate):');
639
+ for (const check of postUpgradeChecks.checks ?? []) {
640
+ const mark = check.ok === true ? 'ok' : check.ok === false ? 'attention' : 'note';
641
+ console.log(` [${mark}] ${check.id}: ${check.detail}`);
642
+ }
643
+ if (postUpgradeChecks.mcpNote) {
644
+ console.log(` [note] mcp: ${postUpgradeChecks.mcpNote}`);
645
+ }
525
646
  }
526
647
  return verification.exitCode;
527
648
  }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * FX01–FX02 — package install decision for `ark upgrade`.
3
+ *
4
+ * Pure-ish helpers: registry version is injectable so unit tests need no network.
5
+ * Production may pass `getRegistryLatest` that runs `npm view arkgate version`.
6
+ */
7
+ import { spawnSync } from 'node:child_process';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ /**
12
+ * Compare numeric major.minor.patch cores (prerelease / build ignored).
13
+ * @returns {-1|0|1}
14
+ */
15
+ export function compareSemverCore(a, b) {
16
+ const parse = (value) => {
17
+ const core = String(value ?? '')
18
+ .trim()
19
+ .replace(/^v/i, '')
20
+ .split(/[-+]/)[0];
21
+ const parts = core.split('.').map((part) => {
22
+ const n = Number.parseInt(part, 10);
23
+ return Number.isFinite(n) ? n : 0;
24
+ });
25
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
26
+ };
27
+ const left = parse(a);
28
+ const right = parse(b);
29
+ for (let i = 0; i < 3; i += 1) {
30
+ if (left[i] < right[i]) return -1;
31
+ if (left[i] > right[i]) return 1;
32
+ }
33
+ return 0;
34
+ }
35
+
36
+ /**
37
+ * Best-effort registry latest (npm view). Returns null on failure.
38
+ * @param {{ timeoutMs?: number, run?: Function }} [opts]
39
+ */
40
+ export function probeRegistryArkgateLatest(opts = {}) {
41
+ const timeout = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 8000;
42
+ const run =
43
+ typeof opts.run === 'function'
44
+ ? opts.run
45
+ : () =>
46
+ spawnSync('npm', ['view', 'arkgate', 'version'], {
47
+ encoding: 'utf8',
48
+ timeout,
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ });
51
+ try {
52
+ const result = run();
53
+ if (!result || result.status !== 0) return null;
54
+ const v = String(result.stdout || '')
55
+ .trim()
56
+ .split(/\s+/)[0];
57
+ return v || null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Whether install of arkgate can be skipped.
65
+ *
66
+ * FX01: when CLI == installed, still install if registryLatest > installed
67
+ * (unless registry probe skipped/unavailable — then fail open to install only if
68
+ * explicitly behind CLI; if equal and no registry, stay skip with honesty).
69
+ *
70
+ * @param {string} root
71
+ * @param {string} [cliVersion]
72
+ * @param {{
73
+ * registryLatest?: string|null,
74
+ * getRegistryLatest?: () => string|null|undefined,
75
+ * skipRegistryProbe?: boolean,
76
+ * }} [options]
77
+ * @returns {{
78
+ * skip: boolean,
79
+ * installedVersion: string|null,
80
+ * reason: string,
81
+ * reasonCode: string,
82
+ * registryLatest: string|null,
83
+ * cliVersion: string|null,
84
+ * }}
85
+ */
86
+ export function shouldSkipArkgateInstall(root, cliVersion, options = {}) {
87
+ const cli = typeof cliVersion === 'string' && cliVersion.trim() ? cliVersion.trim() : null;
88
+ const pkgPath = path.join(root, 'node_modules', 'arkgate', 'package.json');
89
+ if (!fs.existsSync(pkgPath)) {
90
+ return {
91
+ skip: false,
92
+ installedVersion: null,
93
+ reason: 'not-installed',
94
+ reasonCode: 'NOT_INSTALLED',
95
+ registryLatest: null,
96
+ cliVersion: cli,
97
+ };
98
+ }
99
+ let installedVersion = null;
100
+ try {
101
+ installedVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
102
+ } catch {
103
+ return {
104
+ skip: false,
105
+ installedVersion: null,
106
+ reason: 'unreadable',
107
+ reasonCode: 'UNREADABLE',
108
+ registryLatest: null,
109
+ cliVersion: cli,
110
+ };
111
+ }
112
+
113
+ let registryLatest =
114
+ options.registryLatest !== undefined ? options.registryLatest : undefined;
115
+ if (registryLatest === undefined && options.skipRegistryProbe !== true) {
116
+ if (typeof options.getRegistryLatest === 'function') {
117
+ try {
118
+ registryLatest = options.getRegistryLatest() ?? null;
119
+ } catch {
120
+ registryLatest = null;
121
+ }
122
+ } else {
123
+ registryLatest = probeRegistryArkgateLatest();
124
+ }
125
+ }
126
+ if (registryLatest === undefined) registryLatest = null;
127
+
128
+ // Behind CLI version → always install
129
+ if (cli && installedVersion && installedVersion !== cli) {
130
+ return {
131
+ skip: false,
132
+ installedVersion,
133
+ reason: 'version-differs',
134
+ reasonCode: 'VERSION_DIFFERS',
135
+ registryLatest,
136
+ cliVersion: cli,
137
+ };
138
+ }
139
+
140
+ // Same as CLI (or no CLI): check registry
141
+ if (installedVersion && registryLatest && compareSemverCore(installedVersion, registryLatest) < 0) {
142
+ return {
143
+ skip: false,
144
+ installedVersion,
145
+ reason: 'behind-registry',
146
+ reasonCode: 'BEHIND_REGISTRY',
147
+ registryLatest,
148
+ cliVersion: cli,
149
+ };
150
+ }
151
+
152
+ if (cli && installedVersion && installedVersion === cli) {
153
+ if (registryLatest == null && options.skipRegistryProbe !== true) {
154
+ // Probe failed: do not false-skip forever — still report honesty; skip only when
155
+ // we cannot know registry (offline). Field preferred install when unsure is worse
156
+ // for offline CI; document REGISTRY_UNAVAILABLE and skip with reason.
157
+ return {
158
+ skip: true,
159
+ installedVersion,
160
+ reason: 'already-current-registry-unknown',
161
+ reasonCode: 'REGISTRY_UNAVAILABLE',
162
+ registryLatest: null,
163
+ cliVersion: cli,
164
+ };
165
+ }
166
+ return {
167
+ skip: true,
168
+ installedVersion,
169
+ reason: 'already-current',
170
+ reasonCode: 'ALREADY_CURRENT',
171
+ registryLatest: registryLatest ?? installedVersion,
172
+ cliVersion: cli,
173
+ };
174
+ }
175
+
176
+ return {
177
+ skip: false,
178
+ installedVersion,
179
+ reason: 'version-differs',
180
+ reasonCode: 'VERSION_DIFFERS',
181
+ registryLatest,
182
+ cliVersion: cli,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Machine-readable install decision + human recovery (FX02).
188
+ * @param {ReturnType<typeof shouldSkipArkgateInstall>} decision
189
+ * @param {string} root
190
+ * @param {(root: string, spec?: string) => [string, string[]]} packageInstallArgv
191
+ */
192
+ export function buildPackageInstallSkipPayload(decision, root, packageInstallArgv) {
193
+ const targetSpec =
194
+ decision.registryLatest && compareSemverCore(decision.installedVersion || '0.0.0', decision.registryLatest) < 0
195
+ ? decision.registryLatest
196
+ : 'latest';
197
+ const [command, commandArgs] = packageInstallArgv(root, targetSpec);
198
+ const suggestedInstallCmd = `${command} ${commandArgs.join(' ')}`.trim();
199
+ return {
200
+ schemaVersion: '1.0',
201
+ notAScore: true,
202
+ packageInstallSkipped: decision.skip === true,
203
+ reasonCode: decision.reasonCode || 'UNKNOWN',
204
+ reason: decision.reason || null,
205
+ installedVersion: decision.installedVersion,
206
+ cliVersion: decision.cliVersion,
207
+ registryLatest: decision.registryLatest,
208
+ suggestedInstallCmd,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Human lines for skip / behind-registry install decision.
214
+ * @param {ReturnType<typeof buildPackageInstallSkipPayload>} payload
215
+ */
216
+ export function formatPackageInstallDecisionHuman(payload) {
217
+ if (!payload) return [];
218
+ if (payload.packageInstallSkipped && payload.reasonCode === 'ALREADY_CURRENT') {
219
+ return [
220
+ `Package already at arkgate@${payload.installedVersion}` +
221
+ (payload.registryLatest ? ` (registry ${payload.registryLatest})` : '') +
222
+ '; skipping install and recomputing managed preview.',
223
+ ];
224
+ }
225
+ if (payload.packageInstallSkipped && payload.reasonCode === 'REGISTRY_UNAVAILABLE') {
226
+ return [
227
+ `Package at arkgate@${payload.installedVersion} matches this CLI; registry latest unknown (offline or npm view failed). Skipping install.`,
228
+ `If you know a newer release exists, run: ${payload.suggestedInstallCmd}`,
229
+ ];
230
+ }
231
+ if (!payload.packageInstallSkipped && payload.reasonCode === 'BEHIND_REGISTRY') {
232
+ return [
233
+ `Installed arkgate@${payload.installedVersion} is behind registry ${payload.registryLatest}; installing update.`,
234
+ ` ${payload.suggestedInstallCmd}`,
235
+ ];
236
+ }
237
+ if (!payload.packageInstallSkipped) {
238
+ return [`Updating ArkGate (${payload.reasonCode}): ${payload.suggestedInstallCmd}`];
239
+ }
240
+ return [];
241
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Suggested improvements / what’s new after `ark upgrade` (advisory).
3
+ *
4
+ * Product capability list so consumers know what to try or inspect after
5
+ * installing this package line. Always notAScore / never a gate input.
6
+ * Pure constants — no git, no LLM, no invented residual.
7
+ */
8
+
9
+ export const UPGRADE_WHATS_NEW_SCHEMA_VERSION = '1.0';
10
+
11
+ /**
12
+ * Closed list of post-upgrade suggestions for this product line.
13
+ * Ids are stable for agents; titles/try/inspect are human-facing.
14
+ *
15
+ * @returns {{
16
+ * schemaVersion: typeof UPGRADE_WHATS_NEW_SCHEMA_VERSION,
17
+ * notAScore: true,
18
+ * neverGateInput: true,
19
+ * title: string,
20
+ * items: Array<{
21
+ * id: string,
22
+ * title: string,
23
+ * try: string,
24
+ * inspect: string,
25
+ * why: string,
26
+ * }>,
27
+ * }}
28
+ */
29
+ export function buildUpgradeWhatsNewSuggestions() {
30
+ return {
31
+ schemaVersion: UPGRADE_WHATS_NEW_SCHEMA_VERSION,
32
+ notAScore: true,
33
+ neverGateInput: true,
34
+ title: 'Suggested improvements — try or inspect after this package',
35
+ items: [
36
+ {
37
+ id: 'deep-module-coach',
38
+ title: 'Deep-module coach (hot paths + deepening)',
39
+ try: 'npx arkgate-check --doctor',
40
+ inspect:
41
+ 'doctor.deepModuleCoach (JSON) or HTML data-advisory="deepModuleCoach"',
42
+ why:
43
+ 'See recent-churn hot paths and deepening candidates projected only from existing residual. Always notAScore; never invents paths; never flips the gate.',
44
+ },
45
+ {
46
+ id: 'improvement-compass',
47
+ title: 'Improvement compass (residual lenses)',
48
+ try: 'npx arkgate-check --doctor',
49
+ inspect: 'doctor.improvementCompass or HTML data-advisory="improvementCompass"',
50
+ why:
51
+ 'Named residual architecture lenses (SoC, DIP, domain, …) from evidence you already have — not a score; never flips valid / strict-merge / goal.met.',
52
+ },
53
+ {
54
+ id: 'session-status-honesty',
55
+ title: 'Session recipe + status honesty',
56
+ try: 'npx arkgate status --json',
57
+ inspect: 'status.improvementCompass.mode (full | subset | unavailable)',
58
+ why:
59
+ 'Bind identity → status → act. Incomplete facts never invent green residual; when mode is not full, run doctor for the full residual map.',
60
+ },
61
+ {
62
+ id: 'two-axis-done',
63
+ title: 'Two-axis done (Enforce green ≠ feature done)',
64
+ try: 'Read docs/agent-guide.md “Two-axis done” and your ticket/spec',
65
+ inspect: 'Architecture residual (doctor/compass) vs feature residual (your acceptance)',
66
+ why:
67
+ 'Gate green only clears architecture residual. Feature/ticket acceptance stays outside the package — no package LLM verdict for “done.”',
68
+ },
69
+ {
70
+ id: 'upgrade-self-service',
71
+ title: 'Upgrade self-service honesty',
72
+ try: 'npx arkgate upgrade --json',
73
+ inspect: 'selfService (write-path labels + customized preserve)',
74
+ why:
75
+ 'After managed upgrade, learn write-path hard|advisory|unavailable per host and whether customized content was preserved — without a maintainer ticket. Soft hosts never claim hard.',
76
+ },
77
+ {
78
+ id: 'registry-aware-upgrade',
79
+ title: 'Registry-aware package upgrade (FX field truth)',
80
+ try: 'npx arkgate upgrade --apply',
81
+ inspect:
82
+ 'packageInstallSkipped / reasonCode / registryLatest / suggestedInstallCmd (JSON)',
83
+ why:
84
+ 'When CLI version equals node_modules but npm registry is ahead, upgrade no longer false-skips. Offline/registry-unknown stays honest with a copy-paste install command.',
85
+ },
86
+ {
87
+ id: 'skill-drift-refresh',
88
+ title: 'Skill content drift + opt-in refresh',
89
+ try: 'npx arkgate upgrade --json',
90
+ inspect: 'skillDrift (+ --refresh-skills for customized skill rewrite consent)',
91
+ why:
92
+ 'See stale/customized/missing skill counts. Customized skills stay preserved unless you pass --refresh-skills; never silent overwrite of true edits.',
93
+ },
94
+ {
95
+ id: 'mcp-multi-project',
96
+ title: 'Multi-project MCP process honesty',
97
+ try: 'ark_identity with project.expectedRoot; read processPackage on every tool',
98
+ inspect: 'processPackage.processPackageMismatch / processStale + nextAction',
99
+ why:
100
+ 'One user, many checkouts: after package bump, restart MCP so process arkgateVersion matches install. Prefer project-local CLI until identity matched and versions align.',
101
+ },
102
+ ],
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Human lines for upgrade preview/apply stdout.
108
+ * @param {ReturnType<typeof buildUpgradeWhatsNewSuggestions>} [whatsNew]
109
+ * @returns {string[]}
110
+ */
111
+ export function formatUpgradeWhatsNewSuggestions(whatsNew) {
112
+ const payload = whatsNew && typeof whatsNew === 'object' ? whatsNew : buildUpgradeWhatsNewSuggestions();
113
+ const items = Array.isArray(payload.items) ? payload.items : [];
114
+ if (items.length === 0) return [];
115
+ const lines = [
116
+ payload.title || 'Suggested improvements — try or inspect after this package',
117
+ ' (advisory only — not a score; never changes gate verdicts)',
118
+ ];
119
+ for (const item of items) {
120
+ if (!item || typeof item !== 'object') continue;
121
+ const title = typeof item.title === 'string' ? item.title.trim() : '';
122
+ if (!title) continue;
123
+ lines.push(` • ${title}`);
124
+ if (typeof item.try === 'string' && item.try.trim()) {
125
+ lines.push(` try: ${item.try.trim()}`);
126
+ }
127
+ if (typeof item.inspect === 'string' && item.inspect.trim()) {
128
+ lines.push(` inspect: ${item.inspect.trim()}`);
129
+ }
130
+ if (typeof item.why === 'string' && item.why.trim()) {
131
+ lines.push(` why: ${item.why.trim()}`);
132
+ }
133
+ }
134
+ return lines;
135
+ }