Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base18aee8fa1). Dewey approved round 3 (comment 28832) and Filbert approved it with no findings (28835). Mos gave the decision 123 go on public diff 8360122b (mos-0636): no vendored code, no innerHTML or other HTML sinks. The Docs page #/docs lists each project's spec/north-star.md and the Markdown files directly under docs/. Save writes the typed text as one local commit authored by business.human, through a new human-only bus verb project.file.save that carries the opened commit as base and refuses stale-base if the file moved. The preview is a textarea plus a renderer built with createElement and textContent only; raw HTML shows as source text and images never load. CSP is unchanged. Round 3 replaced the backreference HR regex, which overflowed the regexp stack, with an isRule scan. Round 4 added a textContent fallback when paintDoc throws, and the submit handler resets saving in a finally. Mos's rule (ceiling 1 s at 1 MiB, growth x3 per doubling over 1 to 4 MiB): 130/130 and 138/138 cells pass, worst 513 ms at 1 MiB, worst ratio x2.50, control x2.01; the 756-cell grid has 0 timeouts. Mutants 43/44, v21 equivalent. Follow-ups: Vikunja P19 tasks 318 (Save stays off after a non-render paint throw), 319 (dead NUL check in bus.js) and 320 (the 1 MiB + 64 KiB buffer before the human proof, Gitea #1650). Sage's landing gate ran ona4779d2a5plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z, network sage-r148g_default removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
49 lines
2.2 KiB
JavaScript
49 lines
2.2 KiB
JavaScript
import { lstatSync } from 'node:fs';
|
|
import { dirname, isAbsolute, join, normalize, relative, sep } from 'node:path';
|
|
|
|
export class ProjectError extends Error {
|
|
constructor(code, message) { super(`${code}: ${message}`); this.code = code; }
|
|
}
|
|
export const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
export const checkedId = value => {
|
|
if (typeof value !== 'string' || !ID.test(value)) throw new ProjectError('project-id', 'invalid id');
|
|
return value;
|
|
};
|
|
// The files a person opens and saves in the Console (row 148): the spec, and Markdown directly
|
|
// under docs/. Reads and the human writer share this one definition.
|
|
export const documentPath = path => typeof path === 'string' &&
|
|
(path === 'spec/north-star.md' || /^docs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(path));
|
|
export function absolute(path) {
|
|
if (typeof path !== 'string' || !isAbsolute(path) || normalize(path) !== path || path.includes('\0'))
|
|
throw new ProjectError('project-path', 'expected normalized absolute path');
|
|
return path;
|
|
}
|
|
export function noLinks(path) {
|
|
for (let part = path; ; part = dirname(part)) {
|
|
try {
|
|
const st = lstatSync(part);
|
|
if (!st.isDirectory() || st.isSymbolicLink()) throw new ProjectError('project-path', 'path ancestor must be a directory, never a link');
|
|
} catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
if (part === dirname(part)) break;
|
|
}
|
|
}
|
|
export function projectRepo({ business, project, dataRoot }) {
|
|
checkedId(business?.id); checkedId(project);
|
|
if (!Object.hasOwn(business?.projects ?? {}, project))
|
|
throw new ProjectError('project-not-declared', `business does not declare ${project}`);
|
|
absolute(dataRoot);
|
|
const entry = business.projects[project];
|
|
const repo = absolute(entry.repo ?? join(dataRoot, 'projects', business.id, project));
|
|
const contains = (base, child) => {
|
|
const rel = relative(base, child);
|
|
return rel === '' || (rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel));
|
|
};
|
|
for (const project of Object.values(business.projects)) {
|
|
const code = absolute(project.root);
|
|
if (contains(code, repo) || contains(repo, code))
|
|
throw new ProjectError('project-path', 'record repository and code checkout overlap');
|
|
}
|
|
noLinks(repo);
|
|
return repo;
|
|
}
|