feat(voyage): implement loadProjectDirectory pipeline (validate + classify + read)
This commit is contained in:
parent
974835537a
commit
2bf766673d
1 changed files with 108 additions and 0 deletions
|
|
@ -964,6 +964,114 @@ playground first-run shows a complete round-trip-able artifact.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- v4.3 Step 13 — loadProjectDirectory pipeline -----------------
|
||||||
|
// Validate → classify → read → parse-frontmatter → build
|
||||||
|
// ProjectArtifacts. Mirrors lib/parsers/project-discovery.mjs:15-24
|
||||||
|
// typedef on the browser side; storage-key derived deterministically
|
||||||
|
// from basePath so per-project draft state cannot leak across projects.
|
||||||
|
async function loadProjectDirectory(files, basePath) {
|
||||||
|
if (!files || !files.length) return null;
|
||||||
|
var prefix = basePath ? basePath + '/' : '';
|
||||||
|
var artifacts = {
|
||||||
|
basePath: basePath || '',
|
||||||
|
storageKey: '',
|
||||||
|
brief: null,
|
||||||
|
plan: null,
|
||||||
|
review: null,
|
||||||
|
progress: null,
|
||||||
|
research: [],
|
||||||
|
architecture: { overview: null, gaps: null, looseFiles: [] },
|
||||||
|
looseFiles: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Phase 1 — validate + classify (sync).
|
||||||
|
var classified = [];
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
var f = files[i];
|
||||||
|
var rel = f.webkitRelativePath || '';
|
||||||
|
// Validate: path must start with basePath prefix; reject parent traversal.
|
||||||
|
if (basePath && rel.indexOf(prefix) !== 0) continue;
|
||||||
|
if (rel.indexOf('..') !== -1) continue;
|
||||||
|
var inside = rel.slice(prefix.length);
|
||||||
|
if (!inside) continue;
|
||||||
|
var parts = inside.split('/');
|
||||||
|
var name = parts[parts.length - 1];
|
||||||
|
var bucket = null;
|
||||||
|
if (parts.length === 1 && name === 'brief.md') bucket = 'brief';
|
||||||
|
else if (parts.length === 1 && name === 'plan.md') bucket = 'plan';
|
||||||
|
else if (parts.length === 1 && name === 'review.md') bucket = 'review';
|
||||||
|
else if (parts.length === 1 && name === 'progress.json') bucket = 'progress';
|
||||||
|
else if (parts.length === 2 && parts[0] === 'research' && /\.md$/.test(name)) bucket = 'research';
|
||||||
|
else if (parts.length === 2 && parts[0] === 'architecture' && name === 'overview.md') bucket = 'arch_overview';
|
||||||
|
else if (parts.length === 2 && parts[0] === 'architecture' && name === 'gaps.md') bucket = 'arch_gaps';
|
||||||
|
else if (parts.length === 2 && parts[0] === 'architecture' && /\.md$/.test(name)) bucket = 'arch_loose';
|
||||||
|
else bucket = 'loose';
|
||||||
|
classified.push({ file: f, rel: inside, bucket: bucket });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2 — read content + parse frontmatter (async).
|
||||||
|
for (var j = 0; j < classified.length; j++) {
|
||||||
|
var c = classified[j];
|
||||||
|
var text = '';
|
||||||
|
try { text = await c.file.text(); } catch (_) { text = ''; }
|
||||||
|
var fm = quickParseFrontmatter(text);
|
||||||
|
var entry = { path: c.rel, content: text, frontmatter: fm };
|
||||||
|
switch (c.bucket) {
|
||||||
|
case 'brief': artifacts.brief = entry; break;
|
||||||
|
case 'plan': artifacts.plan = entry; break;
|
||||||
|
case 'review': artifacts.review = entry; break;
|
||||||
|
case 'progress': artifacts.progress = entry; break;
|
||||||
|
case 'research': artifacts.research.push(entry); break;
|
||||||
|
case 'arch_overview': artifacts.architecture.overview = entry; break;
|
||||||
|
case 'arch_gaps': artifacts.architecture.gaps = entry; break;
|
||||||
|
case 'arch_loose': artifacts.architecture.looseFiles.push(entry); break;
|
||||||
|
default: artifacts.looseFiles.push(entry); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artifacts.research.sort(function (a, b) { return a.path.localeCompare(b.path); });
|
||||||
|
|
||||||
|
// Phase 3 — deterministic storage-key from basePath (NOT URL params)
|
||||||
|
// so draft-annotation state is isolated per project.
|
||||||
|
artifacts.storageKey = 'voyage_proj_' + djb2Hash(artifacts.basePath || '(unnamed)');
|
||||||
|
|
||||||
|
// Phase 4 — warn if brief missing (operator may still drop a partial dir).
|
||||||
|
if (!artifacts.brief) {
|
||||||
|
announce('Advarsel: brief.md mangler i prosjektmappen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5 — render dashboard if available, else log.
|
||||||
|
if (typeof renderDashboard === 'function') {
|
||||||
|
renderDashboard(artifacts);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
console.log('[voyage] projectArtifacts loaded', {
|
||||||
|
basePath: artifacts.basePath,
|
||||||
|
brief: !!artifacts.brief,
|
||||||
|
plan: !!artifacts.plan,
|
||||||
|
review: !!artifacts.review,
|
||||||
|
research: artifacts.research.length,
|
||||||
|
architecture: {
|
||||||
|
overview: !!artifacts.architecture.overview,
|
||||||
|
gaps: !!artifacts.architecture.gaps,
|
||||||
|
loose: artifacts.architecture.looseFiles.length
|
||||||
|
},
|
||||||
|
progress: !!artifacts.progress,
|
||||||
|
loose: artifacts.looseFiles.length,
|
||||||
|
storageKey: artifacts.storageKey
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return artifacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// djb2 — small deterministic non-cryptographic hash for storage-keys.
|
||||||
|
// Not security-sensitive; only needs to map basePath → stable string.
|
||||||
|
function djb2Hash(s) {
|
||||||
|
var h = 5381;
|
||||||
|
for (var i = 0; i < s.length; i++) h = ((h << 5) + h) + s.charCodeAt(i);
|
||||||
|
return (h >>> 0).toString(36);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- v4.3 Step 12 — drag-drop with webkitGetAsEntry ---------------
|
// ---- v4.3 Step 12 — drag-drop with webkitGetAsEntry ---------------
|
||||||
// Document-level dragenter shows the overlay; drop iterates
|
// Document-level dragenter shows the overlay; drop iterates
|
||||||
// dataTransfer.items, walks DirectoryEntry.readEntries() recursively
|
// dataTransfer.items, walks DirectoryEntry.readEntries() recursively
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue