Splitting works for more batches; updates never delete a release still in use

- A split part may change the compiled code of its own batch's functions
  still in review (their source is put back as on main; shared changes in the
  file still affect them). It no longer counts as changing functions someone
  else holds; their status is measured again.
- An annotated declaration (a STUB on a prototype) is a region of its own
  instead of stopping the split of the whole file.
- Running processes record the release they started from; update.sh keeps
  those. A worker left running across deploys lost its release directory and
  every analysis failed with "Cannot find module analyze-worker.js".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
This commit is contained in:
Saty 2026-10-18 12:50:04 +02:00
commit aaba8ad165
5 changed files with 32 additions and 3 deletions

View file

@ -77,8 +77,13 @@ else
fi
fi
# Keep the last three releases for a quick rollback (ln -sfn <release> /opt/togetherdecomp).
# Keep the last three releases for a quick rollback (ln -sfn <release> /opt/togetherdecomp), and
# every release a running process was started from (it loads files from it later, e.g. workers).
inuse=$(cat /var/lib/togetherdecomp/release-*.txt 2>/dev/null)
ls -1dt "$BASE"/*/ | tail -n +4 | while read -r old; do
[ "$(readlink -f "$old")" = "$(readlink -f "$LINK")" ] || rm -rf "$old"
old=$(readlink -f "$old")
[ "$old" = "$(readlink -f "$LINK")" ] && continue
echo "$inuse" | grep -qxF "$old" && continue
rm -rf "$old"
done
echo "== done: $(readlink -f "$LINK")"

View file

@ -771,6 +771,10 @@ export async function startServer(dataDir, { role = 'all' } = {}) {
if (!['all', 'web', 'worker'].includes(role)) throw new Error(`unknown role ${role}; use web, worker or all`);
const ctx = openServer(dataDir);
ctx.runsWorker = role !== 'web';
// deploy/update.sh keeps the releases running processes were started from.
try {
fs.writeFileSync(path.join(ctx.config.dataDir, `release-${role}.txt`), path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') + '\n');
} catch {}
const stops = [];
if (role !== 'web') {
ctx.tasks.generate();

View file

@ -250,6 +250,10 @@ export class Merger {
}
}
const targets = new Set(attempts.map((a) => a.symbol_id));
// The batch a split part came from holds the other functions of these files (still in review,
// their source put back as on main); shared changes in the file may change their compiled code.
// That is not a change to functions someone else holds; their status is measured again.
if (parentId) for (const a of batchAttempts(this.store, parentId)) targets.add(a.symbol_id);
const outside = this.verifier.changedOutside(check, merged.files, targets);
const confirmedIds = new Set();
const results = [];

View file

@ -23,7 +23,8 @@ export function functionRegions(text) {
}
const out = new Map();
for (const g of groups.values()) {
const end = bodyEnd(lines, g.targetLine - 1);
// A definition runs to its closing brace; an annotated declaration to its semicolon.
const end = bodyEnd(lines, g.targetLine - 1) ?? declarationEnd(lines, g.targetLine - 1);
if (end === null) return null;
const region = { start: g.start, end, ids: g.ids };
for (const id of g.ids) out.set(id, region);
@ -31,6 +32,12 @@ export function functionRegions(text) {
return out;
}
// Index of the line after the one that ends a declaration (its semicolon) starting at `from`.
function declarationEnd(lines, from) {
for (let i = from; i < Math.min(lines.length, from + 20); i++) if (lines[i].replace(/\/\/.*$/, '').includes(';')) return i + 1;
return null;
}
// Index of the line after the brace that closes the body starting at or after line `from`.
function bodyEnd(lines, from) {
let depth = 0;

View file

@ -37,3 +37,12 @@ test('withoutFunctions restores pending functions from the base, or drops new on
// One definition shared by a pending and a finished function can't be split.
assert.equal(withoutFunctions({ 'a.cpp': file }, ['fn_00002'], () => null), null);
});
test('an annotated declaration is a region of its own instead of stopping the split', () => {
const text = '// STUB: fn_00010\nvoid declared(int a);\n\n// FUNCTION: fn_00011\nint defined() {\n return 1;\n}\n';
const r = functionRegions(text);
assert.deepEqual([r.get('fn_00010').start, r.get('fn_00010').end], [0, 2]);
assert.deepEqual([r.get('fn_00011').start, r.get('fn_00011').end], [3, 7]);
const out = withoutFunctions({ 'a.cpp': text }, ['fn_00011'], () => null);
assert.equal(out['a.cpp'], '// STUB: fn_00010\nvoid declared(int a);\n\n');
});