arkgate 4.8.15 → 4.8.17

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 (69) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +5 -1
  4. package/bin/ark-mcp-runtime.mjs +14 -7
  5. package/bin/ark-shared.mjs +2 -0
  6. package/bin/ark.mjs +17 -8
  7. package/bin/lib/adr-path.mjs +116 -0
  8. package/bin/lib/adr-presence.mjs +3 -2
  9. package/bin/lib/analysis-engine.mjs +6 -6
  10. package/bin/lib/architecture-scan.mjs +17 -0
  11. package/bin/lib/baseline-key.mjs +2 -0
  12. package/bin/lib/config-contract.mjs +1 -1
  13. package/bin/lib/diagnostic-catalog.mjs +2 -0
  14. package/bin/lib/doctor-advisories.mjs +26 -0
  15. package/bin/lib/doctor-green-cite.mjs +139 -0
  16. package/bin/lib/doctor-human.mjs +98 -63
  17. package/bin/lib/doctor-next-actions.mjs +9 -0
  18. package/bin/lib/doctor-plan.mjs +6 -3
  19. package/bin/lib/field-install.mjs +47 -7
  20. package/bin/lib/first-run-help.mjs +3 -1
  21. package/bin/lib/improvement-compass-doctor.mjs +3 -1
  22. package/bin/lib/improvement-compass-map.mjs +3 -1
  23. package/bin/lib/invariant-coverage.mjs +121 -0
  24. package/bin/lib/invariant-tests-path.mjs +212 -0
  25. package/bin/lib/package-manager.mjs +8 -0
  26. package/bin/lib/policy-delta-io.mjs +19 -10
  27. package/bin/lib/prototype-shortcuts.mjs +224 -0
  28. package/bin/lib/remediation.mjs +11 -0
  29. package/bin/lib/start-preview.mjs +20 -1
  30. package/dist/{configTypes-Dt3DpVbd.d.ts → configTypes-VD0qcubY.d.ts} +2 -1
  31. package/dist/{diagnosticCatalog-BEg85XlE.d.ts → diagnosticCatalog-DVABouBV.d.ts} +30 -3
  32. package/dist/eslint/index.cjs +4 -4
  33. package/dist/eslint/index.d.ts +1 -1
  34. package/dist/eslint/index.js +4 -4
  35. package/dist/index.cjs +31 -31
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +31 -31
  38. package/dist/nestjs/index.cjs +1 -1
  39. package/dist/nestjs/index.d.ts +3 -3
  40. package/dist/nestjs/index.js +1 -1
  41. package/dist/runtime/index.cjs +10 -10
  42. package/dist/runtime/index.d.ts +6 -6
  43. package/dist/runtime/index.js +10 -10
  44. package/dist/{types-TBiv0WHL.d.ts → types-BSzRy2X1.d.ts} +1 -1
  45. package/dist/{types-CN9tVMPz.d.ts → types-D5GT5ZT8.d.ts} +1 -1
  46. package/docs/README.md +1 -1
  47. package/docs/agent-guide.md +3 -1
  48. package/docs/configuration.md +17 -6
  49. package/docs/develop.md +3 -1
  50. package/docs/diagnostics.md +25 -1
  51. package/docs/enthusiast/how-to-agent-gates.md +3 -1
  52. package/docs/package-surface.md +2 -2
  53. package/docs/use.md +5 -3
  54. package/package.json +1 -1
  55. package/schemas/ark.config.schema.json +1 -1
  56. package/server.json +2 -2
  57. package/templates/agent-skills/README.md +1 -1
  58. package/templates/agent-skills/ark-adopt/SKILL.md +19 -2
  59. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -1
  60. package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
  61. package/templates/agent-skills/ark-explain/SKILL.md +2 -1
  62. package/templates/agent-skills/ark-explore/SKILL.md +22 -1
  63. package/templates/agent-skills/ark-place/SKILL.md +4 -0
  64. package/templates/skills/ark-adopt.md +19 -2
  65. package/templates/skills/ark-autopilot.md +6 -1
  66. package/templates/skills/ark-coverage.md +1 -1
  67. package/templates/skills/ark-explain.md +2 -1
  68. package/templates/skills/ark-explore.md +22 -1
  69. package/templates/skills/ark-place.md +4 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Soft prototype-shortcut residual (P2 §3 / Guiar).
3
+ * Tooling I/O. Never a gate fail.
4
+ *
5
+ * SQLite / JSON-file stores standing in for a declared Persistence home,
6
+ * and admin / god-mode literals sitting outside Domain or an auth tag.
7
+ * Silent when the contract does not imply those houses, the shortcut is
8
+ * already in the right house, or the tree has no such markers.
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { layerForFile } from '../ark-shared.mjs';
14
+ import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
15
+ import { isNonProductionPilotPath } from './design-smells.mjs';
16
+
17
+ export const PROTOTYPE_KIND = Object.freeze({
18
+ SQLITE: 'sqlite',
19
+ JSON_FILE: 'json-file',
20
+ ADMIN: 'admin-literals',
21
+ });
22
+
23
+ export const PROTOTYPE_SQLITE_ASK =
24
+ 'SQLite is the standing store, and it is not in Persistence.';
25
+
26
+ export const PROTOTYPE_JSON_ASK =
27
+ 'A JSON file is standing in for Persistence.';
28
+
29
+ export const PROTOTYPE_ADMIN_ASK =
30
+ 'Admin / god-mode literals sit outside Domain (or an auth tag).';
31
+
32
+ export const PROTOTYPE_PACK_ASK =
33
+ 'Prototype shortcuts are still standing in for Persistence or Domain policy.';
34
+
35
+ export const PROTOTYPE_PERSISTENCE_NEXT =
36
+ 'Create a Persistence adapter (/ark-place), then move the file store behind it (/ark-autopilot). Do not harden the shortcut.';
37
+
38
+ export const PROTOTYPE_ADMIN_NEXT =
39
+ 'Move admin / god-mode literals behind Domain or an auth tag (/ark-place). Then one small refactor with /ark-autopilot.';
40
+
41
+ export const PROTOTYPE_PACK_NEXT =
42
+ 'Create a Persistence adapter (/ark-place) for the file store, and move admin literals behind Domain or an auth tag. Then one small refactor with /ark-autopilot.';
43
+
44
+ const SOURCE_EXT = /\.(?:ts|tsx|js|jsx|mts|cts)$/;
45
+ const MAX_FILE_BYTES = 256 * 1024;
46
+ const MAX_SCAN_FILES = 800;
47
+
48
+ const SQLITE_IMPORT_RE =
49
+ /\b(?:from|require\()\s*['"](?:better-sqlite3|sqlite3|sql\.js|bun:sqlite|node:sqlite)['"]/;
50
+
51
+ const JSON_DB_IMPORT_RE = /\b(?:from|require\()\s*['"](?:lowdb|node-json-db)['"]/;
52
+
53
+ const JSON_DB_PATH_RE =
54
+ /['"`](?:[^'"`]*[\\/])?(?:db|database|store|data-store|dump)\.json['"`]/i;
55
+
56
+ const FS_OR_JSON_RE = /\b(?:readFile|writeFile|readFileSync|writeFileSync|JSON\.parse|JSON\.stringify)\b/;
57
+
58
+ const PRIVILEGE_LITERAL_RE =
59
+ /['"](?:admin|superadmin|super-admin|superuser|god|god-mode|godmode)['"]/i;
60
+
61
+ const PRIVILEGE_CONTEXT_RE =
62
+ /\b(?:role|roles|privilege|permission|permissions|acl|isAdmin|isSuperuser|godMode|GOD_MODE)\b/;
63
+
64
+ const HARDCODED_ADMIN_RE = /\bisAdmin\s*=\s*true\b/;
65
+
66
+ const GOD_IDENT_RE = /\b(?:isGodMode|godMode|GOD_MODE|isSuperuser)\b/;
67
+
68
+ /** Persistence-role house by name. Align with design-smells persistence heuristic. */
69
+ export function isPersistenceRoleLayerName(name) {
70
+ return typeof name === 'string' && /persist|repository|data.?access/i.test(name);
71
+ }
72
+
73
+ function configLayers(input) {
74
+ return Array.isArray(input.config?.layers) ? input.config.layers : [];
75
+ }
76
+
77
+ function stripComments(source) {
78
+ return source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/\/\/[^\n]*/g, ' ');
79
+ }
80
+
81
+ function readTextLimited(absPath) {
82
+ try {
83
+ const st = fs.statSync(absPath);
84
+ if (!st.isFile() || st.size === 0 || st.size > MAX_FILE_BYTES) return null;
85
+ return fs.readFileSync(absPath, 'utf8');
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function normalizeRel(root, filePath) {
92
+ const abs = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
93
+ let rel = path.relative(root, abs).split(path.sep).join('/');
94
+ if (rel.startsWith('./')) rel = rel.slice(2);
95
+ return { abs, rel };
96
+ }
97
+
98
+ function hasSqliteShortcut(source) {
99
+ return SQLITE_IMPORT_RE.test(source);
100
+ }
101
+
102
+ function hasJsonFileShortcut(source) {
103
+ if (JSON_DB_IMPORT_RE.test(source)) return true;
104
+ return JSON_DB_PATH_RE.test(source) && FS_OR_JSON_RE.test(source);
105
+ }
106
+
107
+ function hasAdminLiteral(source) {
108
+ if (HARDCODED_ADMIN_RE.test(source) || GOD_IDENT_RE.test(source)) return true;
109
+ return PRIVILEGE_LITERAL_RE.test(source) && PRIVILEGE_CONTEXT_RE.test(source);
110
+ }
111
+
112
+ function layerNameFor(root, rel, config) {
113
+ try {
114
+ return layerForFile(root, rel, config?.layers ?? []) ?? null;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ function layerByName(config, name) {
121
+ return configLayers({ config }).find((layer) => layer.name === name) ?? null;
122
+ }
123
+
124
+ function isTrustPolicyLayer(layer) {
125
+ const tag = layer?.trustBoundary;
126
+ return tag === 'auth' || tag === 'admin';
127
+ }
128
+
129
+ function composeAsk(kinds) {
130
+ if (kinds.length === 1) {
131
+ if (kinds[0] === PROTOTYPE_KIND.SQLITE) return PROTOTYPE_SQLITE_ASK;
132
+ if (kinds[0] === PROTOTYPE_KIND.JSON_FILE) return PROTOTYPE_JSON_ASK;
133
+ return PROTOTYPE_ADMIN_ASK;
134
+ }
135
+ return PROTOTYPE_PACK_ASK;
136
+ }
137
+
138
+ function composeNext(kinds) {
139
+ const persist = kinds.includes(PROTOTYPE_KIND.SQLITE) || kinds.includes(PROTOTYPE_KIND.JSON_FILE);
140
+ const admin = kinds.includes(PROTOTYPE_KIND.ADMIN);
141
+ if (persist && admin) return PROTOTYPE_PACK_NEXT;
142
+ if (admin) return PROTOTYPE_ADMIN_NEXT;
143
+ return PROTOTYPE_PERSISTENCE_NEXT;
144
+ }
145
+
146
+ /**
147
+ * Soft residual when prototype stores or privilege literals are standing in
148
+ * for a declared Persistence / Domain (or auth) house. Never flips valid.
149
+ *
150
+ * @param {{ root?: string, config?: object, coverage?: object, files?: readonly string[] }} [input]
151
+ * @returns {{ kinds: string[], ask: string, nextAction: string, evidence: string[], persistenceLayers: string[], domainLayers: string[] } | null}
152
+ */
153
+ export function collectPrototypeShortcutsResidual(input = {}) {
154
+ const declared = configLayers(input);
155
+ const persistenceLayers = declared.filter((layer) => isPersistenceRoleLayerName(layer.name));
156
+ const domainLayers = declared.filter((layer) =>
157
+ isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? [])
158
+ );
159
+ const trustPolicyLayers = declared.filter((layer) => isTrustPolicyLayer(layer));
160
+ const persistEligible = persistenceLayers.length > 0;
161
+ const adminEligible = domainLayers.length > 0 || trustPolicyLayers.length > 0;
162
+ if (!persistEligible && !adminEligible) return null;
163
+
164
+ const root = typeof input.root === 'string' ? input.root : '';
165
+ const files = Array.isArray(input.files) ? input.files : [];
166
+ if (files.length === 0) return null;
167
+
168
+ const persistNames = new Set(persistenceLayers.map((layer) => layer.name));
169
+ const domainNames = new Set(domainLayers.map((layer) => layer.name));
170
+
171
+ const sqliteHits = [];
172
+ const jsonHits = [];
173
+ const adminHits = [];
174
+ let scanned = 0;
175
+
176
+ for (const file of files) {
177
+ if (scanned >= MAX_SCAN_FILES) break;
178
+ const { abs, rel } = normalizeRel(root, file);
179
+ if (!rel || rel.startsWith('..')) continue;
180
+ if (!SOURCE_EXT.test(rel)) continue;
181
+ if (rel.includes('node_modules/') || rel.endsWith('.d.ts')) continue;
182
+ if (isNonProductionPilotPath(rel)) continue;
183
+ const source = readTextLimited(abs);
184
+ if (source == null) continue;
185
+ scanned += 1;
186
+ const code = stripComments(source);
187
+ const layer = layerNameFor(root, rel, input.config);
188
+ const inPersistence = Boolean(layer && persistNames.has(layer));
189
+ const policyHome =
190
+ Boolean(layer && domainNames.has(layer)) || isTrustPolicyLayer(layerByName(input.config, layer));
191
+
192
+ if (persistEligible && !inPersistence) {
193
+ if (hasSqliteShortcut(code)) sqliteHits.push(rel);
194
+ if (hasJsonFileShortcut(code)) jsonHits.push(rel);
195
+ }
196
+ if (adminEligible && !policyHome && hasAdminLiteral(code)) adminHits.push(rel);
197
+ }
198
+
199
+ const kinds = [];
200
+ const evidence = [];
201
+ if (persistEligible && sqliteHits.length > 0) {
202
+ kinds.push(PROTOTYPE_KIND.SQLITE);
203
+ evidence.push(...sqliteHits);
204
+ }
205
+ if (persistEligible && jsonHits.length > 0) {
206
+ kinds.push(PROTOTYPE_KIND.JSON_FILE);
207
+ evidence.push(...jsonHits);
208
+ }
209
+ if (adminEligible && adminHits.length > 0) {
210
+ kinds.push(PROTOTYPE_KIND.ADMIN);
211
+ evidence.push(...adminHits);
212
+ }
213
+ if (kinds.length === 0) return null;
214
+
215
+ const uniqueEvidence = [...new Set(evidence)].slice(0, 12);
216
+ return {
217
+ kinds,
218
+ ask: composeAsk(kinds),
219
+ nextAction: composeNext(kinds),
220
+ evidence: uniqueEvidence,
221
+ persistenceLayers: persistenceLayers.map((layer) => layer.name),
222
+ domainLayers: domainLayers.map((layer) => layer.name),
223
+ };
224
+ }
@@ -220,6 +220,10 @@ export function deterministicNextAction(violation) {
220
220
  return typeof violation.file === 'string' && violation.file.length > 0
221
221
  ? `Add 1–2 short phrases to invariants[] in ${violation.file}, then run ark-check --doctor. Starters show the shape.`
222
222
  : 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json, then run ark-check --doctor. Starters show the shape.';
223
+ case 'INVARIANT_TESTS_PATH_MISSING':
224
+ return 'Add coverage.testGlobs or coverage.coverageRoots in ark.config.json pointing at a real tests folder, then re-run. Adopted mode fails closed until that path is present.';
225
+ case 'INVARIANT_COVERAGE_ROOTS_MISSING':
226
+ return 'Add coverage.coverageRoots in ark.config.json pointing at the folder the test runner uses, then re-run. An enforced invariant fails closed until that path is present.';
223
227
  case 'ARKRULE_STRUCTURE':
224
228
  case 'ARKRULE_INVARIANT':
225
229
  case 'INVARIANT_UNCOVERED':
@@ -364,6 +368,7 @@ export function classifyRemediation(violation) {
364
368
  ruleId === 'ARKRULE_INVARIANT' ||
365
369
  ruleId === 'INVARIANT_UNCOVERED' ||
366
370
  ruleId === 'INVARIANT_COVERAGE_OUTSIDE_ROOTS' ||
371
+ ruleId === 'INVARIANT_COVERAGE_ROOTS_MISSING' ||
367
372
  (typeof ruleId === 'string' && ruleId.startsWith('ARKRULE_'))) {
368
373
  return {
369
374
  class: 'judgment',
@@ -494,6 +499,12 @@ export function enrichViolationWithFixClass(violation) {
494
499
  enriched.enthusiastHint =
495
500
  'The covering test lives where the project says its runner does not go. Move it, or declare that root in coverage.coverageRoots.';
496
501
  break;
502
+ case 'INVARIANT_COVERAGE_ROOTS_MISSING':
503
+ enriched.fixClass = 'review-contract';
504
+ enriched.effort = 'small';
505
+ enriched.enthusiastHint =
506
+ 'An invariant is enforced, but the project never said where tests run. Add coverage.coverageRoots so coverage cannot tick a test no runner uses.';
507
+ break;
497
508
  case 'ARKRULE_STRUCTURE':
498
509
  case 'ARKRULE_INVARIANT':
499
510
  case 'INVARIANT_UNCOVERED':
@@ -3,7 +3,12 @@ import { spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { arkCommand, buildArchitectureRecommendation, START_APPLY_REFUSE_FOOTER } from '../ark-shared.mjs';
6
+ import {
7
+ arkCommand,
8
+ arkPackageRecoveryCommand,
9
+ buildArchitectureRecommendation,
10
+ START_APPLY_REFUSE_FOOTER,
11
+ } from '../ark-shared.mjs';
7
12
  import { withProjectedGovernedCoverage } from './projected-governed-coverage.mjs';
8
13
  import { ARKORDER_FIRST_CONTACT_NEXT, ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
9
14
  import { compactAgentInstructions, instructionRule, mcpJson } from './ci-and-commands.mjs';
@@ -135,6 +140,20 @@ function commands(root, args, helpers) {
135
140
  return result;
136
141
  }
137
142
 
143
+ /**
144
+ * Red next-step when post-apply package install fails.
145
+ * Host files may already be written; local bins may be missing.
146
+ */
147
+ export function formatStartPackageInstallFailure({ exitStatus, installCommand }) {
148
+ const doctor = arkPackageRecoveryCommand('arkgate-check', '--doctor');
149
+ return [
150
+ `Package install failed (exit ${exitStatus}). Setup files are written; the local command is not installed yet.`,
151
+ ` ${installCommand}`,
152
+ ` ${doctor}`,
153
+ '`arkgate-check` is a command in the arkgate package — not its own npm package.',
154
+ ].join('\n');
155
+ }
156
+
138
157
  /**
139
158
  * @param {object} preview
140
159
  * @param {{ applying?: boolean }} [options] when applying=true, do not claim “no files were changed”
@@ -85,7 +85,8 @@ type ArkConfigCoverage = {
85
85
  * tests. ArkGate never runs anything: this is a second declaration to compare
86
86
  * the coverage scan against, so a covering test found outside them is reported
87
87
  * (INVARIANT_COVERAGE_OUTSIDE_ROOTS) instead of silently certifying the
88
- * invariant. Absence means no declaration and no such claim.
88
+ * invariant. Absence is silent unless any catalogued invariant is enforced —
89
+ * then missing roots fail closed (INVARIANT_COVERAGE_ROOTS_MISSING).
89
90
  */
90
91
  coverageRoots?: string[];
91
92
  };
@@ -1,5 +1,5 @@
1
- import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-CN9tVMPz.js';
2
- import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-Dt3DpVbd.js';
1
+ import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-D5GT5ZT8.js';
2
+ import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-VD0qcubY.js';
3
3
 
4
4
  /** Versioned public result contract types + JSON Schema (adapter envelope). */
5
5
  /**
@@ -392,7 +392,7 @@ declare function createAdapterResult(input: {
392
392
  }): CurrentAdapterResult;
393
393
 
394
394
  /** ArkGate library version — single source of truth. */
395
- declare const version = "4.8.15";
395
+ declare const version = "4.8.17";
396
396
 
397
397
  /**
398
398
  * AI Code Gate (basic).
@@ -1746,6 +1746,12 @@ type InvariantCoverageEvidence = {
1746
1746
  * there is nothing to compare against, and silence is honest.
1747
1747
  */
1748
1748
  outsideDeclaredRoots?: boolean;
1749
+ /**
1750
+ * True when `coverage.coverageRoots` was non-empty for this evaluation.
1751
+ * `false` when the project declared nothing. Omitted on hand-built evidence
1752
+ * so existing promote fixtures stay valid; the evaluator always sets it.
1753
+ */
1754
+ coverageRootsDeclared?: boolean;
1749
1755
  };
1750
1756
  type InvariantUncoveredKind = 'never-had-tests' | 'tests-disappeared';
1751
1757
  type InvariantCoverageRuleId = 'INVARIANT_UNCOVERED' | 'INVARIANT_COVERAGE_OUTSIDE_ROOTS';
@@ -1965,6 +1971,12 @@ type PolicyDeltaAcknowledgement = {
1965
1971
  candidatePolicyHash: string;
1966
1972
  findingIds: readonly string[];
1967
1973
  reason: string;
1974
+ /**
1975
+ * Relative decision-note path under a conventional home (`docs/adr/`,
1976
+ * `docs/decisions/`, or the AP01 single-file homes). Tooling checks the file.
1977
+ * Hash matching does not require it.
1978
+ */
1979
+ adrPath?: string;
1968
1980
  };
1969
1981
  type ClassifyArkPolicyDeltaOptions = {
1970
1982
  baseArkRules?: EffectiveArkRules;
@@ -2015,6 +2027,21 @@ type AnalysisResult = {
2015
2027
  type AnalyzeResolvedProjectInput = {
2016
2028
  contract: AnalysisContract;
2017
2029
  facts: unknown;
2030
+ /**
2031
+ * D0 adopted or --require-gates / --strict-merge. When true, a missing
2032
+ * domain-invariant tests path fails closed. Omitted stays silent.
2033
+ */
2034
+ adopted?: boolean;
2035
+ /**
2036
+ * Tooling FS check for the declared tests path. `false` means empty on disk.
2037
+ * Omitted: a non-empty config declaration is enough.
2038
+ */
2039
+ invariantTestsPathPresent?: boolean;
2040
+ /**
2041
+ * Tooling FS check for declared `coverage.coverageRoots`. `false` means
2042
+ * missing or empty on disk. Omitted: a non-empty config declaration is enough.
2043
+ */
2044
+ coverageRootsPresent?: boolean;
2018
2045
  /**
2019
2046
  * AR10 — Tooling-supplied contents for invariant coverage evidence.
2020
2047
  * When omitted and the Effective Contract has invariants, coverage is partial