arkgate 4.8.11 → 4.8.14

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 (93) hide show
  1. package/CHANGELOG.md +125 -2
  2. package/README.md +39 -46
  3. package/SECURITY.md +5 -3
  4. package/bin/ark-check-runtime.mjs +11 -9
  5. package/bin/lib/agent-projection-formatters.mjs +2 -0
  6. package/bin/lib/agent-skills-package.mjs +63 -8
  7. package/bin/lib/analysis-engine.mjs +4 -4
  8. package/bin/lib/architecture-scan.mjs +8 -2
  9. package/bin/lib/ark-order-doctor.mjs +160 -0
  10. package/bin/lib/ark-order-report.mjs +65 -0
  11. package/bin/lib/ark-order-sensors.mjs +1 -1
  12. package/bin/lib/ci-and-commands.mjs +7 -2
  13. package/bin/lib/design-smells.mjs +21 -1
  14. package/bin/lib/diagnostic-catalog.mjs +4 -4
  15. package/bin/lib/doctor-advisories.mjs +99 -18
  16. package/bin/lib/doctor-human.mjs +4 -11
  17. package/bin/lib/doctor-plan.mjs +4 -3
  18. package/bin/lib/extra-merge-teeth.mjs +32 -4
  19. package/bin/lib/first-run-help.mjs +11 -2
  20. package/bin/lib/gate-files.mjs +40 -3
  21. package/bin/lib/html-report-advisories.mjs +2 -0
  22. package/bin/lib/html-report-depth.mjs +8 -18
  23. package/bin/lib/html-report.mjs +16 -0
  24. package/bin/lib/install-migrate.mjs +23 -0
  25. package/bin/lib/mcp-hook-payload.mjs +1 -1
  26. package/bin/lib/product-copy.mjs +4 -0
  27. package/bin/lib/remediation.mjs +7 -7
  28. package/bin/lib/resolved-candidate-facts.mjs +144 -36
  29. package/bin/lib/rules-under-contract.mjs +14 -0
  30. package/bin/lib/scan-files.mjs +39 -0
  31. package/bin/lib/start-preview.mjs +4 -0
  32. package/bin/lib/status-command.mjs +28 -0
  33. package/bin/lib/status-manifest.mjs +23 -0
  34. package/bin/lib/upgrade-whats-new.mjs +3 -3
  35. package/bin/lib/violations.mjs +40 -1
  36. package/dist/{diagnosticCatalog-DiflIock.d.ts → diagnosticCatalog-DVx_2RmF.d.ts} +1 -1
  37. package/dist/eslint/index.cjs +4 -4
  38. package/dist/eslint/index.js +4 -4
  39. package/dist/index.cjs +31 -31
  40. package/dist/index.d.ts +129 -15
  41. package/dist/index.js +31 -31
  42. package/dist/nestjs/index.cjs +1 -1
  43. package/dist/nestjs/index.js +1 -1
  44. package/dist/runtime/index.cjs +15 -15
  45. package/dist/runtime/index.d.ts +1 -1
  46. package/dist/runtime/index.js +15 -15
  47. package/docs/README.md +11 -8
  48. package/docs/agent-guide.md +30 -13
  49. package/docs/ai-gates.md +3 -1
  50. package/docs/arkorder.md +35 -10
  51. package/docs/configuration.md +7 -6
  52. package/docs/develop.md +3 -1
  53. package/docs/diagnostics.md +7 -7
  54. package/docs/enthusiast/README.md +6 -1
  55. package/docs/enthusiast/how-to-gallery-starter.md +2 -1
  56. package/docs/package-surface.md +8 -5
  57. package/docs/product-voice.md +40 -13
  58. package/docs/threat-model.md +2 -2
  59. package/docs/typescript-support.md +3 -3
  60. package/docs/use.md +18 -11
  61. package/package.json +1 -1
  62. package/schemas/ark.status-manifest.schema.json +47 -0
  63. package/server.json +2 -2
  64. package/templates/agent-skills/README.md +7 -4
  65. package/templates/agent-skills/ark-adopt/SKILL.md +9 -5
  66. package/templates/agent-skills/ark-architect/SKILL.md +5 -18
  67. package/templates/agent-skills/ark-autopilot/SKILL.md +8 -4
  68. package/templates/agent-skills/ark-contract/SKILL.md +9 -20
  69. package/templates/agent-skills/ark-coverage/SKILL.md +12 -8
  70. package/templates/agent-skills/ark-explain/SKILL.md +7 -3
  71. package/templates/agent-skills/ark-explore/SKILL.md +25 -4
  72. package/templates/agent-skills/ark-fix/SKILL.md +15 -20
  73. package/templates/agent-skills/ark-loop/SKILL.md +14 -20
  74. package/templates/agent-skills/ark-order/SKILL.md +200 -0
  75. package/templates/agent-skills/ark-place/SKILL.md +11 -8
  76. package/templates/agent-skills/ark-runtime/SKILL.md +17 -4
  77. package/templates/agent-skills/ark-think/SKILL.md +24 -126
  78. package/templates/agent-skills/ark-upgrade/SKILL.md +13 -2
  79. package/templates/skills/ark-adopt.md +9 -5
  80. package/templates/skills/ark-architect.md +5 -18
  81. package/templates/skills/ark-autopilot.md +8 -4
  82. package/templates/skills/ark-contract.md +9 -20
  83. package/templates/skills/ark-coverage.md +12 -8
  84. package/templates/skills/ark-explain.md +7 -3
  85. package/templates/skills/ark-explore.md +25 -4
  86. package/templates/skills/ark-fix.md +15 -20
  87. package/templates/skills/ark-loop.md +14 -20
  88. package/templates/skills/ark-order.md +200 -0
  89. package/templates/skills/ark-place.md +11 -8
  90. package/templates/skills/ark-runtime.md +17 -4
  91. package/templates/skills/ark-think.md +24 -126
  92. package/templates/skills/ark-upgrade.md +13 -2
  93. package/templates/tests/ark-adoption-gaps.test.ts +5 -4
@@ -126,12 +126,18 @@ export function resolveArchitectureSnapshot({
126
126
  { ...config, rules: rules ?? config.rules },
127
127
  manifest
128
128
  );
129
+ const scoped = fileLocalScope(root, files);
129
130
  const facts = resolveCandidateFacts({
130
131
  root,
131
132
  config: effectiveConfig,
132
133
  ts,
133
134
  ...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
134
135
  observeInput,
136
+ ...(scoped
137
+ ? { scopeFiles: [...scoped] }
138
+ : args?.changed
139
+ ? { scopeFiles: [] }
140
+ : {}),
135
141
  });
136
142
  const arkRulesLoad = loadEffectiveArkRulesFromDisk(root, effectiveConfig, {
137
143
  observeInput,
@@ -145,7 +151,6 @@ export function resolveArchitectureSnapshot({
145
151
  err.issues = arkRulesLoad.errors;
146
152
  throw err;
147
153
  }
148
- const scoped = fileLocalScope(root, files);
149
154
  const loadedContract = loadContract(effectiveConfig, configPath, {
150
155
  arkRules: arkRulesLoad.arkRules,
151
156
  });
@@ -162,7 +167,8 @@ export function resolveArchitectureSnapshot({
162
167
  ...coverageOptionsFromConfig(effectiveConfig),
163
168
  })
164
169
  : undefined;
165
- // File-local structure sensors + hint load honor `files`; graph still uses full facts.
170
+ // File-local sensors honor the touched set. Facts (and graph) use that set plus
171
+ // its import closure — not the whole include tree. Unbounded `files` stays full-tree.
166
172
  const fileHints = loadHintsForScope(
167
173
  root,
168
174
  facts,
@@ -0,0 +1,160 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderDoctor.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/ark-order-doctor.mjs). Zero Node I/O.
9
+ */
10
+
11
+ import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, } from './extra-merge-teeth.mjs';
12
+ export const ARK_ORDER_DOCTOR_SCHEMA_VERSION = '1.0';
13
+ const RESIDUAL_RULE_CAP = 12;
14
+ export const ARKORDER_ONE_BREATH = 'Layers stop a bad import. ArkOrder stops rewriting a big product choice — like the billing plan — as if it were a seat count. Change those choices through a valve, not a generic update.';
15
+ /** First-contact next step. Reuses the billing gallery + existing doors. */
16
+ export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: examples/arkorder-billing, then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
17
+ function closedMode(value) {
18
+ return value === 'enforced' || value === 'advisory' ? value : null;
19
+ }
20
+ function uniqueArkOrderRuleIds(findings) {
21
+ const seen = new Set();
22
+ if (!Array.isArray(findings))
23
+ return [];
24
+ for (const finding of findings) {
25
+ const id = finding?.ruleId;
26
+ if (typeof id !== 'string' || !isArkOrderRuleId(id) || seen.has(id))
27
+ continue;
28
+ seen.add(id);
29
+ }
30
+ return [...seen].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
31
+ }
32
+ function extraFromConfig(arkOrder) {
33
+ if (!arkOrder || typeof arkOrder !== 'object') {
34
+ return { present: false, mode: null, roots: 0, layers: 0, xiKeys: [] };
35
+ }
36
+ const xiKeys = Array.isArray(arkOrder.xiKeys)
37
+ ? arkOrder.xiKeys.filter((key) => typeof key === 'string' && key.length > 0)
38
+ : [];
39
+ return {
40
+ present: true,
41
+ mode: closedMode(arkOrder.mode),
42
+ roots: Array.isArray(arkOrder.planeRoots) ? arkOrder.planeRoots.length : 0,
43
+ layers: Array.isArray(arkOrder.managedLayers) ? arkOrder.managedLayers.length : 0,
44
+ xiKeys,
45
+ };
46
+ }
47
+ /**
48
+ * Doctor / HTML ArkOrder advisory. Always emitted; absence is an honest silent row.
49
+ */
50
+ export function summarizeArkOrderSection(input = {}) {
51
+ const extra = extraFromConfig(input.arkOrder);
52
+ const uniqueIds = extra.present ? uniqueArkOrderRuleIds(input.findings) : [];
53
+ const ruleIds = uniqueIds.slice(0, RESIDUAL_RULE_CAP);
54
+ const residualCount = uniqueIds.length;
55
+ const mergePlanes = composeMergePlanesHonesty({
56
+ classification: input.classification,
57
+ arkRules: {
58
+ active: input.arkRules?.active === true,
59
+ structureEnforced: input.arkRules?.structureEnforced,
60
+ structureTotal: input.arkRules?.structureTotal,
61
+ structureAdvisory: input.arkRules?.structureAdvisory,
62
+ invariantEnforced: input.arkRules?.invariantEnforced,
63
+ invariantTotal: input.arkRules?.invariantTotal,
64
+ invariantAdvisory: input.arkRules?.invariantAdvisory,
65
+ covered: input.arkRules?.covered,
66
+ uncovered: input.arkRules?.uncovered,
67
+ },
68
+ arkRun: {
69
+ present: input.arkRun?.present === true,
70
+ mode: input.arkRun?.mode ?? null,
71
+ residualCount: input.arkRun?.residualCount,
72
+ },
73
+ arkOrder: {
74
+ present: extra.present,
75
+ mode: extra.mode,
76
+ residualCount,
77
+ },
78
+ });
79
+ const extraMergeTeeth = extra.present && extra.mode === 'enforced' && extraMergeTeethAllowed(input.classification);
80
+ let note;
81
+ if (!extra.present) {
82
+ note = 'Absence of arkOrder is silent — Layers verdicts unchanged. Not a score.';
83
+ }
84
+ else if (extra.mode === 'advisory') {
85
+ note =
86
+ 'Advisory ArkOrder residual only — never flips valid or --strict-merge. Residual is a finding-id count, never a score.';
87
+ }
88
+ else if (extraMergeTeeth) {
89
+ note =
90
+ 'Enforced ArkOrder is on the extra merge plane. Residual is a finding-id count, never a score.';
91
+ }
92
+ else {
93
+ note =
94
+ 'Enforced ArkOrder extra teeth stay demoted until the layer plane is honestly classified. Residual is a finding-id count, never a score.';
95
+ }
96
+ return {
97
+ schemaVersion: ARK_ORDER_DOCTOR_SCHEMA_VERSION,
98
+ notAScore: true,
99
+ active: extra.present,
100
+ mode: extra.mode,
101
+ planeRoots: extra.roots,
102
+ managedLayers: extra.layers,
103
+ xiKeys: extra.xiKeys,
104
+ residual: { count: residualCount, ruleIds },
105
+ extraMergeTeeth,
106
+ failMergeWhen: mergePlanes.failMergeWhen,
107
+ note,
108
+ mergePlanes,
109
+ };
110
+ }
111
+ /** Thin status slice — counts only; residual null means unknown, not green. */
112
+ export function projectStatusArkOrder(input = {}) {
113
+ const present = input.present === true;
114
+ const mode = closedMode(input.mode);
115
+ const residualRaw = input.residual;
116
+ let residual = null;
117
+ if (typeof residualRaw === 'number' && Number.isFinite(residualRaw) && residualRaw >= 0) {
118
+ residual = Math.floor(residualRaw);
119
+ }
120
+ if (!present)
121
+ residual = residual == null ? 0 : residual;
122
+ return {
123
+ notAScore: true,
124
+ present,
125
+ mode: present ? mode : null,
126
+ extraMergeTeeth: present && mode === 'enforced' && input.extraMergeTeeth === true,
127
+ residual,
128
+ };
129
+ }
130
+ export function formatArkOrderDoctorLines(section) {
131
+ if (!section || section.notAScore !== true)
132
+ return [];
133
+ if (section.active !== true) {
134
+ return [
135
+ ARKORDER_ONE_BREATH,
136
+ 'Off — Layers stay the same (not a score).',
137
+ ARKORDER_FIRST_CONTACT_NEXT,
138
+ ];
139
+ }
140
+ const mode = section.mode ?? 'unknown';
141
+ const teeth = section.extraMergeTeeth === true ? 'armed' : 'not armed';
142
+ const keys = section.xiKeys.length > 0 ? section.xiKeys.join(', ') : '(none named — field-write sensor silent)';
143
+ const lines = [
144
+ ARKORDER_ONE_BREATH,
145
+ `mode: ${mode} · xiKeys: ${keys} · extra merge teeth ${teeth} · not a score`,
146
+ ];
147
+ if (section.residual.count > 0) {
148
+ const shown = section.residual.ruleIds.join(', ');
149
+ const more = section.residual.count > section.residual.ruleIds.length
150
+ ? ` (+${section.residual.count - section.residual.ruleIds.length} more)`
151
+ : '';
152
+ lines.push(`Residual: ${shown}${more}`);
153
+ }
154
+ else {
155
+ lines.push('Residual: none on this scan (not a score — green extras ≠ a frozen billing plan).');
156
+ }
157
+ if (section.failMergeWhen)
158
+ lines.push(section.failMergeWhen);
159
+ return lines;
160
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * HTML for the doctor ArkOrder advisory (report parity: data-advisory="arkOrder").
3
+ */
4
+ import { ARKORDER_FIRST_CONTACT_NEXT, ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
5
+
6
+ export function formatArkOrderHtml(section, esc) {
7
+ if (!section || typeof section !== 'object' || section.notAScore !== true) return '';
8
+ const escape = typeof esc === 'function' ? esc : (v) => String(v);
9
+ const note = section.note ? `<p class="muted">${escape(section.note)}</p>` : '';
10
+ if (section.active !== true) {
11
+ return `
12
+ <section class="section card" data-advisory="arkOrder">
13
+ <h2>ArkOrder <span class="muted">(opt-in extra — not a score)</span></h2>
14
+ <p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
15
+ ${ARKORDER_ONE_BREATH}
16
+ Off until you turn it on — Layers stay the same.
17
+ ${ARKORDER_FIRST_CONTACT_NEXT}
18
+ </p>
19
+ ${note}
20
+ </section>`;
21
+ }
22
+ const residual = section.residual && typeof section.residual === 'object' ? section.residual : { count: 0, ruleIds: [] };
23
+ const ids = Array.isArray(residual.ruleIds) ? residual.ruleIds : [];
24
+ const residualHtml =
25
+ residual.count > 0
26
+ ? `<p><span class="tag warn">residual</span> ${ids
27
+ .map((id) => `<code>${escape(id)}</code>`)
28
+ .join(' · ')}${
29
+ residual.count > ids.length ? ` <span class="muted">(+${residual.count - ids.length} more)</span>` : ''
30
+ }</p>`
31
+ : '<p class="muted">Residual: none on this scan (not a score — green extras ≠ a frozen billing plan).</p>';
32
+ const teeth = section.extraMergeTeeth === true
33
+ ? '<span class="tag">extra merge teeth armed</span>'
34
+ : '<span class="tag warn">extra merge teeth not armed</span>';
35
+ const mergeSentence =
36
+ section.mergePlanes && typeof section.mergePlanes.failMergeWhen === 'string'
37
+ ? section.mergePlanes.failMergeWhen
38
+ : section.failMergeWhen;
39
+ const merge =
40
+ mergeSentence
41
+ ? `<p class="muted" style="margin:.35rem 0 .55rem;font-size:.86rem"><b>Merge planes:</b> ${escape(mergeSentence)}</p>`
42
+ : '';
43
+ const keys = Array.isArray(section.xiKeys) && section.xiKeys.length > 0
44
+ ? section.xiKeys.map((key) => `<code>${escape(key)}</code>`).join(' · ')
45
+ : '<span class="muted">(none named — field-write sensor silent)</span>';
46
+ return `
47
+ <section class="section card" data-advisory="arkOrder">
48
+ <h2>ArkOrder <span class="muted">(not a score)</span></h2>
49
+ <p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
50
+ <b>[ArkOrder]</b> ${ARKORDER_ONE_BREATH}
51
+ Separate from <b>[Layer]</b> imports, <b>[ArkRules]</b> shape, and <b>[ArkRun]</b> travel.
52
+ </p>
53
+ ${merge}
54
+ <div class="kpis" style="margin-bottom:.55rem">
55
+ <div class="kpi"><b>${escape(section.mode || '—')}</b><span>Mode</span></div>
56
+ <div class="kpi"><b>${Number(residual.count) || 0}</b><span>Residual ids</span></div>
57
+ <div class="kpi"><b>${Number(section.planeRoots) || 0}</b><span>Plane roots</span></div>
58
+ <div class="kpi"><b>${Number(section.managedLayers) || 0}</b><span>Managed layers</span></div>
59
+ </div>
60
+ <p>${teeth} · <code>notAScore</code></p>
61
+ <p class="dim" style="margin:.35rem 0 .55rem;font-size:.86rem">xiKeys: ${keys}</p>
62
+ ${residualHtml}
63
+ ${note}
64
+ </section>`;
65
+ }
@@ -219,7 +219,7 @@ export function evaluateArkOrderSensors(input) {
219
219
  continue;
220
220
  if (!matchesArkOrderAppliesTo(write.file, extra.appliesTo))
221
221
  continue;
222
- findings.push(finding(extra, 'arkorder-xi-field-write', write.file, write.line, `File writes slow key ${JSON.stringify(write.key)} through a persistence driver; route the field through ingest or a pattern change through proposeRelease.`, { fromLayer, target: write.key }, teethAllowed));
222
+ findings.push(finding(extra, 'arkorder-xi-field-write', write.file, write.line, `This file writes ${JSON.stringify(write.key)} the same way it would write a seat count. Take the event in, or change that choice through the valve (propose, then apply).`, { fromLayer, target: write.key }, teethAllowed));
223
223
  }
224
224
  findings.sort((left, right) => left.file.localeCompare(right.file) ||
225
225
  left.ruleId.localeCompare(right.ruleId) ||
@@ -338,6 +338,8 @@ files; never weaken the gate via subagents.
338
338
 
339
339
  **Escapes, not a second curriculum.** Do **not** run overlapping skills for the same job.
340
340
  Pick **one** primary skill. Prefer doctor top action #1 when unsure.
341
+ Filter: **Contener · Guiar · Ordenar** — contain the write, guide the next step, order leftover mess. Skills never enforce. First-class extras:
342
+ \`/ark-runtime\` (ArkRun) · \`/ark-order\` (ArkOrder).
341
343
 
342
344
  | When | Invoke | Not this |
343
345
  |------|--------|----------|
@@ -352,10 +354,11 @@ Pick **one** primary skill. Prefer doctor top action #1 when unsure.
352
354
  | Gate violation on a change (small cluster) | \`/ark-autopilot\` | leftover \`/ark-fix\` |
353
355
  | Drive plan **A** to goal.met | \`/ark-autopilot\` | leftover \`/ark-loop\` |
354
356
  | Ark **fitness** only (governed%, gates, baseline, install gaps) | \`/ark-coverage\` | leftover design work (use single path above) |
355
- | One design decision, 2–3 options | \`/ark-think\` | full Shape residual (use single path) |
357
+ | One design decision, 2–3 options | \`/ark-explore\` (one decision) | leftover \`/ark-think\`; apply autopilot |
356
358
  | Explain / HTML report tour | \`/ark-explain\` | explore |
357
359
  | Bump arkgate + refresh hosts | \`/ark-upgrade\` | — |
358
- | Optional runtime kernel evaluate | \`/ark-runtime\` | |
360
+ | Optional runtime kernel evaluate | \`/ark-runtime\` | leftover \`/ark-run\` (not a skill); order plane → \`/ark-order\` |
361
+ | Optional ArkOrder evaluate / wire | \`/ark-order\` | extra off → \`/ark-adopt\`; skip grind → \`/ark-autopilot\` |
359
362
 
360
363
  **Post-green door:** when doctor reports ENFORCE · leftover design work, the **primary** next action is the single Shape path above — not a choice among explore / coverage / think. Doctor JSON: \`postGreenPath\` / \`primaryNextAction\`.
361
364
 
@@ -405,6 +408,8 @@ ${projectionBlock}
405
408
 
406
409
  **Primary path (do this):**
407
410
 
411
+ Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.
412
+
408
413
  1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
409
414
  2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
410
415
  3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
@@ -493,6 +493,25 @@ export function summarizeDesignFitness(smells, ctx = {}) {
493
493
  /** How many smell ids the green-run pointer names before it counts the rest. */
494
494
  export const GREEN_PLAN_POINTER_MAX_IDS = 4;
495
495
 
496
+ /** When a smell has no evidence path, say so — do not leave the reader hunting. */
497
+ export const SMELL_UNATTRIBUTED =
498
+ 'no file — leftover design, not a specific line';
499
+
500
+ /**
501
+ * One named smell with its first evidence path (or an explicit "no file").
502
+ * The pointer used to print only the id, so "1 design smell (domain-logic-in-ui)"
503
+ * could not answer "did my change cause this?"
504
+ */
505
+ export function formatSmellPointerName(id, smells) {
506
+ const match = (smells || []).find((smell) => smell?.id === id);
507
+ const evidence = Array.isArray(match?.evidence)
508
+ ? match.evidence.filter((entry) => typeof entry === 'string' && entry.trim())
509
+ : [];
510
+ if (evidence.length === 0) return `${id} (${SMELL_UNATTRIBUTED})`;
511
+ const extra = evidence.length - 1;
512
+ return extra > 0 ? `${id} at ${evidence[0]} (+${extra} more)` : `${id} at ${evidence[0]}`;
513
+ }
514
+
496
515
  /**
497
516
  * Name `--plan` on a run that passed.
498
517
  *
@@ -528,7 +547,8 @@ export function formatGreenPlanPointer(smells, ctx = {}, planCommand = 'ark-chec
528
547
  if (ids.length === 0) return null;
529
548
  const shown = ids.slice(0, GREEN_PLAN_POINTER_MAX_IDS);
530
549
  const hidden = ids.length - shown.length;
531
- const named = hidden > 0 ? `${shown.join(', ')}, +${hidden} more` : shown.join(', ');
550
+ const namedIds = shown.map((id) => formatSmellPointerName(id, smells));
551
+ const named = hidden > 0 ? `${namedIds.join(', ')}, +${hidden} more` : namedIds.join(', ');
532
552
  const plural = ids.length === 1 ? '' : 's';
533
553
  const suppressed = ctx.suppressedCount ?? 0;
534
554
  const opening =
@@ -32,7 +32,7 @@ function entry(ruleId, category, title, why, fix, extras) {
32
32
  */
33
33
  export const DIAGNOSTIC_CATALOG = Object.freeze([
34
34
  // ── layer / graph ────────────────────────────────────────────────────────
35
- entry('LAYER_IMPORT_VIOLATION', 'layer', 'Layer import not allowed', 'A module import (or re-export) crosses a layer edge that ark.config.json does not allow. The architecture contract forbids that dependency direction so outer infrastructure cannot leak into pure or inner layers.', 'Branch by import kind: constants/types/pure → adopt into DomainModel or SharedKernel (do not invent a port); kernel/events/bootstrap from Persistence → inject a port or move the map to SharedTypes (Persistence must not emit); define a port only when the target is a real use-case. Type-only edges use `import type`. Then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
35
+ entry('LAYER_IMPORT_VIOLATION', 'layer', 'This import is not allowed', 'This file imported a folder it may not reach. The write doesn’t land. The same check fails the pull request.', 'Branch by import kind: constants/types/pure → adopt into DomainModel or SharedKernel (do not invent a port); kernel/events/bootstrap from Persistence → inject a port or move the map to SharedTypes (Persistence must not emit); define a port only when the target is a real use-case. Type-only edges use `import type`. Then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
36
36
  entry('LAYER_INTENT_REFERENCE_VIOLATION', 'layer', 'Intent referenced across a blocked layer edge', 'A string intent (or intent-like reference) names a layer that the file’s layer may not reach under the contract rules — the same plane as import edges, for event/intent coupling.', 'Reference that intent from a layer allowed to know about it (usually an adapter or application layer), or relocate the reference — then preflight again.'),
37
37
  entry('LAYER_REFERENCE_VIOLATION', 'layer', 'Layer reference blocked (snippet / AI gate)', 'Snippet analysis found an intent or string reference that would couple layers in a direction the architecture profile forbids.', 'Move the reference to an allowed layer or introduce a port/event boundary, then re-run the snippet gate.'),
38
38
  entry('CIRCULAR_DEPENDENCY', 'layer', 'Dependency cycle', 'Two or more modules import each other in a loop. Cycles make ownership unclear and break stable layer direction.', 'Extract the shared dependency into a third module, invert one edge behind a port, or merge units that are truly one — then preflight again.'),
@@ -68,14 +68,14 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
68
68
  entry('ARKRUN_TRANSPORT_BYPASS', 'arkrun', 'Homemade broker or emitter import', 'A managed layer imports a closed broker/queue/emitter specifier (EventEmitter, queue clients, …) instead of the ArkRun kernel transport.', 'Send through the ArkRun kernel transport instead of importing that broker or emitter, then preflight again. Never mechanical-safe — homemade buses stay judgment.'),
69
69
  entry('ARKORDER_MISSING_PLANE', 'arkorder', 'No createOrderPlane in plane roots', 'The ArkOrder extra is on but no createOrderPlane factory was found in arkOrder.planeRoots, so agents can skip the pattern plane while the write gate stays green.', 'Import createOrderPlane from arkgate/order and call it in a plane root listed in arkOrder.planeRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
70
70
  entry('ARKORDER_KERNEL_IN_DOMAIN', 'arkorder', 'Domain-role layer imports the order plane', 'A Domain-role layer imports arkgate/order. Domain stays plane-free; planeRoots own the factory.', 'Move the arkgate/order import out of the Domain-role layer into a plane root or adapter, then preflight again. Never mechanical-safe.'),
71
- entry('ARKORDER_GENERIC_UPDATE', 'arkorder', 'Generic update of ξ', 'A call to update/patch/set on the order plane rewrites the slow pattern. Haken slaving forbids generic ξ mutation.', 'Use release() for the first freeze of ξ. Later pattern change is proposeRelease then apply(ProposeResult). Never update/patch/set. Never mechanical-safe.'),
71
+ entry('ARKORDER_GENERIC_UPDATE', 'arkorder', 'Generic update of a big product choice', 'A call to update/patch/set rewrites a named product choice (like billing plan) as if it were a seat count.', 'Don\'t use a generic update. First freeze with release(). Later, propose the change, then apply it.'),
72
72
  entry('ARKORDER_TOO_MANY_PARAMS', 'arkorder', 'Too many slow keys', 'ξ has more keys than arkOrder.maxXiKeys. Haken requires a few slow modes, not a dump of microstate.', 'Cut ξ to the slow keys that actually slave the rest, then preflight again. Never mechanical-safe.'),
73
73
  entry('ARKORDER_INGEST_WRITES_XI', 'arkorder', 'ingest assigned into ξ', 'An ingest() result is written into a Release or ξ store. ingest may absorb, escalate_up, or hold; it never mints a pattern.', 'Keep ingest results as absorb/escalate_up/hold only. Change ξ with proposeRelease then apply(ProposeResult). Never mechanical-safe.'),
74
- entry('ARKORDER_XI_FIELD_WRITE', 'arkorder', 'Slow key written around the order plane', 'A managed-layer file imports a persistence driver and writes a declared arkOrder.xiKeys name. Field events absorb or escalate; they do not PATCH the slow pattern.', 'Keep invoices, seats, hours, and logs on ingest. Change the slow key with proposeRelease then apply(ProposeResult), then preflight again. Never mechanical-safe.'),
74
+ entry('ARKORDER_XI_FIELD_WRITE', 'arkorder', 'Big product choice written like a seat count', 'A use-case writes a named product choice (like billing plan) through a database update. Invoices and seats can flow. That choice cannot.', 'Keep invoices and seats as events. Change the choice through the valve (proposeRelease then apply), not a generic update.'),
75
75
  entry('ARKORDER_INFORMATION_BUDGET', 'arkorder', 'Projection observes a forbidden kind', 'h(ξ) allowedKinds includes a kind listed in informationBudget.cannotObserve. A scale may not look at what it was told not to see.', 'Cut that kind from the projector or from cannotObserve, then preflight again. Never mechanical-safe.'),
76
76
  entry('ARKORDER_XI_TTL', 'arkorder', 'Slow key carries a freshness field', 'ξ named ttl/freshUntil/maxAge. Freshness belongs on σ. A slow parameter that expires per transaction is not slow.', 'Move freshness onto σ (freshUntil) and keep ξ stable, then preflight again. Never mechanical-safe.'),
77
77
  entry('ARKORDER_STALE_SIGMA', 'arkorder', 'σ is stale', 'ingest ran after σ.freshUntil (or sigmaMaxAgeMs). ξ does not TTL.', 'Call refreshSigma and ingest again, or proposeRelease then apply(ProposeResult) if the pattern changed. Never mechanical-safe.'),
78
- entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', 'Unvalved second freeze of ξ', 'release() ran after a pattern was already frozen and the new ξ differs. First freeze is release(); later pattern change is proposeRelease then apply.', 'Change ξ with proposeRelease then apply(ProposeResult). release() is only the first freeze. Never mechanical-safe.'),
78
+ entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', 'Second freeze without the valve', 'release() already froze the big choice. A later release() with a different value does not land. First freeze is release(); later change is proposeRelease then apply.', 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.'),
79
79
  // ── atomic preflight / change set ────────────────────────────────────────
80
80
  entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
81
81
  entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
@@ -21,6 +21,90 @@ import { detectGraphBlindSpots, printGraphBlindSection } from './graph-blind.mjs
21
21
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
22
22
  import { collectStewardNudge } from './team-parliament-io.mjs';
23
23
  import { formatArkRunDoctorLines, summarizeArkRunSection } from './ark-run-doctor.mjs';
24
+ import {
25
+ ARKORDER_FIRST_CONTACT_NEXT,
26
+ ARKORDER_ONE_BREATH,
27
+ formatArkOrderDoctorLines,
28
+ summarizeArkOrderSection,
29
+ } from './ark-order-doctor.mjs';
30
+ import { composeMergePlanesHonesty } from './extra-merge-teeth.mjs';
31
+
32
+ export function attachExtraDoctorSections(rulesUnderContract, config, classification, findings) {
33
+ const arkRulesMerge = {
34
+ active: rulesUnderContract?.active === true,
35
+ structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
36
+ structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
37
+ structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
38
+ invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
39
+ invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
40
+ invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
41
+ covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
42
+ uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
43
+ };
44
+ const arkRun = summarizeArkRunSection({
45
+ arkRun: config?.arkRun,
46
+ findings,
47
+ classification,
48
+ arkRules: arkRulesMerge,
49
+ });
50
+ const arkOrder = summarizeArkOrderSection({
51
+ arkOrder: config?.arkOrder,
52
+ findings,
53
+ classification,
54
+ arkRules: arkRulesMerge,
55
+ arkRun: {
56
+ present: arkRun.active === true,
57
+ mode: arkRun.mode,
58
+ residualCount: arkRun.residual?.count,
59
+ },
60
+ });
61
+ const mergePlanes = composeMergePlanesHonesty({
62
+ classification,
63
+ arkRules: arkRulesMerge,
64
+ arkRun: {
65
+ present: arkRun.active === true,
66
+ mode: arkRun.mode,
67
+ residualCount: arkRun.residual?.count,
68
+ },
69
+ arkOrder: {
70
+ present: arkOrder.active === true,
71
+ mode: arkOrder.mode,
72
+ residualCount: arkOrder.residual?.count,
73
+ },
74
+ });
75
+ if (rulesUnderContract?.mergePlanes) rulesUnderContract.mergePlanes = mergePlanes;
76
+ arkRun.mergePlanes = mergePlanes;
77
+ arkRun.failMergeWhen = mergePlanes.failMergeWhen;
78
+ arkOrder.mergePlanes = mergePlanes;
79
+ arkOrder.failMergeWhen = mergePlanes.failMergeWhen;
80
+ return { arkRun, arkOrder, mergePlanes };
81
+ }
82
+
83
+ export function printCompactExtraDoctorLines(advisories, io) {
84
+ const arkRun = advisories?.arkRun;
85
+ if (arkRun?.active === true && arkRun.notAScore === true) {
86
+ console.log('');
87
+ const residual = Number(arkRun.residual?.count) || 0;
88
+ io.line(residual > 0 ? io.warn : ' ', `ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`);
89
+ }
90
+ const arkOrder = advisories?.arkOrder;
91
+ if (arkOrder && arkOrder.notAScore === true) {
92
+ console.log('');
93
+ if (arkOrder.active === true) {
94
+ const residual = Number(arkOrder.residual?.count) || 0;
95
+ const keys =
96
+ Array.isArray(arkOrder.xiKeys) && arkOrder.xiKeys.length > 0
97
+ ? arkOrder.xiKeys.join(', ')
98
+ : 'unnamed';
99
+ const mark = residual > 0 ? io.warn : ' ';
100
+ io.line(mark, ARKORDER_ONE_BREATH);
101
+ io.line(mark, `ArkOrder: ${arkOrder.mode || 'on'} · xiKeys=${keys} · residual=${residual} · not a score`);
102
+ } else {
103
+ io.line(' ', ARKORDER_ONE_BREATH);
104
+ io.line(' ', ARKORDER_FIRST_CONTACT_NEXT);
105
+ }
106
+ }
107
+ }
24
108
 
25
109
  function classificationFromCoverage(cov) {
26
110
  return {
@@ -55,25 +139,12 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
55
139
  : undefined);
56
140
  const classification = classificationFromCoverage(cov);
57
141
  const rulesUnderContract = summarizeRulesUnderContract(root, config, factPaths, classification);
58
- const arkRun = summarizeArkRunSection({
59
- arkRun: config?.arkRun,
60
- findings: activeViolations,
142
+ const { arkRun, arkOrder } = attachExtraDoctorSections(
143
+ rulesUnderContract,
144
+ config,
61
145
  classification,
62
- arkRules: {
63
- active: rulesUnderContract?.active === true,
64
- structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
65
- structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
66
- structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
67
- invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
68
- invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
69
- invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
70
- covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
71
- uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
72
- },
73
- });
74
- if (rulesUnderContract?.mergePlanes) {
75
- rulesUnderContract.mergePlanes = arkRun.mergePlanes;
76
- }
146
+ activeViolations
147
+ );
77
148
  return {
78
149
  contractHealth: computeContractHealth(root, config, cov, rules),
79
150
  ambientState: computeAmbientState(ts, root, config, files),
@@ -86,6 +157,7 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
86
157
  stewardNudge: collectStewardNudge(root, config),
87
158
  rulesUnderContract,
88
159
  arkRun,
160
+ arkOrder,
89
161
  };
90
162
  }
91
163
 
@@ -116,4 +188,13 @@ export function printDoctorAdvisories(advisories, io) {
116
188
  io.line(mark, text);
117
189
  }
118
190
  }
191
+ const arkOrder = advisories.arkOrder;
192
+ if (arkOrder && arkOrder.notAScore === true) {
193
+ console.log('');
194
+ console.log(io.color.bold('ArkOrder (not a score)'));
195
+ const mark = arkOrder.active && arkOrder.residual?.count > 0 ? io.warn : ' ';
196
+ for (const text of formatArkOrderDoctorLines(arkOrder)) {
197
+ io.line(mark, text);
198
+ }
199
+ }
119
200
  }
@@ -4,10 +4,10 @@
4
4
  */
5
5
  import path from 'node:path';
6
6
  import { arkCommand } from '../ark-shared.mjs';
7
- import { operatingModeTitle } from './product-copy.mjs';
7
+ import { NORTH_STAR_ONE_LINE, operatingModeTitle } from './product-copy.mjs';
8
8
  import { isDoctorHealthyNothingToDo } from './post-green-path.mjs';
9
9
  import { printParseHealthSection } from './parse-health.mjs';
10
- import { printDoctorAdvisories } from './doctor-advisories.mjs';
10
+ import { printDoctorAdvisories, printCompactExtraDoctorLines } from './doctor-advisories.mjs';
11
11
  import { designDeltaDoctorLines } from './design-delta.mjs';
12
12
  import { enforcementDoctorLines } from './enforcement-state.mjs';
13
13
  import { analysisIncompleteStatement } from './analysis-completeness.mjs';
@@ -73,6 +73,7 @@ export function printDoctorCompactHuman(view) {
73
73
  const skippableCi = ciNotFailClosedNotice(view);
74
74
 
75
75
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
76
+ console.log(color.dim(NORTH_STAR_ONE_LINE));
76
77
  if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
77
78
  printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
78
79
 
@@ -164,15 +165,7 @@ export function printDoctorCompactHuman(view) {
164
165
  line(warn, nudge.ask);
165
166
  }
166
167
 
167
- const arkRun = doctorAdvisories.arkRun;
168
- if (arkRun?.active === true && arkRun.notAScore === true) {
169
- console.log('');
170
- const residual = Number(arkRun.residual?.count) || 0;
171
- line(
172
- residual > 0 ? warn : ' ',
173
- `ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`
174
- );
175
- }
168
+ printCompactExtraDoctorLines(doctorAdvisories, { line, warn });
176
169
 
177
170
  if (violations.length === 0) {
178
171
  if (!analysisComplete) {
@@ -658,11 +658,11 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
658
658
  pilotTarget: residualPilot?.pilotTarget ?? residualPilot?.pilot ?? null,
659
659
  arkRulesMergeHonesty: rulesUnderContract?.mergePlanes
660
660
  ? {
661
- active: rulesUnderContract.active === true || arkRun?.active === true,
661
+ active: rulesUnderContract.active === true || arkRun?.active === true || doctorAdvisories.arkOrder?.active === true,
662
662
  ...rulesUnderContract.mergePlanes,
663
663
  }
664
- : rulesUnderContract?.active === true || arkRun?.active === true
665
- ? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true }
664
+ : rulesUnderContract?.active === true || arkRun?.active === true || doctorAdvisories.arkOrder?.active === true
665
+ ? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true || doctorAdvisories.arkOrder?.extraMergeTeeth === true }
666
666
  : null,
667
667
  primaryNextAction:
668
668
  adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
@@ -757,6 +757,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
757
757
  ...doctorAdvisories,
758
758
  rulesUnderContract,
759
759
  arkRun,
760
+ arkOrder: doctorAdvisories.arkOrder,
760
761
  // P0-B — single anti-false-green honesty surface (never a score).
761
762
  productHonesty,
762
763
  governed: cov.governed,