import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createHash } from 'node:crypto'; import { scanSuites, syncQualityMirror } from './quality-mirror.js'; import { SYMPTOM_FIELD, SYMPTOM_OPTIONS } from './bugreport.js'; import { DEFINABLE_TYPES } from './engine.js'; import { applyHandbook, applyFormattingShowcase, applyIconShowcase } from './handbook.js'; export { DEFINABLE_TYPES }; export function seedFieldShowcase(w) { if (!w.listSpaces().some((sp) => sp.name === 'Showcase')) seedShowcaseBase(w); syncShowcase(w); return w; } const showcaseDay = (offset) => new Date(Date.now() + offset * 86400000).toISOString().slice(0, 10); function seedShowcaseBase(w) { w.createSpace({ name: 'Showcase', icon: 'lucide:compass', description: 'Every field type, in several configurations — the range of what a field can be, visible in one grid' }); const people = w.createTable({ space: 'Showcase', name: 'People', noun: 'person', icon: 'lucide:users' }); w.addField(people, { name: 'Email', type: 'email' }); w.addField(people, { name: 'Age', type: 'number' }); const ft = w.createTable({ space: 'Showcase', name: 'Field Types', noun: 'example', icon: 'lucide:settings' }); w.addField(ft, { name: 'Notes', type: 'text', config: { default: 'n/a' } }); w.addField(ft, { name: 'Site', type: 'url' }); w.addField(ft, { name: 'Contact', type: 'email' }); w.addField(ft, { name: 'Count', type: 'number' }); w.addField(ft, { name: 'Price', type: 'number', config: { format: 'currency', currency: 'USD', decimals: 2 } }); w.addField(ft, { name: 'Share', type: 'number', config: { format: 'percent', decimals: 1 } }); w.addField(ft, { name: 'Weight', type: 'number', config: { unit: 'kg', decimals: 0 } }); w.addField(ft, { name: 'Due', type: 'date' }); w.addField(ft, { name: 'Start', type: 'date', config: { format: 'us' } }); w.addField(ft, { name: 'Published', type: 'date', config: { format: 'long', time: true } }); w.addField(ft, { name: 'Window', type: 'daterange' }); w.addField(ft, { name: 'Done', type: 'checkbox', config: { default: false } }); w.addField(ft, { name: 'Feed', type: 'toggle', config: { on: 'Live', off: 'Paused', default: false } }); w.addField(ft, { name: 'Priority', type: 'select', config: { options: [ { name: 'Low', color: '#2ea043' }, { name: 'Medium', color: '#f59f00' }, { name: 'High', color: '#e5484d' }] } }); w.addField(ft, { name: 'Category', type: 'select', config: { options: ['Hardware', 'Software', 'Service'] } }); w.addField(ft, { name: 'Tags', type: 'multiselect', config: { options: [ { name: 'alpha', color: '#4769eb' }, { name: 'beta', color: '#8e4ec6' }, { name: 'stable', color: '#2ea043' }, { name: 'legacy', color: '' }] } }); w.addField(ft, { name: 'Stage', type: 'workflow', config: { states: [ { name: 'Backlog', category: 'not-started', default: true }, { name: 'Building', category: 'in-progress' }, { name: 'Shipped', category: 'done' }, { name: 'Dropped', category: 'canceled' }] } }); w.addField(ft, { name: 'Review', type: 'workflow', config: { states: [ { name: 'Pending', category: 'not-started', default: true }, { name: 'Approved', category: 'done' }] } }); w.addField(ft, { name: 'Brief', type: 'document' }); w.addField(ft, { name: 'Definition', type: 'field' }); w.addField(ft, { name: 'Nested definition', type: 'field', config: { depth: 2 } }); w.addField(ft, { name: 'API key', type: 'key' }); w.addField(ft, { name: 'Files', type: 'attachments' }); w.addField(ft, { name: 'Face', type: 'avatar' }); w.addRelation(ft, { name: 'Owner', targetDb: people, cardinality: 'many-to-one', inverseName: 'Owns' }); w.addRelation(ft, { name: 'Peers', targetDb: people, cardinality: 'many-to-many', inverseName: 'Peer of' }); w.addField(ft, { name: 'Owner email', type: 'lookup', config: { relationField: 'Owner', targetField: 'Email' } }); w.addField(ft, { name: 'Peer count', type: 'rollup', config: { relationField: 'Peers', aggregate: 'count' } }); w.addField(ft, { name: 'Peer age', type: 'rollup', config: { relationField: 'Peers', aggregate: 'avg', targetField: 'Age' } }); w.addField(ft, { name: 'Peer names', type: 'rollup', config: { relationField: 'Peers', aggregate: 'join', targetField: 'Name' } }); w.addField(ft, { name: 'Total', type: 'formula', config: { expression: 'Price * Count' } }); w.addField(ft, { name: 'Label', type: 'formula', config: { expression: 'if(empty(Category) or empty(Priority), "", concat(upper(Category), " · ", Priority))' } }); w.addField(ft, { name: 'Days left', type: 'formula', config: { expression: 'if(empty(Due), "", days(today(), Due))' } }); const ada = w.createEntity(people, { name: 'Ada Chen', values: { Email: 'ada@example.com', Age: 34 } }); const leo = w.createEntity(people, { name: 'Leo Marsh', values: { Email: 'leo@example.com', Age: 41 } }); const mia = w.createEntity(people, { name: 'Mia Okafor', values: { Email: 'mia@example.com', Age: 29 } }); const rows = [ { name: 'Sensor board', values: { Notes: 'Rev C, lead-free', Site: 'https://example.com/sensor', Contact: 'sales@example.com', Count: 12, Price: 149.5, Share: 0.325, Weight: 2, Due: showcaseDay(31), Start: showcaseDay(-14), Published: `${showcaseDay(-7)}T14:30:00Z`, Window: { start: showcaseDay(-14), end: showcaseDay(31) }, Done: false, Feed: true, Priority: 'High', Category: 'Hardware', Tags: ['alpha', 'stable'], Definition: { type: 'number', config: { format: 'currency', unit: 'EUR', decimals: 2 } }, 'Nested definition': { type: 'field', config: { depth: 1 } }, 'API key': 'vendor-portal', Owner: ada.id, Peers: [leo.id, mia.id], }, stage: 'Building', review: 'Approved' }, { name: 'Sync service', values: { Notes: 'Runs hourly', Site: 'https://example.com/sync', Contact: 'ops@example.com', Count: 3, Price: 1200, Share: 0.5, Weight: 0, Due: showcaseDay(9), Start: showcaseDay(-30), Published: `${showcaseDay(-21)}T09:00:00Z`, Window: { start: showcaseDay(-30), end: showcaseDay(9) }, Done: true, Priority: 'Medium', Category: 'Software', Tags: ['beta'], Definition: { type: 'select', config: { options: ['on', 'off'] } }, Owner: leo.id, Peers: [ada.id], }, stage: 'Shipped', review: 'Approved' }, { name: 'Onboarding call', values: { Count: 1, Price: 0, Share: 0, Weight: 0, Due: showcaseDay(61), Priority: 'Low', Category: 'Service', Tags: ['legacy'], Definition: { type: 'checkbox', config: {} }, Owner: mia.id, Peers: [ada.id, leo.id, mia.id], }, stage: 'Backlog', review: 'Pending' }, { name: 'Blank row', values: {} , stage: 'Dropped', review: 'Pending' }, ]; for (const r of rows) { const e = w.createEntity(ft, { name: r.name, values: r.values }); w.setState(e.id, 'Stage', r.stage); w.setState(e.id, 'Review', r.review); } w.save(); return w; } const SPARK_KEYS = '[Peer joined]'; export const SHOWCASE_ADDITIONS = [ { table: 'People', fields: [ { after: 'Age', name: 'Skill', type: 'rating', config: { max: 5, icon: 'lucide:star' } }, { after: 'Skill', name: 'Joined', type: 'date' }, { after: 'Joined', name: 'Delta', type: 'number' }, ], rows: { 'Ada Chen': { Skill: 5, Joined: '2024-03-04', Delta: 4 }, 'Leo Marsh': { Skill: 2, Joined: '2023-06-12', Delta: -3 }, 'Mia Okafor': { Skill: 4, Joined: '2025-01-20', Delta: 2 }, } }, { table: 'Field Types', fields: [ { after: 'Weight', name: 'Progress', type: 'number', config: { display: 'bar' } }, { after: 'Progress', name: 'Score', type: 'number', config: { decimals: 1, display: 'bar', scale: 10 } }, { after: 'Score', name: 'Completion', type: 'number', config: { format: 'percent', decimals: 0, display: 'ring', scale: 1 } }, { after: 'Completion', name: 'Load', type: 'number', config: { display: 'heat' } }, { after: 'Load', name: 'Momentum', type: 'number', config: { display: 'bar', color: 'icon' } }, { after: 'Momentum', name: 'Reach', type: 'number', config: { format: 'percent', decimals: 0, display: 'ring', scale: 1, color: 'accent' } }, { after: 'Reach', name: 'Warmth', type: 'number', config: { display: 'heat', color: 'icon' } }, { after: 'Load', name: 'Fit', type: 'rating', config: { max: 5, icon: 'lucide:star' } }, { after: 'Fit', name: 'Effort', type: 'rating', config: { max: 3, icon: 'lucide:zap' } }, { after: 'Effort', name: 'Love', type: 'rating', config: { max: 7, icon: 'lucide:heart' } }, { after: 'Love', name: 'Brightness', type: 'rating', config: { max: 12, icon: 'lucide:sun', default: 6 } }, { after: 'Brightness', name: 'Stars', type: 'rating', config: { max: 5, icon: 'lucide:star', color: 'icon' } }, { after: 'Stars', name: 'Bolts', type: 'rating', config: { max: 10, icon: 'lucide:zap', color: 'icon' } }, { after: 'Bolts', name: 'Hearts', type: 'rating', config: { max: 5, icon: 'lucide:heart', color: 'accent' } }, { after: 'Peer names', name: 'Peer skill', type: 'rollup', config: { relationField: 'Peers', aggregate: 'avg', targetField: 'Skill' } }, { after: 'Peer skill', name: 'Peer ages', type: 'lookup', config: { relationField: 'Peers', targetField: 'Age' } }, { after: 'Peer ages', name: 'Peer skills', type: 'lookup', config: { relationField: 'Peers', targetField: 'Skill' } }, { after: 'Peer skills', name: 'Peer deltas', type: 'lookup', config: { relationField: 'Peers', targetField: 'Delta' } }, { after: 'Peer deltas', name: 'Peer joined', type: 'lookup', config: { relationField: 'Peers', targetField: 'Joined' } }, { after: 'Days left', name: 'Age trend', type: 'formula', config: { expression: `sortby([Peer ages], ${SPARK_KEYS})`, display: 'sparkline', style: 'line' } }, { after: 'Age trend', name: 'Delta columns', type: 'formula', config: { expression: `sortby([Peer deltas], ${SPARK_KEYS})`, display: 'sparkline', style: 'column' } }, { after: 'Delta columns', name: 'Wins and losses', type: 'formula', config: { expression: `sortby([Peer deltas], ${SPARK_KEYS})`, display: 'sparkline', style: 'winloss' } }, { after: 'Wins and losses', name: 'Skill trend', type: 'formula', config: { expression: `if(empty([Peer skills]), null, sortby([Peer skills], ${SPARK_KEYS}))`, display: 'sparkline', style: 'line' } }, { after: 'Skill trend', name: 'Age line', type: 'formula', config: { expression: `sortby([Peer ages], ${SPARK_KEYS})`, display: 'sparkline', style: 'line', color: 'accent' } }, { after: 'Age line', name: 'Delta blocks', type: 'formula', config: { expression: `sortby([Peer deltas], ${SPARK_KEYS})`, display: 'sparkline', style: 'winloss', color: 'icon' } }, ], rows: { 'Sensor board': { Progress: 72, Score: 7.5, Completion: 0.8, Load: 9, Momentum: 40, Reach: 0.6, Warmth: 8, Fit: 4, Effort: 2, Love: 6, Brightness: 9, Stars: 4, Bolts: 7, Hearts: 3 }, 'Sync service': { Progress: 100, Score: 9.2, Completion: 1, Load: 3, Momentum: 85, Reach: 0.9, Warmth: 2, Fit: 5, Effort: 1, Love: 7, Brightness: 12, Stars: 5, Bolts: 10, Hearts: 5 }, 'Onboarding call': { Progress: 15, Score: 2, Completion: 0.25, Load: 0, Momentum: 10, Reach: 0.3, Warmth: 5, Effort: 0, Love: 3, Brightness: 4, Stars: 2, Bolts: 3, Hearts: 1 }, } }, ]; export function showcaseHash() { return createHash('sha256').update(JSON.stringify(SHOWCASE_ADDITIONS)).digest('hex').slice(0, 16); } const isBlank = (v) => v == null || v === '' || (Array.isArray(v) && v.length === 0); function applyShowcaseAdditions(w) { let added = 0; for (const { table, fields, rows } of SHOWCASE_ADDITIONS) { const db = w.findTable(`Showcase/${table}`); if (!db) continue; for (const { after, ...def } of fields) { if (w.findField(db, def.name)) continue; const f = w.addField(db, def); added++; const anchor = w.findField(db, after); if (!anchor) continue; const order = db.fieldOrder.filter((id) => id !== f.id); order.splice(order.indexOf(anchor.id) + 1, 0, f.id); w.updateTable(db.id, { fieldOrder: order }); for (const v of db.tableViews ?? []) { if (v.fields.includes(anchor.id)) w.tableView(`Showcase/${table}/${v.id}`, { move: { field: f.name, after: anchor.name } }); } } for (const [name, values] of Object.entries(rows)) { const row = w.findEntity(db, name); if (!row) continue; const patch = {}; for (const [k, v] of Object.entries(values)) { const f = w.findField(db, k); if (f && isBlank(row.values[f.id])) patch[k] = v; } if (Object.keys(patch).length) w.updateEntity(row.id, patch); } } return added; } export function syncShowcase(w, { force = false } = {}) { if (!w.listSpaces().some((sp) => sp.name === 'Showcase')) return { applied: false }; const hash = showcaseHash(); if (!force && w.state.meta.showcaseSync === hash) return { applied: false, hash }; const actor = w.actor; w.actor = 'showcase-sync'; let added; try { added = applyShowcaseAdditions(w); } finally { w.actor = actor; } w.state.meta.showcaseSync = hash; w.save(); return { applied: true, added, hash }; } export function seedWeaver(w) { w.state.meta.name = 'weave'; w.createSpace({ name: 'Handbook', icon: 'lucide:file-text', description: 'Official documentation and how-tos' }); const guides = w.createTable({ space: 'Handbook', name: 'Guide', icon: 'lucide:file-text' }); w.addField(guides, { name: 'Audience', type: 'select', config: { options: ['Human', 'Agent', 'Both'] } }); w.addField(guides, { name: 'Order', type: 'number' }); w.createEntity(guides, { name: 'Quickstart', values: { Audience: 'Both', Order: 1 }, doc: `# Quickstart \`\`\`bash node bin/weave.js serve --port 4400 --data ./uno.json \`\`\` Open http://127.0.0.1:4400 — the sidebar lists spaces and tables. Press **⌘K** to search everything with permalinks. Workspaces live side by side: this docs workspace is at \`/w/weaver/\`, your data workspace at \`/\`. Use the switcher in the sidebar.`, }); w.createEntity(guides, { name: 'Data model', values: { Audience: 'Both', Order: 2 }, doc: `# Data model Spaces group **tables**; tables hold **entities** with auto public ids (\`Task#3\`). Field types: text, number, date, daterange, checkbox, toggle (a switch with two named states), url, email, select, multiselect, **workflow** (multistate with categories), **relation** (always a bidirectional pair), **lookup**, **rollup** (count/sum/avg/min/max/join), **formula**, and **document** (markdown, several per table). Every entity's document fields render natively as MD, HTML, and PDF at \`/e//doc/.\`.`, }); w.createEntity(guides, { name: 'CLI reference', values: { Audience: 'Both', Order: 3 }, doc: `# CLI reference \`\`\`bash weave schema weave query Task --where '[["Project.Name","=","Apollo"]]' --select 'Estimate' weave create Task "Fix bug" --values '{"Priority":"P1"}' weave state Task#5 State "In Progress" weave doc set Task#5 --field Spec --content '# Spec' weave doc export Task#5 --format pdf --out t5.pdf \`\`\` Entities are addressable as \`Table#publicId\`, UUID, or name with \`--table\`.`, }); w.createSpace({ name: 'Wiki', icon: 'lucide:bookmark', description: 'Design notes and architecture' }); const articles = w.createTable({ space: 'Wiki', name: 'Article', icon: 'lucide:bookmark' }); w.addField(articles, { name: 'Topic', type: 'select', config: { options: ['Architecture', 'Philosophy', 'Internals'] } }); w.createEntity(articles, { name: 'Zero-dependency philosophy', values: { Topic: 'Philosophy' }, doc: `# Zero dependencies Weave has no runtime dependencies. The markdown renderer, PDF writer, CSV parser, HTTP router, and MCP server are all in-tree and tested. The whole codebase is readable in an afternoon; state is one human-readable JSON file per workspace with atomic writes.`, }); w.createEntity(articles, { name: 'The PDF writer', values: { Topic: 'Internals' }, doc: `# The PDF writer \`src/pdf.js\` emits PDF 1.4 directly: standard fonts (no embedding), real Helvetica AFM metrics for wrapping, WinAnsi typography (curly quotes, bullets, dashes), US Letter pages, and a byte-exact xref table (verified by tests).`, }); w.createEntity(articles, { name: 'Workspace hierarchy', values: { Topic: 'Architecture' }, doc: `# Hierarchy Workspace → spaces → tables → entities. Multiple workspaces share one web app (\`/w//\`). The direction of travel: spaces and the workspace itself become tables too, so schema, settings, users, and automations are all agent-editable through the same primitives.`, }); w.createSpace({ name: 'Quality', icon: 'lucide:shield-check', description: 'The Weave test suite, dogfooded' }); const suites = w.createTable({ space: 'Quality', name: 'Suite', icon: 'lucide:shield-check' }); const cases = w.createTable({ space: 'Quality', name: 'Case', icon: 'lucide:square-check' }); w.addField(cases, { name: 'Status', type: 'workflow', config: { states: [ { name: 'Failing', category: 'not-started' }, { name: 'Flaky', category: 'in-progress' }, { name: 'Passing', category: 'done', default: true }, ], }, }); w.addRelation(cases, { name: 'Suite', targetDb: suites, cardinality: 'many-to-one', inverseName: 'Cases' }); w.addField(suites, { name: 'Case Count', type: 'rollup', config: { relationField: 'Cases', aggregate: 'count' } }); w.addField(suites, { name: 'File', type: 'text' }); syncQualityMirror(w, scanSuites(join(dirname(fileURLToPath(import.meta.url)), '..'))); w.createSpace({ name: 'Development', icon: 'lucide:activity', description: 'Open issues and the roadmap, maintained as Weave is built' }); const issues = w.createTable({ space: 'Development', name: 'Issue', icon: 'lucide:bug' }); w.addField(issues, { name: 'Status', type: 'workflow', config: { states: [ { name: 'Open', category: 'not-started', default: true }, { name: 'In Progress', category: 'in-progress' }, { name: 'Fixed', category: 'done' }, ], }, }); w.addField(issues, { name: 'Severity', type: 'select', config: { options: ['Low', 'Medium', 'High'] } }); w.addField(issues, { name: SYMPTOM_FIELD, type: 'multiselect', config: { options: SYMPTOM_OPTIONS } }); const features = w.createTable({ space: 'Development', name: 'Feature', icon: 'lucide:star' }); w.addField(features, { name: 'Status', type: 'workflow', config: { states: [ { name: 'Planned', category: 'not-started', default: true }, { name: 'Building', category: 'in-progress' }, { name: 'Shipped', category: 'done' }, ], }, }); w.addField(features, { name: 'Milestone', type: 'select', config: { options: ['v0.1', 'v0.2', 'v0.3'] } }); const shipped = [ ['Core engine: tables, relations, workflows, lookups, rollups, formulas', 'v0.1'], ['Per-entity documents as native MD / HTML / PDF', 'v0.1'], ['Web UI: table, board, list + entity pages', 'v0.1'], ['CLI + MCP server + REST API', 'v0.1'], ['Automations incl. outgoing webhooks; CSV import/export; file attachments', 'v0.1'], ['Rename: databases → tables', 'v0.2'], ['Multiple document fields per entity', 'v0.2'], ['Inline editing of all fields + docs in every view', 'v0.2'], ['Relation map visualizing relations and automations', 'v0.2'], ['Universal ⌘K search with permalinks', 'v0.2'], ['Multi-workspace: uno + weaver with switcher', 'v0.2'], ]; for (const [name, ms] of shipped) { const e = w.createEntity(features, { name, values: { Milestone: ms } }); w.setState(e.id, 'Status', 'Shipped'); } const planned = [ ['Hierarchical meta-model: spaces & workspace as tables (agent-native)', 'v0.3'], ['Auto-composed bidirectional schema docs (edit schema as JSON/YAML)', 'v0.3'], ['Workspace-level audit log + agent user accounts & permissions', 'v0.3'], ['Permalink-based [[...]] entity links in markdown', 'v0.3'], ['Attachments field type', 'v0.3'], ['Saved multi-table views with public share links', 'v0.3'], ['UI design system adoption (decision pending)', 'v0.3'], ]; for (const [name, ms] of planned) w.createEntity(features, { name, values: { Milestone: ms } }); const knownIssues = [ ['Single-writer: CLI and server must not write one workspace file concurrently', 'Medium', 'Open'], ['No filter-builder UI (filters are API/CLI/MCP only)', 'Medium', 'Open'], ['Board drag-and-drop does not scroll columns horizontally while dragging', 'Low', 'Open'], ['PDF renders non-WinAnsi glyphs (e.g. emoji) as "?"', 'Low', 'Open'], ]; for (const [name, sev, st] of knownIssues) { const e = w.createEntity(issues, { name, values: { Severity: sev } }); if (st !== 'Open') w.setState(e.id, 'Status', st); } ensureReleaseTable(w); seedFieldShowcase(w); applyHandbook(w); applyFormattingShowcase(w); applyIconShowcase(w); w.save(); return w; } export function ensureReleaseTable(w) { const table = (qualified) => w.listTables().find((t) => `${w.getSpace(t.spaceId)?.name}/${t.name}` === qualified); const existing = table('Development/Release'); if (existing) return existing; const issues = table('Development/Issue'); const features = table('Development/Feature'); if (!issues || !features) return null; const rel = w.createTable({ space: 'Development', name: 'Release', icon: 'lucide:rocket' }); w.addField(rel, { name: 'Date', type: 'date' }); w.addField(rel, { name: 'Commit', type: 'text' }); w.addRelation(rel, { name: 'Fixes', targetDb: issues.id, cardinality: 'many-to-many', inverseName: 'Fixed in' }); w.addRelation(rel, { name: 'Ships', targetDb: features.id, cardinality: 'many-to-many', inverseName: 'Shipped in' }); return w.getTable(rel.id ?? rel); } function widenSelects(w, db, entries, selects) { const table = w.getTable(db.id); for (const sel of selects) { const field = Object.values(table.fields).find((f) => f.name === sel); if (!field || (field.type !== 'select' && field.type !== 'multiselect')) continue; const options = field.config.options ?? []; const known = new Set(options.map((o) => o.name)); const add = []; for (const entry of entries ?? []) { for (const v of [entry[sel.toLowerCase()] ?? []].flat()) { if (typeof v !== 'string' || known.has(v) || add.includes(v)) continue; add.push(v); } } if (add.length) w.updateField(table.id, field.id, { config: { options: [...options, ...add] } }); } } export function syncDevelopment(w, manifest) { if (!manifest || !Array.isArray(manifest.issues)) return { applied: false }; const releases = Array.isArray(manifest.releases) ? manifest.releases : []; for (const r of releases) { if (!(r.description ?? '').trim() && r.name === `v${manifest.version}`) { throw new Error(`release ${r.name} has no notes — every Development/Release record carries its notes`); } } const stamp = `${manifest.version}:${manifest.generatedAt}:${(manifest.issues.length + (manifest.features?.length ?? 0) + releases.length)}`; if (w.state.meta.developmentSync === stamp) return { applied: false }; const table = (qualified) => w.listTables().find((t) => `${w.getSpace(t.spaceId)?.name}/${t.name}` === qualified); const issuesT = table('Development/Issue'); const featuresT = table('Development/Feature'); if (!issuesT || !featuresT) return { applied: false }; let created = 0, updated = 0; const skipped = []; const apply = (db, entries, selects) => { widenSelects(w, db, entries, selects); const byName = new Map(w.listEntities(db.id).map((e) => [w.entityName(e), e])); for (const entry of entries ?? []) { const existing = byName.get(entry.name); const values = {}; for (const sel of selects) { const v = entry[sel.toLowerCase()]; if (v != null) values[sel] = v; } try { if (!existing) { const e = w.createEntity(db.id, { name: entry.name, values, ...(entry.description ? { doc: entry.description } : {}) }); if (entry.status) w.setState(e.id, 'Status', entry.status); created++; continue; } const read = w.readEntity(existing.id); let touched = false; if (entry.status && read.fields.Status !== entry.status) { w.setState(existing.id, 'Status', entry.status); touched = true; } const patch = {}; for (const [k, v] of Object.entries(values)) { const cur = read.fields[k]; if (JSON.stringify(cur ?? null) !== JSON.stringify(v ?? null)) patch[k] = v; } if (Object.keys(patch).length) { w.updateEntity(existing.id, patch); touched = true; } if (touched) updated++; } catch (err) { skipped.push(`${db.name} '${entry.name}': ${err.message}`); } } }; apply(issuesT, manifest.issues, ['Severity', 'Symptom']); apply(featuresT, manifest.features, ['Milestone']); if (releases.length) { const relT = ensureReleaseTable(w); apply(relT, releases, ['Date', 'Commit']); const byName = (db) => new Map(w.listEntities(db.id).map((e) => [w.entityName(e), e.id])); const issueIds = byName(issuesT), featureIds = byName(featuresT), relIds = byName(relT); const reconcile = (id, field, wantIds) => { const cur = w.readEntity(id).raw[field] ?? []; const stale = cur.filter((x) => !wantIds.includes(x)); const add = wantIds.filter((x) => !cur.includes(x)); if (add.length) w.link(id, field, add); if (stale.length) w.unlink(id, field, stale); }; for (const r of releases) { const id = relIds.get(r.name); if (!id) continue; reconcile(id, 'Fixes', (r.fixes ?? []).map((n) => issueIds.get(n)).filter(Boolean)); reconcile(id, 'Ships', (r.ships ?? []).map((n) => featureIds.get(n)).filter(Boolean)); } } if (!skipped.length) w.state.meta.developmentSync = stamp; w.save(); return { applied: true, created, updated, skipped }; }