arkgate 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +81 -10
  2. package/README.md +11 -7
  3. package/bin/ark-check-runtime.mjs +50 -14
  4. package/bin/ark-layer-match.mjs +25 -12
  5. package/bin/lib/agent-projection.mjs +1 -1
  6. package/bin/lib/analysis-engine.mjs +5 -5
  7. package/bin/lib/ci-and-commands.mjs +11 -5
  8. package/bin/lib/doctor-plan.mjs +25 -0
  9. package/bin/lib/html-report-advisories.mjs +33 -0
  10. package/bin/lib/html-report-depth.mjs +24 -0
  11. package/bin/lib/html-report.mjs +8 -1
  12. package/bin/lib/improvement-compass-doctor.mjs +106 -0
  13. package/bin/lib/improvement-compass-map.mjs +507 -0
  14. package/bin/lib/improvement-compass-types.mjs +85 -0
  15. package/bin/lib/improvement-compass.mjs +79 -0
  16. package/bin/lib/managed-upgrade-honesty.mjs +201 -0
  17. package/bin/lib/managed-upgrade.mjs +37 -4
  18. package/bin/lib/status-command.mjs +127 -2
  19. package/bin/lib/status-manifest.mjs +187 -1
  20. package/dist/eslint/index.cjs +2 -2
  21. package/dist/eslint/index.js +2 -2
  22. package/dist/index.cjs +35 -35
  23. package/dist/index.d.ts +267 -5
  24. package/dist/index.js +35 -35
  25. package/docs/README.md +7 -6
  26. package/docs/agent-guide.md +50 -7
  27. package/docs/develop.md +9 -4
  28. package/docs/diagnostics.md +1 -1
  29. package/docs/package-surface.md +38 -34
  30. package/docs/product-voice.md +30 -1
  31. package/docs/use.md +67 -1
  32. package/package.json +1 -1
  33. package/schemas/ark.status-manifest.schema.json +53 -0
  34. package/server.json +2 -2
  35. package/templates/agent-skills/README.md +2 -2
  36. package/templates/agent-skills/ark-adopt/SKILL.md +20 -0
  37. package/templates/agent-skills/ark-architect/SKILL.md +21 -1
  38. package/templates/agent-skills/ark-autopilot/SKILL.md +25 -5
  39. package/templates/agent-skills/ark-contract/SKILL.md +20 -0
  40. package/templates/agent-skills/ark-coverage/SKILL.md +20 -0
  41. package/templates/agent-skills/ark-explain/SKILL.md +20 -0
  42. package/templates/agent-skills/ark-explore/SKILL.md +23 -3
  43. package/templates/agent-skills/ark-fix/SKILL.md +22 -2
  44. package/templates/agent-skills/ark-loop/SKILL.md +22 -2
  45. package/templates/agent-skills/ark-place/SKILL.md +20 -0
  46. package/templates/agent-skills/ark-runtime/SKILL.md +7 -0
  47. package/templates/agent-skills/ark-think/SKILL.md +20 -0
  48. package/templates/agent-skills/ark-upgrade/SKILL.md +20 -0
  49. package/templates/skills/ark-adopt.md +20 -0
  50. package/templates/skills/ark-architect.md +21 -1
  51. package/templates/skills/ark-autopilot.md +25 -5
  52. package/templates/skills/ark-contract.md +20 -0
  53. package/templates/skills/ark-coverage.md +20 -0
  54. package/templates/skills/ark-explain.md +20 -0
  55. package/templates/skills/ark-explore.md +23 -3
  56. package/templates/skills/ark-fix.md +22 -2
  57. package/templates/skills/ark-loop.md +22 -2
  58. package/templates/skills/ark-place.md +20 -0
  59. package/templates/skills/ark-runtime.md +7 -0
  60. package/templates/skills/ark-think.md +20 -0
  61. package/templates/skills/ark-upgrade.md +20 -0
@@ -62,6 +62,10 @@ import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisor
62
62
  import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
63
63
  import { designDeltaDoctorLines } from './design-delta.mjs';
64
64
  import { enforcementDoctorLines } from './enforcement-state.mjs';
65
+ import {
66
+ buildDoctorImprovementCompass,
67
+ printImprovementCompassSection,
68
+ } from './improvement-compass-doctor.mjs';
65
69
 
66
70
  const color = {
67
71
  green: (s) => `\x1b[32m${s}\x1b[0m`,
@@ -640,6 +644,23 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
640
644
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
641
645
  });
642
646
 
647
+ // Improvement compass: projection only — never feeds ok/valid/goal.met.
648
+ const improvementCompass = buildDoctorImprovementCompass({
649
+ designSmells,
650
+ violations,
651
+ designWeak: designFitness.designWeak === true,
652
+ physicalCohesion: doctorAdvisories.physicalCohesion,
653
+ rulesUnderContract,
654
+ baselineExists: baseline.exists,
655
+ baselineStale: analysisComplete ? staleBaseline : null,
656
+ frozenResidual: baseline.exists ? baseline.keys.size : null,
657
+ dirtyBaselineRisk: productHonesty?.reasonIds?.includes?.('dirty-baseline') === true,
658
+ ungovernedDirCount: cov.suggestions?.length ?? 0,
659
+ emptyLayerCount: cov.emptyLayers?.length ?? 0,
660
+ goldenPatternPresent: goldenPattern.present === true,
661
+ arkRulesLoaded: rulesUnderContract?.active === true,
662
+ });
663
+
643
664
  if (asJson) {
644
665
  (options.writeJson ?? console.log)(
645
666
  JSON.stringify(
@@ -654,6 +675,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
654
675
  // Path-correct ENFORCE can still be design-weak (P02).
655
676
  designFitness,
656
677
  designSmells,
678
+ // Improvement compass (lenses; notAScore; never a gate input).
679
+ improvementCompass,
657
680
  ...(options.designDelta ? { designDelta: options.designDelta } : {}),
658
681
  // Q01: primary next action when Shape residual dominates (null if not design-weak).
659
682
  postGreenPath,
@@ -846,6 +869,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
846
869
  }
847
870
  }
848
871
 
872
+ printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
873
+
849
874
  console.log('');
850
875
  console.log(color.bold('Design fitness'));
851
876
  if (designSmells.length === 0) {
@@ -10,6 +10,7 @@
10
10
  import { effectiveCapabilityDeny } from './analysis-engine.mjs';
11
11
  import { graphBlindSpotsHtml } from './graph-blind.mjs';
12
12
  import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
13
+ import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
13
14
 
14
15
  // htmlEscape is injected by the caller (html-report.mjs) — importing it back
15
16
  // would be a dependency cycle, and the repo's own gate blocks that. The
@@ -247,10 +248,42 @@ function rulesUnderContractHtml(section) {
247
248
  return formatRulesUnderContractHtml(section, esc);
248
249
  }
249
250
 
251
+ /** Improvement compass — advisory lenses only; never a score bar or gate input. */
252
+ function improvementCompassHtml(compass) {
253
+ if (!compass || compass.notAScore !== true || !Array.isArray(compass.lenses)) return '';
254
+ const residual = Array.isArray(compass.topResidual) ? compass.topResidual : [];
255
+ const residualLine =
256
+ residual.length === 0
257
+ ? '<p class="muted">Residual: none on instrumented lenses (not a score — green edges ≠ finished design).</p>'
258
+ : `<p><span class="tag warn">residual</span> ${residual
259
+ .map((id) => {
260
+ const lens = compass.lenses.find((l) => l.id === id);
261
+ return `<code>${esc(id)}</code>${lens?.summary ? ` — ${esc(lens.summary)}` : ''}`;
262
+ })
263
+ .join('<br/>')}</p>`;
264
+ const oos = compass.lenses
265
+ .filter((l) => l && l.status === 'out-of-scope')
266
+ .map((l) => `<code>${esc(l.id)}</code>`)
267
+ .join(' · ');
268
+ // Same primary next as doctor human: severity-ordered topResidual, not lens-id order.
269
+ const next = primaryImprovementCompassNextAction(compass);
270
+ const nextLine = next
271
+ ? `<p class="muted">Next: <code>${esc(next.ref)}</code> — ${esc(next.summary)}</p>`
272
+ : '';
273
+ return `
274
+ <section class="section card" data-advisory="improvementCompass">
275
+ <h2>Improvement compass <span class="muted">(not a score — projection only; never changes the verdict)</span></h2>
276
+ ${residualLine}
277
+ <p class="muted">Out of scope (honest): ${oos || 'scalability · resilience · security'}</p>
278
+ ${nextLine}
279
+ </section>`;
280
+ }
281
+
250
282
  export function renderAdvisorySections(advisories, escape) {
251
283
  if (!advisories || typeof advisories !== 'object') return '';
252
284
  if (typeof escape === 'function') esc = escape;
253
285
  return [
286
+ improvementCompassHtml(advisories.improvementCompass),
254
287
  contractHealthHtml(advisories.contractHealth),
255
288
  ambientStateHtml(advisories.ambientState),
256
289
  physicalCohesionHtml(advisories.physicalCohesion),
@@ -21,6 +21,8 @@ import {
21
21
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
22
22
  import { readBaseline, baselineOccurrenceKeys } from './violations.mjs';
23
23
  import { describePackageVersionDualTruth } from './field-install.mjs';
24
+ import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
25
+ import { computePhysicalCohesion } from './physical-cohesion.mjs';
24
26
 
25
27
  function esc(value) {
26
28
  return String(value)
@@ -43,6 +45,7 @@ function esc(value) {
43
45
  * frozenKeys?: number,
44
46
  * activeCount?: number,
45
47
  * activeBlockingCount?: number,
48
+ * baselineStale?: number | null,
46
49
  * }} [baselineSplit] same numbers doctor uses (do not recompute from active-only list)
47
50
  */
48
51
  export function buildReportDepthPayload(
@@ -155,6 +158,26 @@ export function buildReportDepthPayload(
155
158
  primaryNextAction: postGreenPath?.action ?? dualTruthNext,
156
159
  activeBlockingViolations: activeBlockingCount,
157
160
  });
161
+ // Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
162
+ const physicalCohesion = computePhysicalCohesion(root, files);
163
+ const baselineStale =
164
+ typeof baselineSplit.baselineStale === 'number' ? baselineSplit.baselineStale : null;
165
+ // Improvement compass — same projection as doctor; notAScore; never a gate input.
166
+ const improvementCompass = buildDoctorImprovementCompass({
167
+ designSmells,
168
+ violations: activeViolations,
169
+ designWeak: designFitness.designWeak === true,
170
+ physicalCohesion,
171
+ rulesUnderContract,
172
+ baselineExists: baseline.exists || frozenKeys > 0,
173
+ baselineStale,
174
+ frozenResidual: frozenKeys,
175
+ dirtyBaselineRisk: productHonesty?.reasonIds?.includes?.('dirty-baseline') === true,
176
+ ungovernedDirCount: coverage?.suggestions?.length ?? 0,
177
+ emptyLayerCount: coverage?.emptyLayers?.length ?? 0,
178
+ goldenPatternPresent: goldenPattern.present === true,
179
+ arkRulesLoaded: rulesUnderContract?.active === true,
180
+ });
158
181
  return {
159
182
  adoption,
160
183
  designDepth: {
@@ -166,6 +189,7 @@ export function buildReportDepthPayload(
166
189
  // P0-B / P1-M — folded into designDepth so --report stays a single payload.
167
190
  productHonesty,
168
191
  mergePlanes: rulesUnderContract?.mergePlanes ?? null,
192
+ improvementCompass,
169
193
  },
170
194
  };
171
195
  }
@@ -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
- return {
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) {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Doctor adapter for the Domain improvement compass (notAScore projection).
3
+ * Keeps doctor-plan.mjs inside its module budget; pure assembly only.
4
+ */
5
+ import {
6
+ buildImprovementCompass,
7
+ formatImprovementCompassDoctorLines,
8
+ primaryImprovementCompassNextAction,
9
+ } from './improvement-compass.mjs';
10
+
11
+ /**
12
+ * @param {{
13
+ * designSmells?: object[],
14
+ * violations?: object[],
15
+ * designWeak?: boolean,
16
+ * physicalCohesion?: { findings?: object[] } | null,
17
+ * rulesUnderContract?: object | null,
18
+ * baselineExists?: boolean,
19
+ * baselineStale?: number | null,
20
+ * frozenResidual?: number | null,
21
+ * dirtyBaselineRisk?: boolean,
22
+ * ungovernedDirCount?: number,
23
+ * emptyLayerCount?: number,
24
+ * goldenPatternPresent?: boolean,
25
+ * arkRulesLoaded?: boolean,
26
+ * }} input
27
+ */
28
+ export function buildDoctorImprovementCompass(input = {}) {
29
+ const violations = Array.isArray(input.violations) ? input.violations : [];
30
+ const ruleId = (v) => String(v?.ruleId ?? v?.code ?? '');
31
+
32
+ let cycleCount = 0;
33
+ let peerIsolationCount = 0;
34
+ let pureOrCapabilityResidual = 0;
35
+ let forbiddenGlobalResidual = 0;
36
+ let arkRulesStructureResidual = 0;
37
+
38
+ for (const v of violations) {
39
+ const id = ruleId(v).toUpperCase();
40
+ if (!id) continue;
41
+ if (id.includes('CYCLE') || id === 'CIRCULAR_DEPENDENCY') cycleCount += 1;
42
+ if (id.includes('PEER_ISOLATION')) peerIsolationCount += 1;
43
+ if (id === 'CAPABILITY_VIOLATION') pureOrCapabilityResidual += 1;
44
+ if (id === 'FORBIDDEN_GLOBAL' || id.startsWith('FORBIDDEN_')) forbiddenGlobalResidual += 1;
45
+ if (id.startsWith('ARKRULE_') || id === 'INVARIANT_UNCOVERED') arkRulesStructureResidual += 1;
46
+ }
47
+
48
+ const pcFindings = input.physicalCohesion?.findings;
49
+ const physicalCohesionFindingCount = Array.isArray(pcFindings) ? pcFindings.length : 0;
50
+
51
+ const arkRulesLoaded =
52
+ input.arkRulesLoaded === true ||
53
+ input.rulesUnderContract?.active === true ||
54
+ (typeof input.rulesUnderContract?.structureRules === 'number' &&
55
+ input.rulesUnderContract.structureRules > 0);
56
+
57
+ return buildImprovementCompass({
58
+ designSmells: Array.isArray(input.designSmells) ? input.designSmells : [],
59
+ violations: violations.map((v) => ({
60
+ ruleId: ruleId(v) || undefined,
61
+ message: typeof v?.message === 'string' ? v.message : undefined,
62
+ file: typeof v?.file === 'string' ? v.file : typeof v?.path === 'string' ? v.path : undefined,
63
+ fromLayer: v?.fromLayer,
64
+ toLayer: v?.toLayer,
65
+ failsStrict: v?.failsStrict,
66
+ typeOnly: v?.typeOnly === true || v?.namedBindingsTypeOnly === true || undefined,
67
+ })),
68
+ cycleCount,
69
+ peerIsolationCount,
70
+ physicalCohesionFindingCount,
71
+ arkRulesLoaded,
72
+ arkRulesStructureResidual,
73
+ designWeak: input.designWeak === true,
74
+ baselineExists: input.baselineExists === true,
75
+ baselineStale: input.baselineStale ?? null,
76
+ frozenResidual: input.frozenResidual ?? null,
77
+ dirtyBaselineRisk: input.dirtyBaselineRisk === true,
78
+ pureOrCapabilityResidual,
79
+ forbiddenGlobalResidual,
80
+ ungovernedDirCount: Number(input.ungovernedDirCount) || 0,
81
+ emptyLayerCount: Number(input.emptyLayerCount) || 0,
82
+ goldenPatternPresent: input.goldenPatternPresent === true,
83
+ // Doctor path is TypeScript-oriented (ArkGate product surface).
84
+ stackKind: 'typescript',
85
+ });
86
+ }
87
+
88
+ export {
89
+ formatImprovementCompassDoctorLines,
90
+ primaryImprovementCompassNextAction,
91
+ };
92
+
93
+ /**
94
+ * Human doctor section (never a score bar).
95
+ * @param {import('./improvement-compass.mjs').ImprovementCompass} compass
96
+ * @param {{ line: Function, warn: string, ok: string, color: { bold: Function } }} io
97
+ */
98
+ export function printImprovementCompassSection(compass, io) {
99
+ const { line, warn, ok, color } = io;
100
+ console.log('');
101
+ console.log(color.bold('Improvement compass (not a score)'));
102
+ const mark = compass.topResidual.length > 0 ? warn : ok;
103
+ for (const text of formatImprovementCompassDoctorLines(compass)) {
104
+ line(mark, text);
105
+ }
106
+ }