arkgate 4.6.7 → 4.7.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 (56) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +20 -9
  3. package/SECURITY.md +1 -1
  4. package/bin/ark-check-runtime.mjs +13 -1
  5. package/bin/ark-mcp-runtime.mjs +65 -3
  6. package/bin/lib/adapter-contract.mjs +17 -36
  7. package/bin/lib/analysis-engine.mjs +6 -6
  8. package/bin/lib/ark-run-doctor.mjs +144 -0
  9. package/bin/lib/ark-run-facts.mjs +472 -0
  10. package/bin/lib/ark-run-report.mjs +57 -0
  11. package/bin/lib/ark-run-sensors.mjs +309 -0
  12. package/bin/lib/config-contract.mjs +86 -11
  13. package/bin/lib/diagnostic-catalog.mjs +8 -0
  14. package/bin/lib/doctor-advisories.mjs +45 -8
  15. package/bin/lib/doctor-human.mjs +10 -0
  16. package/bin/lib/doctor-plan.mjs +20 -16
  17. package/bin/lib/extra-merge-teeth.mjs +187 -0
  18. package/bin/lib/html-report-advisories.mjs +2 -0
  19. package/bin/lib/html-report-depth.mjs +22 -2
  20. package/bin/lib/html-report.mjs +16 -0
  21. package/bin/lib/remediation.mjs +132 -0
  22. package/bin/lib/resolved-candidate-facts.mjs +67 -2
  23. package/bin/lib/rules-under-contract.mjs +37 -89
  24. package/bin/lib/snippet-analysis.mjs +43 -2
  25. package/bin/lib/status-command.mjs +28 -0
  26. package/bin/lib/status-manifest.mjs +23 -0
  27. package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
  28. package/dist/eslint/index.cjs +6 -2
  29. package/dist/eslint/index.d.ts +70 -2
  30. package/dist/eslint/index.js +6 -2
  31. package/dist/index.cjs +35 -35
  32. package/dist/index.d.ts +787 -272
  33. package/dist/index.js +35 -35
  34. package/docs/README.md +2 -1
  35. package/docs/agent-guide.md +21 -15
  36. package/docs/ai-gates.md +13 -0
  37. package/docs/configuration.md +24 -11
  38. package/docs/develop.md +12 -3
  39. package/docs/diagnostics.md +75 -0
  40. package/docs/enthusiast/README.md +4 -3
  41. package/docs/package-surface.md +16 -13
  42. package/docs/product-voice.md +6 -3
  43. package/docs/threat-model.md +1 -1
  44. package/docs/use.md +5 -4
  45. package/package.json +1 -1
  46. package/schemas/ark.config.schema.json +41 -2
  47. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  48. package/schemas/ark.status-manifest.schema.json +47 -0
  49. package/server.json +2 -2
  50. package/templates/agent-skills/README.md +1 -1
  51. package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
  52. package/templates/agent-skills/ark-place/SKILL.md +26 -2
  53. package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
  54. package/templates/skills/ark-adopt.md +23 -2
  55. package/templates/skills/ark-place.md +26 -2
  56. package/templates/skills/ark-runtime.md +66 -24
@@ -7,6 +7,11 @@
7
7
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
8
8
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
9
9
  import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
10
+ import {
11
+ EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
12
+ composeMergePlanesHonesty,
13
+ demoteExtraPlaneTeethUnderClassificationFloor,
14
+ } from './extra-merge-teeth.mjs';
10
15
 
11
16
  /**
12
17
  * Cap long catalogs in doctor JSON (and HTML, which consumes the same summary).
@@ -16,12 +21,11 @@ const COVERED_SAMPLE_MAX = 24;
16
21
  const STRUCTURE_CATALOG_MAX = 40;
17
22
  const UNCOVERED_CATALOG_MAX = 30;
18
23
 
19
- /** Minimum governed % before enforced ArkRules may arm extra merge teeth (P1M / FG-EXTRATEETH). */
20
- export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
24
+ export { EXTRA_MERGE_TEETH_GOVERNED_FLOOR };
21
25
 
22
26
  /**
23
27
  * P1M / extraMergeTeeth: under the classification floor, demote enforced ArkRules
24
- * structure/invariant findings so merge matches doctor stamp (layer graph only).
28
+ * and ArkRun findings so merge matches doctor stamp (layer graph only).
25
29
  * Unknown classification (null/null) → do not demote (contract-only callers).
26
30
  *
27
31
  * @param {object[]} violations
@@ -29,28 +33,7 @@ export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
29
33
  * @returns {object[]}
30
34
  */
31
35
  export function demoteArkRuleTeethUnderClassificationFloor(violations, classification = {}) {
32
- if (!Array.isArray(violations)) return violations;
33
- const governed =
34
- typeof classification.governedPercent === 'number' ? classification.governedPercent : null;
35
- const populated =
36
- typeof classification.populatedLayerCount === 'number'
37
- ? classification.populatedLayerCount
38
- : null;
39
- if (governed == null && populated == null) return violations;
40
- const allowsTeeth =
41
- (governed ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR && (populated ?? 0) >= 1;
42
- if (allowsTeeth) return violations;
43
- for (const v of violations) {
44
- const isArkRule =
45
- v?.arkruleId != null ||
46
- (typeof v?.ruleId === 'string' &&
47
- (v.ruleId.startsWith('ARKRULE') || v.ruleId.startsWith('arkrule')));
48
- if (isArkRule && v.failsStrict !== false) {
49
- v.failsStrict = false;
50
- if (v.severity === 'error') v.severity = 'warning';
51
- }
52
- }
53
- return violations;
36
+ return demoteExtraPlaneTeethUnderClassificationFloor(violations, classification);
54
37
  }
55
38
 
56
39
  /**
@@ -63,6 +46,18 @@ export function demoteArkRuleTeethUnderClassificationFloor(violations, classific
63
46
  * classifiedFiles?: number | null,
64
47
  * }} [classification] layer-plane coverage (when known)
65
48
  */
49
+ function arkRunMergeInput(config, residualCount = 0) {
50
+ const extra = config?.arkRun;
51
+ if (!extra || typeof extra !== 'object') {
52
+ return { present: false, mode: null, residualCount: 0 };
53
+ }
54
+ return {
55
+ present: true,
56
+ mode: extra.mode === 'enforced' || extra.mode === 'advisory' ? extra.mode : null,
57
+ residualCount: Number(residualCount) || 0,
58
+ };
59
+ }
60
+
66
61
  export function summarizeRulesUnderContract(root, config, facts, classification) {
67
62
  if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
68
63
  return {
@@ -71,6 +66,11 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
71
66
  invariants: 0,
72
67
  coveredInvariants: 0,
73
68
  uncoveredInvariants: 0,
69
+ mergePlanes: composeMergePlanesHonesty({
70
+ classification,
71
+ arkRules: { active: false },
72
+ arkRun: arkRunMergeInput(config),
73
+ }),
74
74
  notAScore: true,
75
75
  note: 'No arkRules map — intra-layer ArkRules are opt-in.',
76
76
  };
@@ -162,73 +162,21 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
162
162
  const invariantAdvisory = invariants - invariantEnforced;
163
163
  const coveredInvariants = coverage.coverage.filter((c) => c.covered).length;
164
164
  const uncoveredInvariants = coverage.coverage.filter((c) => !c.covered).length;
165
- const hasEnforcedTeeth = structureEnforced > 0 || invariantEnforced > 0;
166
- // P1M-EXTRATEETH-EMPTY-GRAPH / FG-EXTRATEETH-EMPTY-CLASSIFICATION:
167
- // Do not arm structure/invariant merge teeth when the layer plane is empty or
168
- // barely classified (e.g. 0% governed). Classification unknown → allow teeth
169
- // (contract-only callers / unit tests without coverage).
170
- const governedPercent =
171
- classification && typeof classification.governedPercent === 'number'
172
- ? classification.governedPercent
173
- : null;
174
- const populatedLayerCount =
175
- classification && typeof classification.populatedLayerCount === 'number'
176
- ? classification.populatedLayerCount
177
- : classification && typeof classification.classifiedFiles === 'number'
178
- ? classification.classifiedFiles > 0
179
- ? 1
180
- : 0
181
- : null;
182
- const classificationKnown = governedPercent != null || populatedLayerCount != null;
183
- const classificationAllowsTeeth = !classificationKnown
184
- ? true
185
- : (governedPercent ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR &&
186
- (populatedLayerCount ?? 0) >= 1;
187
- const extraMergeTeeth = hasEnforcedTeeth && classificationAllowsTeeth;
188
- const teethDeferredForClassification =
189
- hasEnforcedTeeth && classificationKnown && !classificationAllowsTeeth;
190
- // P1-M — which plane can fail merge (layers vs enforced structure vs invariants).
191
- const mergePlanes = {
192
- layers: {
193
- role: 'inter-layer-edges',
194
- alwaysOnGate: true,
195
- note: 'Import/export layer graph — the default merge plane. Absent arkRules changes nothing here.',
196
- },
197
- structureSensors: {
198
- role: 'intra-layer-heuristics',
199
- total: structureRules,
200
- enforced: structureEnforced,
201
- advisory: structureAdvisory,
202
- note: 'Structure sensors are heuristics (prefer false negatives). Only mode:enforced fails merge; noisy sensors stay advisory by default. Advisory-only packs never add merge teeth (FG-ARKRULES-ADVISORY-ONLY).',
203
- },
204
- invariants: {
205
- role: 'catalog-plus-coverage',
206
- total: invariants,
207
- enforced: invariantEnforced,
208
- advisory: invariantAdvisory,
165
+ const mergePlanes = composeMergePlanesHonesty({
166
+ classification,
167
+ arkRules: {
168
+ active: true,
169
+ structureEnforced,
170
+ structureTotal: structureRules,
171
+ structureAdvisory,
172
+ invariantEnforced,
173
+ invariantTotal: invariants,
174
+ invariantAdvisory,
209
175
  covered: coveredInvariants,
210
176
  uncovered: uncoveredInvariants,
211
- note: 'Invariants are catalog + coverage evidence, not a business runtime. Enforced + proven-uncovered fails merge; absence of enforced rules adds no extra teeth.',
212
177
  },
213
- dualPlaneStamp:
214
- 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime). The two planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth.',
215
- extraMergeTeeth,
216
- ...(classificationKnown
217
- ? {
218
- classificationGate: {
219
- governedPercent: governedPercent ?? null,
220
- populatedLayerCount: populatedLayerCount ?? null,
221
- floorPercent: EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
222
- allowsTeeth: classificationAllowsTeeth,
223
- },
224
- }
225
- : {}),
226
- failMergeWhen: extraMergeTeeth
227
- ? 'Layer graph failures plus enforced structure/invariant findings (advisory sensors never fail merge alone).'
228
- : teethDeferredForClassification
229
- ? `Layer graph only — enforced ArkRules structure/invariant findings are demoted under the teeth floor (need ≥${EXTRA_MERGE_TEETH_GOVERNED_FLOOR}% governed and ≥1 populated layer); they do not merge-block until classification is honest.`
230
- : 'Layer graph only — no enforced ArkRules structure/invariant teeth on this tree. Advisory packs do not arm merge teeth.',
231
- };
178
+ arkRun: arkRunMergeInput(config),
179
+ });
232
180
 
233
181
  return {
234
182
  active: true,
@@ -1,5 +1,7 @@
1
1
  /** Fail-closed completeness evidence for one proposed source snippet. */
2
+ import { layerForRelativePath } from '../ark-layer-match.mjs';
2
3
  import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
4
+ import { evaluateArkRunEditorSensorsFromSource } from './ark-run-sensors.mjs';
3
5
 
4
6
  export function flattenTsParseDiagnostics(ts, diagnostics, sourceFile) {
5
7
  if (!Array.isArray(diagnostics) || !ts) return [];
@@ -31,11 +33,50 @@ function finding(ruleId, message, file, nextAction) {
31
33
  };
32
34
  }
33
35
 
36
+ function arkRunSnippetViolations(source, context = {}) {
37
+ const extra = context.arkRun;
38
+ const layers = context.layers;
39
+ const file = context.relFile || context.filePath;
40
+ if (!extra || !Array.isArray(layers) || typeof file !== 'string' || file.length === 0) {
41
+ return [];
42
+ }
43
+ const layerForFile = (pathValue) => {
44
+ if (pathValue === file && typeof context.layer === 'string') return context.layer;
45
+ return layerForRelativePath(pathValue, layers);
46
+ };
47
+ const { findings } = evaluateArkRunEditorSensorsFromSource({
48
+ arkRun: extra,
49
+ layers,
50
+ file,
51
+ source,
52
+ layerForFile,
53
+ classification: context.classification,
54
+ });
55
+ return findings
56
+ .filter((finding) => finding.failsStrict)
57
+ .map((finding) => ({
58
+ ruleId: finding.ruleId,
59
+ code: finding.ruleId,
60
+ message: finding.message,
61
+ file: finding.file,
62
+ line: finding.line,
63
+ fromLayer: finding.fromLayer,
64
+ target: finding.target,
65
+ nextAction: finding.nextAction,
66
+ failsStrict: true,
67
+ severity: 'error',
68
+ }));
69
+ }
70
+
34
71
  export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
35
72
  const observed = gate.validate(source, context);
73
+ const arkRunViolations = arkRunSnippetViolations(source, context);
36
74
  const base = {
37
- valid: Boolean(observed.lexicalValid ?? observed.valid),
38
- violations: Array.isArray(observed.violations) ? observed.violations : [],
75
+ valid: Boolean(observed.lexicalValid ?? observed.valid) && arkRunViolations.length === 0,
76
+ violations: [
77
+ ...(Array.isArray(observed.violations) ? observed.violations : []),
78
+ ...arkRunViolations,
79
+ ],
39
80
  };
40
81
  const file = context.filePath;
41
82
 
@@ -25,6 +25,7 @@ import { detectActiveAgentHost } from './skill-install.mjs';
25
25
  import { readBaseline } from './violations.mjs';
26
26
  import { reportsDir, readJsonSafe } from './html-report.mjs';
27
27
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
28
+ import { projectStatusArkRun } from './ark-run-doctor.mjs';
28
29
  import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
29
30
  import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
30
31
 
@@ -340,6 +341,19 @@ export function collectStatusFacts(options = {}) {
340
341
 
341
342
  const arkruleFrozenFallback = countArkruleFrozenKeys(baseline);
342
343
 
344
+ const arkRun = (() => {
345
+ const extra = config?.arkRun;
346
+ if (!extra || typeof extra !== 'object') {
347
+ return projectStatusArkRun({ present: false, residual: 0 });
348
+ }
349
+ const snap = latest?.arkRun && typeof latest.arkRun === 'object' ? latest.arkRun : null;
350
+ const mode = extra.mode === 'enforced' || extra.mode === 'advisory' ? extra.mode : null;
351
+ const extraMergeTeeth =
352
+ snap && typeof snap.extraMergeTeeth === 'boolean' ? snap.extraMergeTeeth === true : false;
353
+ const residual = typeof snap?.residual === 'number' ? snap.residual : null;
354
+ return projectStatusArkRun({ present: true, mode, extraMergeTeeth, residual });
355
+ })();
356
+
343
357
  // DF02 — always project compass with honesty mode (never invent green residual).
344
358
  // Prefer explicit override (tests/MCP inject doctor-facts); else report snapshot.
345
359
  let improvementCompass = null;
@@ -388,6 +402,7 @@ export function collectStatusFacts(options = {}) {
388
402
  latest?.leftoverDesignWork === true ||
389
403
  latest?.designFitness?.designWeak === true ||
390
404
  latest?.doctor?.designFitness?.designWeak === true,
405
+ arkRun: options.arkRun ?? arkRun,
391
406
  adopted:
392
407
  options.adopted ??
393
408
  classifyAdopted({
@@ -506,6 +521,19 @@ export function runStatusCommand(args = {}) {
506
521
  );
507
522
  }
508
523
  if (manifest.vsBase?.line) write(` ${manifest.vsBase.line}`);
524
+ const arkRunLine = manifest.arkRun;
525
+ if (arkRunLine && arkRunLine.notAScore === true) {
526
+ const residual =
527
+ arkRunLine.residual == null ? 'unknown' : String(arkRunLine.residual);
528
+ write(
529
+ ` arkRun: ${arkRunLine.present ? arkRunLine.mode || 'on' : 'absent'}` +
530
+ ` · residual=${residual}` +
531
+ (arkRunLine.present
532
+ ? ` · extraMergeTeeth=${arkRunLine.extraMergeTeeth === true}`
533
+ : '') +
534
+ ' · not a score'
535
+ );
536
+ }
509
537
  write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
510
538
  }
511
539
 
@@ -8,6 +8,7 @@
8
8
  * Pure CLI helper (bin/lib/status-manifest.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { projectStatusArkRun } from './ark-run-doctor.mjs';
11
12
  export const ARK_STATUS_MANIFEST_SCHEMA_VERSION = '1.0';
12
13
  export const ARK_STATUS_MANIFEST_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.status-manifest.schema.json';
13
14
  /**
@@ -242,6 +243,12 @@ export function resolveStatusNextAction(facts, binding, activation, lastCheck, r
242
243
  summary: 'ArkRules residual remains frozen — review inventory debt without claiming a score.',
243
244
  };
244
245
  }
246
+ if (facts.arkRun?.present === true && (facts.arkRun.residual ?? 0) > 0) {
247
+ return {
248
+ id: 'review-arkrun-residual',
249
+ summary: 'ArkRun residual remains — wire kernel usage or declarations through @arkgate/runtime. Not a score.',
250
+ };
251
+ }
245
252
  if (facts.adopted === 'required-merge' || facts.adopted === 'advisory-only-acked') {
246
253
  return {
247
254
  id: 'stay-enforced',
@@ -326,6 +333,9 @@ export function buildStatusManifest(facts) {
326
333
  if (facts.vsBase && typeof facts.vsBase.baseRef === 'string' && facts.vsBase.baseRef.length > 0) {
327
334
  status.vsBase = facts.vsBase;
328
335
  }
336
+ if (facts.arkRun && typeof facts.arkRun === 'object') {
337
+ status.arkRun = projectStatusArkRun(facts.arkRun);
338
+ }
329
339
  return status;
330
340
  }
331
341
  const STATUS_COMPASS_MODE_SET = new Set(STATUS_COMPASS_MODES);
@@ -605,5 +615,18 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
605
615
  baselineGrew: { type: 'boolean' },
606
616
  },
607
617
  },
618
+ arkRun: {
619
+ type: 'object',
620
+ description: 'ArkRun extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
621
+ additionalProperties: false,
622
+ required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
623
+ properties: {
624
+ notAScore: { const: true },
625
+ present: { type: 'boolean' },
626
+ mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
627
+ extraMergeTeeth: { type: 'boolean' },
628
+ residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
629
+ },
630
+ },
608
631
  },
609
632
  };
@@ -6,7 +6,7 @@
6
6
  * must stay self-contained: type-only imports/exports are erased on transpile, so
7
7
  * this split never reaches bin/lib/config-contract.mjs.
8
8
  */
9
- type ArkConfigSchemaVersion = '1.0' | '1.1';
9
+ type ArkConfigSchemaVersion = '1.0' | '1.1' | '1.2';
10
10
  type ArkConfigCyclePolicy = 'strict' | 'soft' | 'framework-soft' | 'off';
11
11
  type ArkConfigLayerCapabilities = {
12
12
  deny?: string[];
@@ -51,6 +51,18 @@ type ArkConfigSafety = {
51
51
  * Absence changes no inter-layer verdict.
52
52
  */
53
53
  type ArkConfigArkRulesRefs = Record<string, string>;
54
+ /** ADR 0020 — advisory never adds merge teeth; enforced is the extra's merge plane. */
55
+ type ArkConfigArkRunMode = 'advisory' | 'enforced';
56
+ /**
57
+ * ADR 0020 — optional inline ArkRun extra (schema 1.2+). Absence is silent.
58
+ * Present objects are fully defaulted by the loader.
59
+ */
60
+ type ArkConfigArkRun = {
61
+ mode: ArkConfigArkRunMode;
62
+ compositionRoots: string[];
63
+ managedLayers: string[];
64
+ requireDeclarations: boolean;
65
+ };
54
66
  type ArkConfig = {
55
67
  $schema: string;
56
68
  schemaVersion: ArkConfigSchemaVersion;
@@ -66,6 +78,8 @@ type ArkConfig = {
66
78
  safety?: ArkConfigSafety;
67
79
  /** ADR 0012 — modular ArkRules references (schema 1.1+). */
68
80
  arkRules?: ArkConfigArkRulesRefs;
81
+ /** ADR 0020 — optional ArkRun extra (schema 1.2+). Absence changes no Layers/ArkRules verdict. */
82
+ arkRun?: ArkConfigArkRun;
69
83
  /**
70
84
  * Optional GitHub handles or emails who may loosen the contract or grow the baseline.
71
85
  * Metadata — excluded from policy hash. Absence means no steward lock (policy-ack still applies).
@@ -77,10 +91,10 @@ type ArkConfigIssue = {
77
91
  message: string;
78
92
  };
79
93
  /** Original input version when the loader rewrote schemaVersion toward current. */
80
- type ArkConfigMigratedFrom = 'unversioned' | '1.0' | null;
94
+ type ArkConfigMigratedFrom = 'unversioned' | '1.0' | '1.1' | null;
81
95
  type ArkConfigLoadResult = {
82
96
  config: ArkConfig;
83
97
  migratedFrom: ArkConfigMigratedFrom;
84
98
  };
85
99
 
86
- export type { ArkConfig as A, ArkConfigRule as a, ArkConfigSchemaVersion as b, ArkConfigLoadResult as c, ArkConfigLayer as d, ArkConfigIssue as e };
100
+ export type { ArkConfig as A, ArkConfigRule as a, ArkConfigSchemaVersion as b, ArkConfigLoadResult as c, ArkConfigLayer as d, ArkConfigIssue as e, ArkConfigArkRun as f };