@slope-dev/cli 0.1.0 → 0.2.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.
@@ -3,7 +3,8 @@ import { join } from 'node:path';
3
3
  import { formatBriefing } from '@slope-dev/core';
4
4
  import { loadConfig } from '../config.js';
5
5
  import { loadScorecards } from '../loader.js';
6
- export function briefingCommand(args) {
6
+ import { createRegistry } from '../registries/index.js';
7
+ export async function briefingCommand(args) {
7
8
  const config = loadConfig();
8
9
  const cwd = process.cwd();
9
10
  const scorecards = loadScorecards(config, cwd);
@@ -29,6 +30,7 @@ export function briefingCommand(args) {
29
30
  const categories = [];
30
31
  const keywords = [];
31
32
  let includeTraining = true;
33
+ let sprintFlag;
32
34
  for (const arg of args) {
33
35
  if (arg.startsWith('--categories=')) {
34
36
  categories.push(...arg.slice('--categories='.length).split(',').map(s => s.trim()).filter(Boolean));
@@ -36,14 +38,39 @@ export function briefingCommand(args) {
36
38
  else if (arg.startsWith('--keywords=')) {
37
39
  keywords.push(...arg.slice('--keywords='.length).split(',').map(s => s.trim()).filter(Boolean));
38
40
  }
41
+ else if (arg.startsWith('--sprint=')) {
42
+ sprintFlag = parseInt(arg.slice('--sprint='.length), 10);
43
+ }
39
44
  else if (arg === '--no-training') {
40
45
  includeTraining = false;
41
46
  }
42
47
  }
48
+ // Resolve sprint number
49
+ let sprintNumber;
50
+ if (sprintFlag) {
51
+ sprintNumber = sprintFlag;
52
+ }
53
+ else if (config.currentSprint) {
54
+ sprintNumber = config.currentSprint;
55
+ }
56
+ else if (scorecards.length > 0) {
57
+ const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
58
+ sprintNumber = maxSprint + 1;
59
+ }
60
+ else {
61
+ sprintNumber = 1;
62
+ }
63
+ // Load claims
64
+ let claims = [];
65
+ try {
66
+ const registry = createRegistry(config, cwd);
67
+ claims = await registry.list(sprintNumber);
68
+ }
69
+ catch { /* skip — claims are optional */ }
43
70
  const filter = (categories.length > 0 || keywords.length > 0)
44
71
  ? { categories: categories.length > 0 ? categories : undefined, keywords: keywords.length > 0 ? keywords : undefined }
45
72
  : undefined;
46
- const output = formatBriefing({ scorecards, commonIssues, lastSession, filter, includeTraining });
73
+ const output = formatBriefing({ scorecards, commonIssues, lastSession, filter, includeTraining, claims });
47
74
  console.log('');
48
75
  console.log(output);
49
76
  }
@@ -0,0 +1,99 @@
1
+ import { checkConflicts } from '@slope-dev/core';
2
+ import { loadConfig } from '../config.js';
3
+ import { loadScorecards } from '../loader.js';
4
+ import { createRegistry } from '../registries/index.js';
5
+ function parseArgs(args) {
6
+ const result = {};
7
+ for (const arg of args) {
8
+ const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
9
+ if (match)
10
+ result[match[1]] = match[2];
11
+ }
12
+ return result;
13
+ }
14
+ function resolveSprint(flags, cwd) {
15
+ if (flags.sprint)
16
+ return parseInt(flags.sprint, 10);
17
+ const config = loadConfig(cwd);
18
+ if (config.currentSprint)
19
+ return config.currentSprint;
20
+ const scorecards = loadScorecards(config, cwd);
21
+ if (scorecards.length === 0)
22
+ return 1;
23
+ const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
24
+ return maxSprint + 1;
25
+ }
26
+ export async function claimCommand(args) {
27
+ const flags = parseArgs(args);
28
+ const force = args.includes('--force');
29
+ const cwd = process.cwd();
30
+ const config = loadConfig(cwd);
31
+ const registry = createRegistry(config, cwd);
32
+ const target = flags.target;
33
+ if (!target) {
34
+ console.error('Error: --target is required');
35
+ process.exit(1);
36
+ }
37
+ const scope = flags.scope || 'ticket';
38
+ const player = flags.player || process.env.USER || 'unknown';
39
+ const sprintNumber = resolveSprint(flags, cwd);
40
+ // Preflight conflict check: build a temporary claim and test against existing claims
41
+ const existingClaims = await registry.list(sprintNumber);
42
+ const tempClaim = {
43
+ id: '__pending__',
44
+ sprint_number: sprintNumber,
45
+ player,
46
+ target,
47
+ scope,
48
+ claimed_at: new Date().toISOString(),
49
+ ...(flags.notes ? { notes: flags.notes } : {}),
50
+ };
51
+ const conflicts = checkConflicts([...existingClaims, tempClaim]);
52
+ const overlaps = conflicts.filter(c => c.severity === 'overlap');
53
+ const adjacents = conflicts.filter(c => c.severity === 'adjacent');
54
+ // Block on overlaps unless --force
55
+ if (overlaps.length > 0 && !force) {
56
+ console.error(`\nClaim blocked — overlap conflict(s) detected:`);
57
+ for (const c of overlaps) {
58
+ console.error(` [!!] ${c.reason}`);
59
+ }
60
+ console.error(`\nUse --force to override.`);
61
+ process.exit(1);
62
+ }
63
+ // Register the claim
64
+ const claim = await registry.claim({
65
+ sprint_number: sprintNumber,
66
+ player,
67
+ target,
68
+ scope,
69
+ ...(flags.notes ? { notes: flags.notes } : {}),
70
+ });
71
+ // Forced overlap warning
72
+ if (overlaps.length > 0 && force) {
73
+ console.log(`\nClaim registered (forced override):`);
74
+ console.log(` Warning: ${overlaps.length} overlap conflict(s) overridden:`);
75
+ for (const c of overlaps) {
76
+ console.log(` [!!] ${c.reason}`);
77
+ }
78
+ }
79
+ else {
80
+ console.log(`\nClaim registered:`);
81
+ }
82
+ console.log(` ID: ${claim.id}`);
83
+ console.log(` Sprint: ${claim.sprint_number}`);
84
+ console.log(` Player: ${claim.player}`);
85
+ console.log(` Target: ${claim.target} (${claim.scope})`);
86
+ if (claim.notes)
87
+ console.log(` Notes: ${claim.notes}`);
88
+ // Adjacent conflicts are informational only
89
+ if (adjacents.length > 0) {
90
+ console.log(`\n Note: ${adjacents.length} adjacent conflict(s):`);
91
+ for (const c of adjacents) {
92
+ console.log(` [~] ${c.reason}`);
93
+ }
94
+ }
95
+ if (overlaps.length === 0 && adjacents.length === 0) {
96
+ console.log(`\n No conflicts detected.`);
97
+ }
98
+ console.log('');
99
+ }
@@ -94,6 +94,12 @@ export function initCommand(args) {
94
94
  writeFileSync(sessionsPath, JSON.stringify({ sessions: [] }, null, 2) + '\n');
95
95
  console.log(` Created ${sessionsPath}`);
96
96
  }
97
+ // Write empty claims.json
98
+ const claimsPath = join(cwd, '.slope', 'claims.json');
99
+ if (!existsSync(claimsPath)) {
100
+ writeFileSync(claimsPath, JSON.stringify({ claims: [] }, null, 2) + '\n');
101
+ console.log(` Created ${claimsPath}`);
102
+ }
97
103
  // Claude Code templates
98
104
  if (claudeCode) {
99
105
  const templatesRoot = join(__dirname, '..', '..', '..', '..', 'templates', 'claude-code');
@@ -0,0 +1,64 @@
1
+ import { loadConfig } from '../config.js';
2
+ import { loadScorecards } from '../loader.js';
3
+ import { createRegistry } from '../registries/index.js';
4
+ function parseArgs(args) {
5
+ const result = {};
6
+ for (const arg of args) {
7
+ const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
8
+ if (match)
9
+ result[match[1]] = match[2];
10
+ }
11
+ return result;
12
+ }
13
+ function resolveSprintRange(flags, cwd) {
14
+ const config = loadConfig(cwd);
15
+ if (flags.sprint)
16
+ return [parseInt(flags.sprint, 10)];
17
+ if (config.currentSprint)
18
+ return [config.currentSprint];
19
+ const scorecards = loadScorecards(config, cwd);
20
+ if (scorecards.length === 0)
21
+ return [1];
22
+ const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
23
+ // Check the current and next sprint (most likely locations)
24
+ return Array.from({ length: maxSprint + 1 }, (_, i) => i + 1);
25
+ }
26
+ export async function releaseCommand(args) {
27
+ const flags = parseArgs(args);
28
+ const cwd = process.cwd();
29
+ const config = loadConfig(cwd);
30
+ const registry = createRegistry(config, cwd);
31
+ // Release by ID
32
+ if (flags.id) {
33
+ const released = await registry.release(flags.id);
34
+ if (released) {
35
+ console.log(`\nClaim ${flags.id} released.\n`);
36
+ }
37
+ else {
38
+ console.error(`\nClaim ${flags.id} not found.\n`);
39
+ process.exit(1);
40
+ }
41
+ return;
42
+ }
43
+ // Release by target + player lookup
44
+ if (flags.target) {
45
+ const player = flags.player || process.env.USER || 'unknown';
46
+ const sprints = resolveSprintRange(flags, cwd);
47
+ for (const sprint of sprints) {
48
+ const claims = await registry.list(sprint);
49
+ const match = claims.find(c => c.target === flags.target && c.player === player);
50
+ if (match) {
51
+ const released = await registry.release(match.id);
52
+ if (released) {
53
+ console.log(`\nClaim ${match.id} (${match.target} by ${match.player}, sprint ${match.sprint_number}) released.\n`);
54
+ return;
55
+ }
56
+ }
57
+ }
58
+ console.error(`\nNo claim found for target "${flags.target}" by player "${player}".\n`);
59
+ process.exit(1);
60
+ return;
61
+ }
62
+ console.error('Error: --id or --target is required');
63
+ process.exit(1);
64
+ }
@@ -0,0 +1,64 @@
1
+ import { checkConflicts } from '@slope-dev/core';
2
+ import { loadConfig } from '../config.js';
3
+ import { loadScorecards } from '../loader.js';
4
+ import { createRegistry } from '../registries/index.js';
5
+ function parseArgs(args) {
6
+ const result = {};
7
+ for (const arg of args) {
8
+ const match = arg.match(/^--(\w[\w-]*)=(.+)$/);
9
+ if (match)
10
+ result[match[1]] = match[2];
11
+ }
12
+ return result;
13
+ }
14
+ function resolveSprint(flags, cwd) {
15
+ if (flags.sprint)
16
+ return parseInt(flags.sprint, 10);
17
+ const config = loadConfig(cwd);
18
+ if (config.currentSprint)
19
+ return config.currentSprint;
20
+ const scorecards = loadScorecards(config, cwd);
21
+ if (scorecards.length === 0)
22
+ return 1;
23
+ const maxSprint = Math.max(...scorecards.map(s => s.sprint_number));
24
+ return maxSprint + 1;
25
+ }
26
+ export async function statusCommand(args) {
27
+ const flags = parseArgs(args);
28
+ const cwd = process.cwd();
29
+ const config = loadConfig(cwd);
30
+ const registry = createRegistry(config, cwd);
31
+ const sprintNumber = resolveSprint(flags, cwd);
32
+ const claims = await registry.list(sprintNumber);
33
+ console.log(`\nSprint ${sprintNumber} — Course Status`);
34
+ console.log('═'.repeat(40));
35
+ if (claims.length === 0) {
36
+ console.log('\n No claims registered.\n');
37
+ return;
38
+ }
39
+ // Group by player
40
+ const byPlayer = new Map();
41
+ for (const claim of claims) {
42
+ const list = byPlayer.get(claim.player) || [];
43
+ list.push(claim);
44
+ byPlayer.set(claim.player, list);
45
+ }
46
+ for (const [player, playerClaims] of byPlayer) {
47
+ console.log(`\n ${player}:`);
48
+ for (const c of playerClaims) {
49
+ const scopeTag = c.scope === 'area' ? '[area]' : '[ticket]';
50
+ const notes = c.notes ? ` — ${c.notes}` : '';
51
+ console.log(` ${scopeTag} ${c.target}${notes} (${c.id})`);
52
+ }
53
+ }
54
+ // Check conflicts
55
+ const conflicts = checkConflicts(claims);
56
+ if (conflicts.length > 0) {
57
+ console.log(`\n Conflicts (${conflicts.length}):`);
58
+ for (const c of conflicts) {
59
+ const icon = c.severity === 'overlap' ? '!!' : '~';
60
+ console.log(` [${icon}] ${c.reason} (${c.severity})`);
61
+ }
62
+ }
63
+ console.log('');
64
+ }
package/dist/config.js CHANGED
@@ -6,6 +6,8 @@ const DEFAULT_CONFIG = {
6
6
  minSprint: 1,
7
7
  commonIssuesPath: '.slope/common-issues.json',
8
8
  sessionsPath: '.slope/sessions.json',
9
+ registry: 'file',
10
+ claimsPath: '.slope/claims.json',
9
11
  };
10
12
  const CONFIG_DIR = '.slope';
11
13
  const CONFIG_FILE = 'config.json';
package/dist/index.js CHANGED
@@ -7,9 +7,12 @@
7
7
  * slope card Display handicap card
8
8
  * slope validate [path] Validate scorecard(s)
9
9
  * slope review [path] [--plain] Format sprint review
10
- * slope briefing [options] Pre-round briefing
10
+ * slope briefing [--sprint=N] [options] Pre-round briefing
11
11
  * slope plan --complexity=<level> Pre-shot advisor
12
12
  * slope classify --scope=... ... Classify a shot
13
+ * slope claim --target=<t> [--force] Claim a ticket or area
14
+ * slope release --id=<id> Release a claim
15
+ * slope status [--sprint=N] Show sprint course status
13
16
  */
14
17
  import { initCommand } from './commands/init.js';
15
18
  import { cardCommand } from './commands/card.js';
@@ -18,6 +21,9 @@ import { reviewCommand } from './commands/review.js';
18
21
  import { briefingCommand } from './commands/briefing.js';
19
22
  import { planCommand } from './commands/plan.js';
20
23
  import { classifyCommand } from './commands/classify.js';
24
+ import { claimCommand } from './commands/claim.js';
25
+ import { releaseCommand } from './commands/release.js';
26
+ import { statusCommand } from './commands/status.js';
21
27
  const subcommand = process.argv[2];
22
28
  switch (subcommand) {
23
29
  case 'init':
@@ -37,7 +43,10 @@ switch (subcommand) {
37
43
  break;
38
44
  }
39
45
  case 'briefing':
40
- briefingCommand(process.argv.slice(3));
46
+ briefingCommand(process.argv.slice(3)).catch(err => {
47
+ console.error('Error:', err.message);
48
+ process.exit(1);
49
+ });
41
50
  break;
42
51
  case 'plan':
43
52
  planCommand(process.argv.slice(3));
@@ -45,6 +54,24 @@ switch (subcommand) {
45
54
  case 'classify':
46
55
  classifyCommand(process.argv.slice(3));
47
56
  break;
57
+ case 'claim':
58
+ claimCommand(process.argv.slice(3)).catch(err => {
59
+ console.error('Error:', err.message);
60
+ process.exit(1);
61
+ });
62
+ break;
63
+ case 'release':
64
+ releaseCommand(process.argv.slice(3)).catch(err => {
65
+ console.error('Error:', err.message);
66
+ process.exit(1);
67
+ });
68
+ break;
69
+ case 'status':
70
+ statusCommand(process.argv.slice(3)).catch(err => {
71
+ console.error('Error:', err.message);
72
+ process.exit(1);
73
+ });
74
+ break;
48
75
  default:
49
76
  console.log(`
50
77
  SLOPE CLI — Sprint Lifecycle & Operational Performance Engine
@@ -54,9 +81,13 @@ Usage:
54
81
  slope card Show handicap card
55
82
  slope validate [path] Validate scorecard(s)
56
83
  slope review [path] [--plain] Format sprint review markdown
57
- slope briefing [options] Pre-round briefing
84
+ slope briefing [--sprint=N] [options] Pre-round briefing
58
85
  slope plan --complexity=<level> Pre-shot advisor (club + training + hazards)
59
86
  slope classify --scope=... ... Classify a shot from execution trace
87
+ slope claim --target=<t> [--force] Claim a ticket or area for the sprint
88
+ slope release --id=<id> Release a claim by ID
89
+ slope release --target=<t> [--player=<p>] Release a claim by target
90
+ slope status [--sprint=N] Show sprint course status + conflicts
60
91
 
61
92
  Examples:
62
93
  slope init Create .slope/ with config + example scorecard
@@ -72,6 +103,12 @@ Examples:
72
103
  slope plan --complexity=medium Club recommendation for medium ticket
73
104
  slope plan --complexity=large --areas=db Include hazard warnings for db area
74
105
  slope classify --scope="a.ts" --modified="a.ts" --tests=pass --reverts=0
106
+ slope briefing --sprint=2 Briefing for sprint 2
107
+ slope claim --target=S2-1 --sprint=2 Claim ticket S2-1 for sprint 2
108
+ slope claim --target=packages/cli --scope=area Claim an area
109
+ slope claim --target=S2-1 --force Claim even if overlap conflict exists
110
+ slope status --sprint=2 Show all claims for sprint 2
111
+ slope release --target=S2-1 Release your claim on S2-1
75
112
  `);
76
113
  process.exit(subcommand ? 1 : 0);
77
114
  }
@@ -0,0 +1,40 @@
1
+ export class ApiRegistry {
2
+ baseUrl;
3
+ constructor(baseUrl) {
4
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
5
+ }
6
+ async claim(input) {
7
+ const res = await fetch(`${this.baseUrl}/claims`, {
8
+ method: 'POST',
9
+ headers: { 'Content-Type': 'application/json' },
10
+ body: JSON.stringify(input),
11
+ });
12
+ if (!res.ok)
13
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
14
+ return res.json();
15
+ }
16
+ async release(id) {
17
+ const res = await fetch(`${this.baseUrl}/claims/${encodeURIComponent(id)}`, {
18
+ method: 'DELETE',
19
+ });
20
+ if (res.status === 404)
21
+ return false;
22
+ if (!res.ok)
23
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
24
+ return true;
25
+ }
26
+ async list(sprintNumber) {
27
+ const res = await fetch(`${this.baseUrl}/claims?sprint=${sprintNumber}`);
28
+ if (!res.ok)
29
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
30
+ return res.json();
31
+ }
32
+ async get(id) {
33
+ const res = await fetch(`${this.baseUrl}/claims/${encodeURIComponent(id)}`);
34
+ if (res.status === 404)
35
+ return undefined;
36
+ if (!res.ok)
37
+ throw new Error(`API error: ${res.status} ${res.statusText}`);
38
+ return res.json();
39
+ }
40
+ }
@@ -0,0 +1,53 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export class FileRegistry {
4
+ filePath;
5
+ constructor(filePath) {
6
+ this.filePath = filePath;
7
+ }
8
+ async claim(input) {
9
+ const claims = this.readClaims();
10
+ const claim = {
11
+ ...input,
12
+ id: `claim-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
13
+ claimed_at: new Date().toISOString(),
14
+ };
15
+ claims.push(claim);
16
+ this.writeClaims(claims);
17
+ return claim;
18
+ }
19
+ async release(id) {
20
+ const claims = this.readClaims();
21
+ const idx = claims.findIndex(c => c.id === id);
22
+ if (idx === -1)
23
+ return false;
24
+ claims.splice(idx, 1);
25
+ this.writeClaims(claims);
26
+ return true;
27
+ }
28
+ async list(sprintNumber) {
29
+ return this.readClaims().filter(c => c.sprint_number === sprintNumber);
30
+ }
31
+ async get(id) {
32
+ return this.readClaims().find(c => c.id === id);
33
+ }
34
+ readClaims() {
35
+ if (!existsSync(this.filePath))
36
+ return [];
37
+ try {
38
+ const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
39
+ return Array.isArray(data.claims) ? data.claims : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ writeClaims(claims) {
46
+ const dir = dirname(this.filePath);
47
+ if (!existsSync(dir)) {
48
+ mkdirSync(dir, { recursive: true });
49
+ }
50
+ const data = { claims };
51
+ writeFileSync(this.filePath, JSON.stringify(data, null, 2) + '\n');
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import { join } from 'node:path';
2
+ import { FileRegistry } from './file-registry.js';
3
+ import { ApiRegistry } from './api-registry.js';
4
+ export function createRegistry(config, cwd = process.cwd()) {
5
+ switch (config.registry) {
6
+ case 'api': {
7
+ if (!config.registryApiUrl) {
8
+ throw new Error('registryApiUrl is required when registry is set to "api"');
9
+ }
10
+ return new ApiRegistry(config.registryApiUrl);
11
+ }
12
+ case 'file':
13
+ default:
14
+ return new FileRegistry(join(cwd, config.claimsPath));
15
+ }
16
+ }
17
+ export { FileRegistry } from './file-registry.js';
18
+ export { ApiRegistry } from './api-registry.js';
package/package.json CHANGED
@@ -1,17 +1,23 @@
1
1
  {
2
2
  "name": "@slope-dev/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SLOPE CLI — Sprint Lifecycle & Operational Performance Engine",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "slope": "./dist/index.js"
8
8
  },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "typecheck": "tsc --noEmit"
13
+ },
9
14
  "dependencies": {
10
- "@slope-dev/core": "0.1.0"
15
+ "@slope-dev/core": "workspace:*"
11
16
  },
12
17
  "devDependencies": {
13
18
  "@types/node": "^25.3.0",
14
- "typescript": "^5.7.0"
19
+ "typescript": "^5.7.0",
20
+ "vitest": "^3.0.0"
15
21
  },
16
22
  "files": [
17
23
  "dist"
@@ -34,8 +40,5 @@
34
40
  ],
35
41
  "engines": {
36
42
  "node": ">=18"
37
- },
38
- "scripts": {
39
- "build": "tsc"
40
43
  }
41
- }
44
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Sam Bryers
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.