atris 2.5.3 → 2.5.4
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.
- package/README.md +10 -0
- package/atris/experiments/README.md +118 -0
- package/atris/experiments/_examples/smoke-keep-revert/README.md +45 -0
- package/atris/experiments/_examples/smoke-keep-revert/candidate.py +8 -0
- package/atris/experiments/_examples/smoke-keep-revert/loop.py +129 -0
- package/atris/experiments/_examples/smoke-keep-revert/measure.py +47 -0
- package/atris/experiments/_examples/smoke-keep-revert/program.md +3 -0
- package/atris/experiments/_examples/smoke-keep-revert/proposals/bad_patch.py +19 -0
- package/atris/experiments/_examples/smoke-keep-revert/proposals/fix_patch.py +22 -0
- package/atris/experiments/_examples/smoke-keep-revert/reset.py +21 -0
- package/atris/experiments/_examples/smoke-keep-revert/results.tsv +5 -0
- package/atris/experiments/_examples/smoke-keep-revert/visual.svg +52 -0
- package/atris/experiments/_fixtures/invalid/BadName/loop.py +1 -0
- package/atris/experiments/_fixtures/invalid/BadName/program.md +3 -0
- package/atris/experiments/_fixtures/invalid/BadName/results.tsv +1 -0
- package/atris/experiments/_fixtures/invalid/bloated-context/loop.py +1 -0
- package/atris/experiments/_fixtures/invalid/bloated-context/measure.py +1 -0
- package/atris/experiments/_fixtures/invalid/bloated-context/program.md +6 -0
- package/atris/experiments/_fixtures/invalid/bloated-context/results.tsv +1 -0
- package/atris/experiments/_fixtures/valid/good-experiment/loop.py +1 -0
- package/atris/experiments/_fixtures/valid/good-experiment/measure.py +1 -0
- package/atris/experiments/_fixtures/valid/good-experiment/program.md +3 -0
- package/atris/experiments/_fixtures/valid/good-experiment/results.tsv +1 -0
- package/atris/experiments/_template/pack/loop.py +3 -0
- package/atris/experiments/_template/pack/measure.py +13 -0
- package/atris/experiments/_template/pack/program.md +3 -0
- package/atris/experiments/_template/pack/reset.py +3 -0
- package/atris/experiments/_template/pack/results.tsv +1 -0
- package/atris/experiments/benchmark_runtime.py +81 -0
- package/atris/experiments/benchmark_validate.py +70 -0
- package/atris/experiments/validate.py +92 -0
- package/atris/policies/atris-design.md +66 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/apps/SKILL.md +243 -0
- package/atris/skills/autoresearch/SKILL.md +63 -0
- package/atris/skills/create-app/SKILL.md +9 -13
- package/atris/skills/design/SKILL.md +15 -1
- package/bin/atris.js +76 -5
- package/commands/business.js +132 -0
- package/commands/clean.js +113 -70
- package/commands/console.js +397 -0
- package/commands/experiments.js +216 -0
- package/commands/init.js +4 -0
- package/commands/pull.js +311 -0
- package/commands/push.js +170 -0
- package/commands/run.js +366 -0
- package/commands/status.js +21 -1
- package/package.json +2 -1
package/bin/atris.js
CHANGED
|
@@ -164,6 +164,14 @@ function searchJournal(keyword) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
function consoleCmd() {
|
|
167
|
+
const extractedCommand = path.join(__dirname, '..', 'commands', 'console.js');
|
|
168
|
+
if (fs.existsSync(extractedCommand)) {
|
|
169
|
+
const loaded = require('../commands/console');
|
|
170
|
+
if (loaded && typeof loaded.consoleCommand === 'function') {
|
|
171
|
+
return loaded.consoleCommand();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
167
175
|
const workspace = process.cwd();
|
|
168
176
|
const daemonScript = path.join(workspace, 'cli', 'atrisd.sh');
|
|
169
177
|
|
|
@@ -211,11 +219,12 @@ function showHelp() {
|
|
|
211
219
|
console.log(' plan - Create build spec with visualization');
|
|
212
220
|
console.log(' do - Execute tasks');
|
|
213
221
|
console.log(' review - Validate work (tests, safety checks, docs)');
|
|
222
|
+
console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
|
|
214
223
|
console.log('');
|
|
215
224
|
console.log('Context & tracking:');
|
|
216
225
|
console.log(' log - Add ideas to inbox');
|
|
217
226
|
console.log(' activate - Load Atris context');
|
|
218
|
-
console.log(' status - See active work and completions');
|
|
227
|
+
console.log(' status - See active work and completions (--json for machine output)');
|
|
219
228
|
console.log(' analytics - Show recent productivity from journals');
|
|
220
229
|
console.log(' search - Search journal history (atris search <keyword>)');
|
|
221
230
|
console.log(' clean - Housekeeping (stale tasks, archive journals, broken refs)');
|
|
@@ -226,10 +235,18 @@ function showHelp() {
|
|
|
226
235
|
console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
|
|
227
236
|
console.log(' visualize - Legacy visualization helper (prefer "atris plan")');
|
|
228
237
|
console.log('');
|
|
238
|
+
console.log('Experiments:');
|
|
239
|
+
console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
|
|
240
|
+
console.log(' experiments validate - Validate experiment packs');
|
|
241
|
+
console.log(' experiments benchmark [m] - Run validate/runtime experiment benchmarks');
|
|
242
|
+
console.log('');
|
|
229
243
|
console.log('Quick commands:');
|
|
230
244
|
console.log(' atris - Load context and start (natural language)');
|
|
231
245
|
console.log(' next - Auto-advance to next step');
|
|
232
246
|
console.log('');
|
|
247
|
+
console.log('Sync:');
|
|
248
|
+
console.log(' pull - Pull journals + member data from cloud');
|
|
249
|
+
console.log('');
|
|
233
250
|
console.log('Cloud & agents:');
|
|
234
251
|
console.log(' console - Start/attach always-on coding console (tmux daemon)');
|
|
235
252
|
console.log(' agent - Select which Atris agent to use');
|
|
@@ -366,9 +383,9 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
|
|
|
366
383
|
// All other commands are lazy-loaded inline (require() only when invoked)
|
|
367
384
|
|
|
368
385
|
// Check if this is a known command or natural language input
|
|
369
|
-
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'plan', 'do', 'review',
|
|
386
|
+
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review',
|
|
370
387
|
'activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'accounts', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
371
|
-
'clean', 'verify', 'search', 'skill', 'member', 'plugin', 'sync',
|
|
388
|
+
'clean', 'verify', 'search', 'skill', 'member', 'plugin', 'experiments', 'pull', 'sync',
|
|
372
389
|
'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
|
|
373
390
|
|
|
374
391
|
// Check if command is an atris.md spec file - triggers welcome visualization
|
|
@@ -715,6 +732,42 @@ if (command === 'init') {
|
|
|
715
732
|
console.log(' Prefer: atris plan');
|
|
716
733
|
console.log('');
|
|
717
734
|
require('../commands/visualize').visualizeAtris();
|
|
735
|
+
} else if (command === 'run') {
|
|
736
|
+
const args = process.argv.slice(3);
|
|
737
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
738
|
+
console.log('');
|
|
739
|
+
console.log('Usage: atris run [options]');
|
|
740
|
+
console.log('');
|
|
741
|
+
console.log('Auto-chain plan → do → review cycles autonomously.');
|
|
742
|
+
console.log('Reads inbox ideas, creates tasks, builds them, validates, repeats.');
|
|
743
|
+
console.log('');
|
|
744
|
+
console.log('Options:');
|
|
745
|
+
console.log(' --cycles=N Max cycles (default: 5)');
|
|
746
|
+
console.log(' --once Single plan→do→review cycle');
|
|
747
|
+
console.log(' --verbose Show claude -p output');
|
|
748
|
+
console.log(' --dry-run Preview without executing');
|
|
749
|
+
console.log(' --timeout=N Phase timeout in seconds (default: 600)');
|
|
750
|
+
console.log(' --push Auto-push after each cycle (default: true)');
|
|
751
|
+
console.log(' --no-push Skip auto-push after each cycle');
|
|
752
|
+
console.log('');
|
|
753
|
+
process.exit(0);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
757
|
+
const dryRun = args.includes('--dry-run');
|
|
758
|
+
const once = args.includes('--once');
|
|
759
|
+
const push = !args.includes('--no-push');
|
|
760
|
+
const cyclesArg = args.find(a => a.startsWith('--cycles='));
|
|
761
|
+
const maxCycles = cyclesArg ? parseInt(cyclesArg.split('=')[1]) : 5;
|
|
762
|
+
const timeoutArg = args.find(a => a.startsWith('--timeout='));
|
|
763
|
+
const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1]) * 1000 : undefined;
|
|
764
|
+
|
|
765
|
+
require('../commands/run').runAtris({ maxCycles, verbose, dryRun, once, push, timeout })
|
|
766
|
+
.then(() => process.exit(0))
|
|
767
|
+
.catch((error) => {
|
|
768
|
+
console.error(`\u2717 Run failed: ${error.message || error}`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
});
|
|
718
771
|
} else if (command === 'autopilot') {
|
|
719
772
|
const args = process.argv.slice(3);
|
|
720
773
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -813,7 +866,8 @@ if (command === 'init') {
|
|
|
813
866
|
});
|
|
814
867
|
} else if (command === 'status') {
|
|
815
868
|
const isQuick = process.argv.includes('--quick') || process.argv.includes('-q');
|
|
816
|
-
|
|
869
|
+
const isJson = process.argv.includes('--json');
|
|
870
|
+
statusCmd(isQuick, isJson);
|
|
817
871
|
} else if (command === 'analytics') {
|
|
818
872
|
require('../commands/analytics').analyticsAtris();
|
|
819
873
|
} else if (command === 'clean') {
|
|
@@ -866,10 +920,28 @@ if (command === 'init') {
|
|
|
866
920
|
const subcommand = process.argv[3];
|
|
867
921
|
const args = process.argv.slice(4);
|
|
868
922
|
require('../commands/member').memberCommand(subcommand, ...args);
|
|
923
|
+
} else if (command === 'pull') {
|
|
924
|
+
require('../commands/pull').pullAtris()
|
|
925
|
+
.then(() => process.exit(0))
|
|
926
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
927
|
+
} else if (command === 'push') {
|
|
928
|
+
require('../commands/push').pushAtris()
|
|
929
|
+
.then(() => process.exit(0))
|
|
930
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
931
|
+
} else if (command === 'business') {
|
|
932
|
+
const subcommand = process.argv[3];
|
|
933
|
+
const args = process.argv.slice(4);
|
|
934
|
+
require('../commands/business').businessCommand(subcommand, ...args)
|
|
935
|
+
.then(() => process.exit(0))
|
|
936
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
869
937
|
} else if (command === 'plugin') {
|
|
870
938
|
const subcommand = process.argv[3] || 'build';
|
|
871
939
|
const args = process.argv.slice(4);
|
|
872
940
|
require('../commands/plugin').pluginCommand(subcommand, ...args);
|
|
941
|
+
} else if (command === 'experiments') {
|
|
942
|
+
const subcommand = process.argv[3];
|
|
943
|
+
const args = process.argv.slice(4);
|
|
944
|
+
require('../commands/experiments').experimentsCommand(subcommand, ...args);
|
|
873
945
|
} else {
|
|
874
946
|
console.log(`Unknown command: ${command}`);
|
|
875
947
|
console.log('Run "atris help" to see available commands');
|
|
@@ -1292,4 +1364,3 @@ async function atrisDevEntry(userInput = null) {
|
|
|
1292
1364
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1293
1365
|
console.log('');
|
|
1294
1366
|
}
|
|
1295
|
-
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadCredentials } = require('../utils/auth');
|
|
4
|
+
const { apiRequestJson } = require('../utils/api');
|
|
5
|
+
|
|
6
|
+
function getBusinessConfigPath() {
|
|
7
|
+
const home = require('os').homedir();
|
|
8
|
+
const dir = path.join(home, '.atris');
|
|
9
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
return path.join(dir, 'businesses.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadBusinesses() {
|
|
14
|
+
const p = getBusinessConfigPath();
|
|
15
|
+
if (!fs.existsSync(p)) return {};
|
|
16
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function saveBusinesses(data) {
|
|
20
|
+
fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function addBusiness(slug) {
|
|
24
|
+
if (!slug) {
|
|
25
|
+
console.error('Usage: atris business add <slug>');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const creds = loadCredentials();
|
|
30
|
+
if (!creds || !creds.token) {
|
|
31
|
+
console.error('Not logged in. Run: atris login');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve slug to business
|
|
36
|
+
const result = await apiRequestJson(`/businesses/by-slug/${slug}`, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
token: creds.token,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
// Try listing all and matching
|
|
43
|
+
const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
|
|
44
|
+
if (listResult.ok && Array.isArray(listResult.data)) {
|
|
45
|
+
const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
|
|
46
|
+
if (match) {
|
|
47
|
+
const businesses = loadBusinesses();
|
|
48
|
+
businesses[slug] = {
|
|
49
|
+
business_id: match.id,
|
|
50
|
+
workspace_id: match.workspace_id,
|
|
51
|
+
name: match.name,
|
|
52
|
+
slug: match.slug,
|
|
53
|
+
added_at: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
saveBusinesses(businesses);
|
|
56
|
+
console.log(`\nAdded "${match.name}" (${match.slug})`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.error(`Business "${slug}" not found.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const biz = result.data;
|
|
65
|
+
const businesses = loadBusinesses();
|
|
66
|
+
businesses[slug] = {
|
|
67
|
+
business_id: biz.id,
|
|
68
|
+
workspace_id: biz.workspace_id,
|
|
69
|
+
name: biz.name,
|
|
70
|
+
slug: biz.slug,
|
|
71
|
+
added_at: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
saveBusinesses(businesses);
|
|
74
|
+
console.log(`\nAdded "${biz.name}" (${biz.slug})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function listBusinesses() {
|
|
78
|
+
const businesses = loadBusinesses();
|
|
79
|
+
const slugs = Object.keys(businesses);
|
|
80
|
+
|
|
81
|
+
if (slugs.length === 0) {
|
|
82
|
+
console.log('\nNo businesses connected. Run: atris business add <slug>');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('\nConnected businesses:\n');
|
|
87
|
+
for (const slug of slugs) {
|
|
88
|
+
const b = businesses[slug];
|
|
89
|
+
console.log(` ${b.name || slug} (${b.slug || slug})`);
|
|
90
|
+
console.log(` ID: ${b.business_id}`);
|
|
91
|
+
console.log(` Added: ${b.added_at || 'unknown'}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function removeBusiness(slug) {
|
|
97
|
+
if (!slug) {
|
|
98
|
+
console.error('Usage: atris business remove <slug>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const businesses = loadBusinesses();
|
|
103
|
+
if (!businesses[slug]) {
|
|
104
|
+
console.error(`Business "${slug}" not connected.`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const name = businesses[slug].name || slug;
|
|
109
|
+
delete businesses[slug];
|
|
110
|
+
saveBusinesses(businesses);
|
|
111
|
+
console.log(`\nRemoved "${name}"`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function businessCommand(subcommand, ...args) {
|
|
115
|
+
switch (subcommand) {
|
|
116
|
+
case 'add':
|
|
117
|
+
await addBusiness(args[0]);
|
|
118
|
+
break;
|
|
119
|
+
case 'list':
|
|
120
|
+
case 'ls':
|
|
121
|
+
await listBusinesses();
|
|
122
|
+
break;
|
|
123
|
+
case 'remove':
|
|
124
|
+
case 'rm':
|
|
125
|
+
await removeBusiness(args[0]);
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
console.log('Usage: atris business <add|list|remove> [slug]');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { businessCommand, loadBusinesses, saveBusinesses, getBusinessConfigPath };
|
package/commands/clean.js
CHANGED
|
@@ -162,7 +162,8 @@ function findStaleTasks(atrisDir) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
* Find and heal broken MAP.md references
|
|
165
|
+
* Find and heal broken MAP.md references (single-line AND range refs)
|
|
166
|
+
* Detects both out-of-bounds AND drift (symbol moved to different line)
|
|
166
167
|
* Returns { healed: number, unhealable: array }
|
|
167
168
|
*/
|
|
168
169
|
function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
@@ -173,68 +174,85 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
173
174
|
const unhealable = [];
|
|
174
175
|
let healed = 0;
|
|
175
176
|
|
|
176
|
-
// Match
|
|
177
|
-
//
|
|
178
|
-
|
|
177
|
+
// Match both `file.js:123` and `file.js:123-456` with surrounding context
|
|
178
|
+
// [^\S\n] = horizontal whitespace only (no newlines)
|
|
179
|
+
// Required delimiter [(,[,—,-] prevents bleeding into adjacent refs on same line
|
|
180
|
+
const refPattern = /(`?)([a-zA-Z0-9_\-./\\]+\.(js|ts|py|go|rs|rb|java|c|cpp|h|hpp|md|json|yaml|yml)):(\d+)(?:-(\d+))?(`?)([^\S\n]*[\(\[—\-][^\S\n]*([^)\]\n]+))?/g;
|
|
181
|
+
|
|
182
|
+
// Cache file reads
|
|
183
|
+
const fileCache = {};
|
|
184
|
+
function readFileCached(filePath) {
|
|
185
|
+
if (!fileCache[filePath]) {
|
|
186
|
+
const fullPath = path.join(cwd, filePath);
|
|
187
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
188
|
+
try {
|
|
189
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
190
|
+
fileCache[filePath] = { content, lines: content.split('\n') };
|
|
191
|
+
} catch { return null; }
|
|
192
|
+
}
|
|
193
|
+
return fileCache[filePath];
|
|
194
|
+
}
|
|
179
195
|
|
|
180
196
|
const replacements = [];
|
|
181
197
|
let match;
|
|
182
198
|
|
|
183
199
|
while ((match = refPattern.exec(mapContent)) !== null) {
|
|
184
|
-
const fullMatch = match[0];
|
|
185
200
|
const backtickBefore = match[1] || '';
|
|
186
201
|
const filePath = match[2];
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const backtickAfter = match[
|
|
190
|
-
const contextPart = match[
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!fs.existsSync(fullPath)) {
|
|
196
|
-
unhealable.push({ file: filePath, line: lineNum, reason: 'File not found' });
|
|
202
|
+
const startLine = parseInt(match[4], 10);
|
|
203
|
+
const endLine = match[5] ? parseInt(match[5], 10) : null;
|
|
204
|
+
const backtickAfter = match[6] || '';
|
|
205
|
+
const contextPart = match[8] || '';
|
|
206
|
+
|
|
207
|
+
const file = readFileCached(filePath);
|
|
208
|
+
if (!file) {
|
|
209
|
+
unhealable.push({ file: filePath, line: startLine, reason: 'File not found' });
|
|
197
210
|
continue;
|
|
198
211
|
}
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
let fileContent;
|
|
202
|
-
try {
|
|
203
|
-
fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
204
|
-
} catch (err) {
|
|
205
|
-
unhealable.push({ file: filePath, line: lineNum, reason: `Cannot read: ${err.message}` });
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
213
|
+
const symbol = extractSymbol(contextPart);
|
|
208
214
|
|
|
209
|
-
|
|
215
|
+
// Check if reference is still accurate
|
|
216
|
+
const outOfBounds = startLine > file.lines.length || (endLine && endLine > file.lines.length);
|
|
217
|
+
const drifted = symbol && startLine <= file.lines.length && !symbolAtLine(file.content, symbol, startLine);
|
|
210
218
|
|
|
211
|
-
|
|
212
|
-
if (lineNum <= lines.length) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Line number is broken - try to heal
|
|
217
|
-
const symbol = extractSymbol(contextPart);
|
|
219
|
+
if (!outOfBounds && !drifted) continue;
|
|
218
220
|
|
|
219
221
|
if (!symbol) {
|
|
220
|
-
unhealable.push({ file: filePath, line:
|
|
222
|
+
unhealable.push({ file: filePath, line: startLine, reason: 'No symbol to search for' });
|
|
221
223
|
continue;
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
//
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
unhealable.push({ file: filePath, line: lineNum, reason: `Symbol "${symbol}" not found` });
|
|
226
|
+
// Find where the symbol actually is now
|
|
227
|
+
const newStart = findSymbolLine(file.content, symbol);
|
|
228
|
+
if (!newStart) {
|
|
229
|
+
unhealable.push({ file: filePath, line: startLine, reason: `Symbol "${symbol}" not found` });
|
|
229
230
|
continue;
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
if (endLine) {
|
|
234
|
+
// Range ref: find new end by scanning for function end
|
|
235
|
+
const originalSpan = endLine - startLine;
|
|
236
|
+
const newEnd = findFunctionEnd(file.lines, newStart) || (newStart + originalSpan);
|
|
237
|
+
const clampedEnd = Math.min(newEnd, file.lines.length);
|
|
238
|
+
|
|
239
|
+
const oldRef = `${backtickBefore}${filePath}:${startLine}-${endLine}${backtickAfter}`;
|
|
240
|
+
const newRef = `${backtickBefore}${filePath}:${newStart}-${clampedEnd}${backtickAfter}`;
|
|
235
241
|
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
if (oldRef !== newRef) {
|
|
243
|
+
replacements.push({ old: oldRef, new: newRef, symbol });
|
|
244
|
+
healed++;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Single-line ref
|
|
248
|
+
const oldRef = `${backtickBefore}${filePath}:${startLine}${backtickAfter}`;
|
|
249
|
+
const newRef = `${backtickBefore}${filePath}:${newStart}${backtickAfter}`;
|
|
250
|
+
|
|
251
|
+
if (oldRef !== newRef) {
|
|
252
|
+
replacements.push({ old: oldRef, new: newRef, symbol });
|
|
253
|
+
healed++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
238
256
|
}
|
|
239
257
|
|
|
240
258
|
// Apply replacements
|
|
@@ -245,7 +263,45 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
245
263
|
fs.writeFileSync(mapFile, mapContent);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
|
-
return { healed, unhealable };
|
|
266
|
+
return { healed, unhealable, replacements: dryRun ? replacements : [] };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if a symbol is actually defined at or near a given line
|
|
271
|
+
*/
|
|
272
|
+
function symbolAtLine(fileContent, symbol, lineNum) {
|
|
273
|
+
const lines = fileContent.split('\n');
|
|
274
|
+
// Check a 5-line window around the referenced line
|
|
275
|
+
const start = Math.max(0, lineNum - 3);
|
|
276
|
+
const end = Math.min(lines.length, lineNum + 2);
|
|
277
|
+
const escaped = escapeRegExp(symbol);
|
|
278
|
+
const re = new RegExp(`\\b${escaped}\\b`);
|
|
279
|
+
for (let i = start; i < end; i++) {
|
|
280
|
+
if (re.test(lines[i])) return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Find the end line of a function starting at startLine (1-indexed)
|
|
287
|
+
* Tracks brace depth to find the matching closing brace
|
|
288
|
+
*/
|
|
289
|
+
function findFunctionEnd(lines, startLine) {
|
|
290
|
+
let depth = 0;
|
|
291
|
+
let foundOpen = false;
|
|
292
|
+
|
|
293
|
+
for (let i = startLine - 1; i < lines.length; i++) {
|
|
294
|
+
const line = lines[i];
|
|
295
|
+
for (const ch of line) {
|
|
296
|
+
if (ch === '{') { depth++; foundOpen = true; }
|
|
297
|
+
if (ch === '}') { depth--; }
|
|
298
|
+
}
|
|
299
|
+
if (foundOpen && depth === 0) {
|
|
300
|
+
return i + 1; // 1-indexed
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null;
|
|
249
305
|
}
|
|
250
306
|
|
|
251
307
|
/**
|
|
@@ -256,6 +312,7 @@ function extractSymbol(context) {
|
|
|
256
312
|
|
|
257
313
|
// Clean up the context
|
|
258
314
|
const cleaned = context.trim()
|
|
315
|
+
.replace(/`/g, '') // Strip backticks
|
|
259
316
|
.replace(/^[\(\[—\-:]+\s*/, '') // Remove leading punctuation
|
|
260
317
|
.replace(/[\)\]]+$/, '') // Remove trailing brackets
|
|
261
318
|
.trim();
|
|
@@ -280,45 +337,31 @@ function extractSymbol(context) {
|
|
|
280
337
|
}
|
|
281
338
|
|
|
282
339
|
/**
|
|
283
|
-
* Find the line number where a symbol is defined
|
|
340
|
+
* Find the line number where a symbol is defined (strict patterns only)
|
|
341
|
+
* Returns null if only loose matches found — prevents healing to wrong locations
|
|
284
342
|
*/
|
|
285
343
|
function findSymbolLine(fileContent, symbol) {
|
|
286
344
|
const lines = fileContent.split('\n');
|
|
345
|
+
const esc = escapeRegExp(symbol);
|
|
287
346
|
|
|
288
|
-
//
|
|
347
|
+
// Strict definition patterns only — no loose fallback
|
|
289
348
|
const patterns = [
|
|
290
|
-
// function name(
|
|
291
|
-
new RegExp(`^\\s*(
|
|
292
|
-
//
|
|
293
|
-
new RegExp(`^\\s
|
|
294
|
-
//
|
|
295
|
-
new RegExp(`^\\s*
|
|
296
|
-
// name: function or name() {
|
|
297
|
-
new RegExp(`^\\s*${escapeRegExp(symbol)}\\s*[:(]`),
|
|
298
|
-
// exports.name or module.exports.name
|
|
299
|
-
new RegExp(`exports\\.${escapeRegExp(symbol)}\\s*=`),
|
|
300
|
-
// def name( for Python
|
|
301
|
-
new RegExp(`^\\s*def\\s+${escapeRegExp(symbol)}\\s*\\(`),
|
|
302
|
-
// Just the symbol on a line (loose match)
|
|
303
|
-
new RegExp(`\\b${escapeRegExp(symbol)}\\b`)
|
|
349
|
+
new RegExp(`^\\s*(async\\s+)?function\\s+${esc}\\s*\\(`), // function name(
|
|
350
|
+
new RegExp(`^\\s*(const|let|var)\\s+${esc}\\s*=`), // const/let/var name =
|
|
351
|
+
new RegExp(`^\\s*class\\s+${esc}\\b`), // class name
|
|
352
|
+
new RegExp(`^\\s*${esc}\\s*[:(]`), // name: or name(
|
|
353
|
+
new RegExp(`exports\\.${esc}\\s*=`), // exports.name =
|
|
354
|
+
new RegExp(`^\\s*def\\s+${esc}\\s*\\(`), // def name( (Python)
|
|
304
355
|
];
|
|
305
356
|
|
|
306
|
-
|
|
307
|
-
for (let i = 0; i < patterns.length - 1; i++) {
|
|
357
|
+
for (const pattern of patterns) {
|
|
308
358
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
309
|
-
if (
|
|
310
|
-
return lineIdx + 1;
|
|
359
|
+
if (pattern.test(lines[lineIdx])) {
|
|
360
|
+
return lineIdx + 1;
|
|
311
361
|
}
|
|
312
362
|
}
|
|
313
363
|
}
|
|
314
364
|
|
|
315
|
-
// Fallback: loose match (just contains the symbol)
|
|
316
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
317
|
-
if (patterns[patterns.length - 1].test(lines[lineIdx])) {
|
|
318
|
-
return lineIdx + 1;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
365
|
return null;
|
|
323
366
|
}
|
|
324
367
|
|