chainlesschain 0.66.0 → 0.81.0

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.
@@ -369,3 +369,454 @@ function _notifySubscribers(taskId, status) {
369
369
 
370
370
  // Export for testing
371
371
  export { _subscriptions };
372
+
373
+ // ─── V2 Canonical Surface (Phase 81) ─────────────────────────────
374
+
375
+ /**
376
+ * Frozen task status enum (Phase 81 — adds INPUT_REQUIRED and CANCELED)
377
+ */
378
+ export const TASK_STATUS_V2 = Object.freeze({
379
+ SUBMITTED: "submitted",
380
+ WORKING: "working",
381
+ INPUT_REQUIRED: "input-required",
382
+ COMPLETED: "completed",
383
+ FAILED: "failed",
384
+ CANCELED: "canceled",
385
+ });
386
+
387
+ /**
388
+ * Frozen agent card status enum (Phase 81)
389
+ */
390
+ export const CARD_STATUS_V2 = Object.freeze({
391
+ ACTIVE: "active",
392
+ INACTIVE: "inactive",
393
+ EXPIRED: "expired",
394
+ });
395
+
396
+ /**
397
+ * Frozen subscription type enum (Phase 81)
398
+ */
399
+ export const SUBSCRIPTION_TYPE = Object.freeze({
400
+ TASK_UPDATE: "task_update",
401
+ AGENT_STATUS: "agent_status",
402
+ CAPABILITY_CHANGE: "capability_change",
403
+ });
404
+
405
+ /**
406
+ * Frozen capability-negotiation outcome enum (Phase 81)
407
+ */
408
+ export const NEGOTIATION_RESULT = Object.freeze({
409
+ COMPATIBLE: "compatible",
410
+ PARTIAL: "partial",
411
+ INCOMPATIBLE: "incompatible",
412
+ });
413
+
414
+ /**
415
+ * Task state machine — only allow documented transitions.
416
+ * Terminal: completed, failed, canceled.
417
+ */
418
+ const _allowedTaskTransitions = Object.freeze({
419
+ submitted: new Set(["working", "canceled", "failed"]),
420
+ working: new Set(["input-required", "completed", "failed", "canceled"]),
421
+ "input-required": new Set(["working", "canceled", "failed"]),
422
+ completed: new Set(),
423
+ failed: new Set(),
424
+ canceled: new Set(),
425
+ });
426
+
427
+ /**
428
+ * Agent card status transitions.
429
+ */
430
+ const _allowedCardTransitions = Object.freeze({
431
+ active: new Set(["inactive", "expired"]),
432
+ inactive: new Set(["active", "expired"]),
433
+ expired: new Set(["active"]),
434
+ });
435
+
436
+ // In-memory V2 state (parallel to existing DB-backed surface)
437
+ const _v2Tasks = new Map(); // taskId → { agentId, status, input, output, error, history[], deadline, inputPrompt, cancelReason, createdAt, updatedAt }
438
+ const _v2Cards = new Map(); // cardId → { cardId, status, updatedAt }
439
+ const _v2TypedSubs = new Map(); // key `${type}:${resourceId}` → Set<callback>
440
+
441
+ // ─── Card status & validation ────────────────────────────────────
442
+
443
+ /**
444
+ * Validate an A2A agent card against the Phase 81 schema.
445
+ * Returns { valid, errors[] } — does not throw.
446
+ */
447
+ export function validateAgentCard(card) {
448
+ const errors = [];
449
+ if (!card || typeof card !== "object") {
450
+ return { valid: false, errors: ["card must be an object"] };
451
+ }
452
+ if (!card.name || typeof card.name !== "string") {
453
+ errors.push("name is required and must be a string");
454
+ }
455
+ if (card.capabilities !== undefined && !Array.isArray(card.capabilities)) {
456
+ errors.push("capabilities must be an array");
457
+ }
458
+ if (card.skills !== undefined && !Array.isArray(card.skills)) {
459
+ errors.push("skills must be an array");
460
+ }
461
+ if (card.url !== undefined && typeof card.url !== "string") {
462
+ errors.push("url must be a string");
463
+ }
464
+ if (card.version !== undefined && !/^\d+\.\d+\.\d+$/.test(card.version)) {
465
+ errors.push("version must follow semver major.minor.patch");
466
+ }
467
+ if (
468
+ card.auth_type !== undefined &&
469
+ !["none", "bearer", "basic", "oauth2"].includes(card.auth_type)
470
+ ) {
471
+ errors.push("auth_type must be one of none|bearer|basic|oauth2");
472
+ }
473
+ return { valid: errors.length === 0, errors };
474
+ }
475
+
476
+ /**
477
+ * Transition an agent card between active/inactive/expired.
478
+ */
479
+ export function setCardStatus(db, cardId, status) {
480
+ if (!cardId) throw new Error("Card ID is required");
481
+ if (!Object.values(CARD_STATUS_V2).includes(status)) {
482
+ throw new Error(`Invalid card status: ${status}`);
483
+ }
484
+ const prev = _v2Cards.get(cardId)?.status || "active";
485
+ const allowed = _allowedCardTransitions[prev] || new Set();
486
+ if (prev !== status && !allowed.has(status)) {
487
+ throw new Error(`Invalid card transition: ${prev} → ${status}`);
488
+ }
489
+ const now = nowISO();
490
+ _v2Cards.set(cardId, { cardId, status, updatedAt: now });
491
+ if (db) {
492
+ try {
493
+ ensureA2ATables(db);
494
+ db.prepare(
495
+ `UPDATE a2a_agent_cards SET status = ?, updated_at = ? WHERE id = ?`,
496
+ ).run(status, now, cardId);
497
+ } catch (_err) {
498
+ // DB may not support UPDATE in mock — in-memory state is authoritative
499
+ }
500
+ }
501
+ return { cardId, status, updatedAt: now };
502
+ }
503
+
504
+ /**
505
+ * Get a card's V2 status (falls back to 'active' if no V2 entry exists).
506
+ */
507
+ export function getCardStatusV2(cardId) {
508
+ return _v2Cards.get(cardId)?.status || "active";
509
+ }
510
+
511
+ // ─── Task lifecycle V2 ───────────────────────────────────────────
512
+
513
+ /**
514
+ * Submit a task with optional timeout tracking.
515
+ */
516
+ export function sendTaskV2(db, { agentId, input, timeoutMs }) {
517
+ if (!agentId) throw new Error("Agent ID is required");
518
+ if (!input) throw new Error("Task input is required");
519
+ const taskId = generateId("task");
520
+ const now = nowISO();
521
+ const deadline =
522
+ typeof timeoutMs === "number" && timeoutMs > 0
523
+ ? Date.now() + timeoutMs
524
+ : null;
525
+ const task = {
526
+ taskId,
527
+ agentId,
528
+ status: TASK_STATUS_V2.SUBMITTED,
529
+ input,
530
+ output: "",
531
+ error: "",
532
+ history: [{ status: TASK_STATUS_V2.SUBMITTED, timestamp: now }],
533
+ deadline,
534
+ inputPrompt: null,
535
+ cancelReason: null,
536
+ createdAt: now,
537
+ updatedAt: now,
538
+ };
539
+ _v2Tasks.set(taskId, task);
540
+ _notifyTyped(SUBSCRIPTION_TYPE.TASK_UPDATE, taskId, {
541
+ taskId,
542
+ status: task.status,
543
+ });
544
+ return { taskId, status: task.status, deadline };
545
+ }
546
+
547
+ function _transitionTask(taskId, nextStatus, patch = {}) {
548
+ const task = _v2Tasks.get(taskId);
549
+ if (!task) throw new Error(`Task not found: ${taskId}`);
550
+ const allowed = _allowedTaskTransitions[task.status] || new Set();
551
+ if (!allowed.has(nextStatus)) {
552
+ throw new Error(`Invalid task transition: ${task.status} → ${nextStatus}`);
553
+ }
554
+ const now = nowISO();
555
+ task.status = nextStatus;
556
+ task.updatedAt = now;
557
+ task.history.push({
558
+ status: nextStatus,
559
+ timestamp: now,
560
+ ...patch.historyExtra,
561
+ });
562
+ if (patch.output !== undefined) task.output = patch.output;
563
+ if (patch.error !== undefined) task.error = patch.error;
564
+ if (patch.inputPrompt !== undefined) task.inputPrompt = patch.inputPrompt;
565
+ if (patch.cancelReason !== undefined) task.cancelReason = patch.cancelReason;
566
+ _notifyTyped(SUBSCRIPTION_TYPE.TASK_UPDATE, taskId, {
567
+ taskId,
568
+ status: nextStatus,
569
+ });
570
+ return { taskId, status: nextStatus };
571
+ }
572
+
573
+ /**
574
+ * Move a task from submitted → working (or input-required → working).
575
+ */
576
+ export function startWorking(_db, taskId) {
577
+ return _transitionTask(taskId, TASK_STATUS_V2.WORKING);
578
+ }
579
+
580
+ /**
581
+ * Request user input while working → input-required.
582
+ */
583
+ export function requestInput(_db, taskId, prompt) {
584
+ if (!prompt) throw new Error("Prompt is required");
585
+ return _transitionTask(taskId, TASK_STATUS_V2.INPUT_REQUIRED, {
586
+ inputPrompt: prompt,
587
+ });
588
+ }
589
+
590
+ /**
591
+ * Provide requested input; input-required → working (clears prompt).
592
+ */
593
+ export function provideInput(_db, taskId, responseInput) {
594
+ const task = _v2Tasks.get(taskId);
595
+ if (!task) throw new Error(`Task not found: ${taskId}`);
596
+ if (task.status !== TASK_STATUS_V2.INPUT_REQUIRED) {
597
+ throw new Error(
598
+ `provideInput requires status input-required, got ${task.status}`,
599
+ );
600
+ }
601
+ const result = _transitionTask(taskId, TASK_STATUS_V2.WORKING, {
602
+ inputPrompt: null,
603
+ historyExtra: { response: responseInput },
604
+ });
605
+ task.input = `${task.input}\n${responseInput}`;
606
+ return result;
607
+ }
608
+
609
+ /**
610
+ * Mark task complete (from working only).
611
+ */
612
+ export function completeTaskV2(_db, taskId, output, artifacts = []) {
613
+ const task = _v2Tasks.get(taskId);
614
+ if (!task) throw new Error(`Task not found: ${taskId}`);
615
+ const res = _transitionTask(taskId, TASK_STATUS_V2.COMPLETED, {
616
+ output: output || "",
617
+ });
618
+ task.artifacts = artifacts;
619
+ return res;
620
+ }
621
+
622
+ /**
623
+ * Mark task failed.
624
+ */
625
+ export function failTaskV2(_db, taskId, error) {
626
+ return _transitionTask(taskId, TASK_STATUS_V2.FAILED, {
627
+ error: error || "Unknown error",
628
+ });
629
+ }
630
+
631
+ /**
632
+ * Cancel a non-terminal task.
633
+ */
634
+ export function cancelTask(_db, taskId, reason) {
635
+ return _transitionTask(taskId, TASK_STATUS_V2.CANCELED, {
636
+ cancelReason: reason || "user_requested",
637
+ });
638
+ }
639
+
640
+ /**
641
+ * Check task timeout — if past deadline and not terminal, auto-fails.
642
+ * Returns { timedOut: boolean, status }.
643
+ */
644
+ export function checkTaskTimeout(_db, taskId, now = Date.now()) {
645
+ const task = _v2Tasks.get(taskId);
646
+ if (!task) throw new Error(`Task not found: ${taskId}`);
647
+ const terminal = [
648
+ TASK_STATUS_V2.COMPLETED,
649
+ TASK_STATUS_V2.FAILED,
650
+ TASK_STATUS_V2.CANCELED,
651
+ ];
652
+ if (terminal.includes(task.status)) {
653
+ return { timedOut: false, status: task.status };
654
+ }
655
+ if (task.deadline && now >= task.deadline) {
656
+ _transitionTask(taskId, TASK_STATUS_V2.FAILED, { error: "timeout" });
657
+ return { timedOut: true, status: TASK_STATUS_V2.FAILED };
658
+ }
659
+ return { timedOut: false, status: task.status };
660
+ }
661
+
662
+ /**
663
+ * Get V2 task snapshot.
664
+ */
665
+ export function getTaskV2(taskId) {
666
+ const task = _v2Tasks.get(taskId);
667
+ if (!task) throw new Error(`Task not found: ${taskId}`);
668
+ return { ...task, history: [...task.history] };
669
+ }
670
+
671
+ /**
672
+ * List V2 tasks (optional filter by agentId / status).
673
+ */
674
+ export function listTasksV2({ agentId, status } = {}) {
675
+ const out = [];
676
+ for (const task of _v2Tasks.values()) {
677
+ if (agentId && task.agentId !== agentId) continue;
678
+ if (status && task.status !== status) continue;
679
+ out.push({ ...task, history: [...task.history] });
680
+ }
681
+ return out;
682
+ }
683
+
684
+ // ─── Typed subscriptions ─────────────────────────────────────────
685
+
686
+ function _notifyTyped(type, resourceId, payload) {
687
+ const key = `${type}:${resourceId}`;
688
+ const subs = _v2TypedSubs.get(key);
689
+ if (subs) {
690
+ for (const cb of subs) {
691
+ try {
692
+ cb(payload);
693
+ } catch (_err) {
694
+ // Subscriber error should not break lifecycle
695
+ }
696
+ }
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Subscribe with typed filter.
702
+ */
703
+ export function subscribeTyped(type, resourceId, callback) {
704
+ if (!Object.values(SUBSCRIPTION_TYPE).includes(type)) {
705
+ throw new Error(`Invalid subscription type: ${type}`);
706
+ }
707
+ if (!resourceId) throw new Error("resourceId is required");
708
+ const key = `${type}:${resourceId}`;
709
+ if (!_v2TypedSubs.has(key)) _v2TypedSubs.set(key, new Set());
710
+ _v2TypedSubs.get(key).add(callback);
711
+ return () => {
712
+ const subs = _v2TypedSubs.get(key);
713
+ if (subs) {
714
+ subs.delete(callback);
715
+ if (subs.size === 0) _v2TypedSubs.delete(key);
716
+ }
717
+ };
718
+ }
719
+
720
+ // ─── Capability negotiation V2 ───────────────────────────────────
721
+
722
+ function _parseVersion(v) {
723
+ if (typeof v !== "string") return null;
724
+ const m = v.match(/^(\d+)\.(\d+)\.(\d+)$/);
725
+ if (!m) return null;
726
+ return { major: +m[1], minor: +m[2], patch: +m[3] };
727
+ }
728
+
729
+ function _semverCompatible(clientVer, serverVer) {
730
+ const c = _parseVersion(clientVer);
731
+ const s = _parseVersion(serverVer);
732
+ if (!c || !s) return true; // Unknown versions → assume compatible
733
+ if (c.major !== s.major) return false;
734
+ // Minor: client can be ≤ server minor
735
+ if (c.minor > s.minor) return false;
736
+ return true;
737
+ }
738
+
739
+ /**
740
+ * Negotiate capabilities against an agent's declared skills.
741
+ * @param {object} opts — { required: string[], preferred?: string[], version?: string }
742
+ * @param {object} agentCard — { capabilities: string[], version?: string }
743
+ */
744
+ export function negotiateCapabilityV2(
745
+ agentCard,
746
+ { required = [], preferred = [], version },
747
+ ) {
748
+ if (!agentCard || typeof agentCard !== "object") {
749
+ throw new Error("agentCard is required");
750
+ }
751
+ const caps = Array.isArray(agentCard.capabilities)
752
+ ? agentCard.capabilities
753
+ : [];
754
+ const missingRequired = required.filter((c) => !caps.includes(c));
755
+ const supportedPreferred = preferred.filter((c) => caps.includes(c));
756
+ const missingPreferred = preferred.filter((c) => !caps.includes(c));
757
+ const versionOk = version
758
+ ? _semverCompatible(version, agentCard.version)
759
+ : true;
760
+
761
+ let result;
762
+ if (!versionOk || missingRequired.length > 0) {
763
+ result = NEGOTIATION_RESULT.INCOMPATIBLE;
764
+ } else if (missingPreferred.length > 0) {
765
+ result = NEGOTIATION_RESULT.PARTIAL;
766
+ } else {
767
+ result = NEGOTIATION_RESULT.COMPATIBLE;
768
+ }
769
+ return {
770
+ result,
771
+ missingRequired,
772
+ supportedPreferred,
773
+ missingPreferred,
774
+ versionOk,
775
+ };
776
+ }
777
+
778
+ // ─── Stats V2 ────────────────────────────────────────────────────
779
+
780
+ export function getA2AStatsV2() {
781
+ const byStatus = {};
782
+ for (const v of Object.values(TASK_STATUS_V2)) byStatus[v] = 0;
783
+ let withDeadline = 0;
784
+ let canceled = 0;
785
+ for (const task of _v2Tasks.values()) {
786
+ byStatus[task.status] = (byStatus[task.status] || 0) + 1;
787
+ if (task.deadline) withDeadline += 1;
788
+ if (task.cancelReason) canceled += 1;
789
+ }
790
+ const cardsByStatus = {};
791
+ for (const v of Object.values(CARD_STATUS_V2)) cardsByStatus[v] = 0;
792
+ for (const c of _v2Cards.values()) {
793
+ cardsByStatus[c.status] = (cardsByStatus[c.status] || 0) + 1;
794
+ }
795
+ return {
796
+ tasks: {
797
+ total: _v2Tasks.size,
798
+ byStatus,
799
+ withDeadline,
800
+ canceledWithReason: canceled,
801
+ },
802
+ cards: {
803
+ tracked: _v2Cards.size,
804
+ byStatus: cardsByStatus,
805
+ },
806
+ subscriptions: {
807
+ legacy: _subscriptions.size,
808
+ typed: _v2TypedSubs.size,
809
+ },
810
+ };
811
+ }
812
+
813
+ /**
814
+ * Reset V2-only in-memory state (for tests).
815
+ */
816
+ export function _resetV2State() {
817
+ _v2Tasks.clear();
818
+ _v2Cards.clear();
819
+ _v2TypedSubs.clear();
820
+ }
821
+
822
+ export { _v2Tasks, _v2Cards, _v2TypedSubs };
@@ -503,3 +503,242 @@ export function deployApp(db, appId, options = {}) {
503
503
  const deployedAt = new Date().toISOString();
504
504
  return { appId, outputDir, files: fileNames, deployedAt };
505
505
  }
506
+
507
+ // ─── Phase 93 V2 surface (strictly additive) ───────────────────────────
508
+
509
+ export const COMPONENT_CATEGORY = Object.freeze({
510
+ INPUT: "input",
511
+ DISPLAY: "display",
512
+ CHART: "chart",
513
+ LAYOUT: "layout",
514
+ OVERLAY: "overlay",
515
+ });
516
+
517
+ export const DATASOURCE_TYPE = Object.freeze({
518
+ REST: "rest",
519
+ GRAPHQL: "graphql",
520
+ DATABASE: "database",
521
+ CSV: "csv",
522
+ });
523
+
524
+ export const APP_STATUS = Object.freeze({
525
+ DRAFT: "draft",
526
+ PUBLISHED: "published",
527
+ ARCHIVED: "archived",
528
+ });
529
+
530
+ const _allowedStatusTransitions = Object.freeze({
531
+ draft: new Set(["published", "archived"]),
532
+ published: new Set(["draft", "archived"]),
533
+ archived: new Set(["draft"]),
534
+ });
535
+
536
+ const _v2DataSources = new Map();
537
+ const _v2StatusHistory = new Map();
538
+
539
+ function _isValidStatusTransition(from, to) {
540
+ if (from === to) return true;
541
+ const allowed = _allowedStatusTransitions[from];
542
+ return !!(allowed && allowed.has(to));
543
+ }
544
+
545
+ export function listComponentsV2({ category } = {}) {
546
+ const all = listComponents();
547
+ if (!category) return all.map((c) => ({ ...c }));
548
+ const valid = Object.values(COMPONENT_CATEGORY);
549
+ if (!valid.includes(category)) {
550
+ throw new Error(
551
+ `Invalid category '${category}'. Expected one of ${valid.join(", ")}`,
552
+ );
553
+ }
554
+ return all.filter((c) => c.category === category).map((c) => ({ ...c }));
555
+ }
556
+
557
+ export function registerDataSourceV2(db, { appId, name, type, config = {} }) {
558
+ const valid = Object.values(DATASOURCE_TYPE);
559
+ if (!valid.includes(type)) {
560
+ throw new Error(
561
+ `Invalid datasource type '${type}'. Expected one of ${valid.join(", ")}`,
562
+ );
563
+ }
564
+ if (!appId) throw new Error("appId is required");
565
+ if (!name) throw new Error("name is required");
566
+
567
+ const result = addDataSource(db, appId, name, type, config);
568
+ _v2DataSources.set(result.id, { ...result, config, validated: false });
569
+ return result;
570
+ }
571
+
572
+ export function testDataSourceConnection(dataSourceId) {
573
+ const ds = _v2DataSources.get(dataSourceId);
574
+ if (!ds) {
575
+ return { dataSourceId, ok: false, reason: "datasource not found" };
576
+ }
577
+ const config = ds.config || {};
578
+ let ok = false;
579
+ let reason = "";
580
+ switch (ds.type) {
581
+ case DATASOURCE_TYPE.REST:
582
+ ok = typeof config.url === "string" && config.url.length > 0;
583
+ reason = ok ? "ok" : "missing url";
584
+ break;
585
+ case DATASOURCE_TYPE.GRAPHQL:
586
+ ok = typeof config.endpoint === "string" && config.endpoint.length > 0;
587
+ reason = ok ? "ok" : "missing endpoint";
588
+ break;
589
+ case DATASOURCE_TYPE.DATABASE:
590
+ ok = typeof config.host === "string" && config.host.length > 0;
591
+ reason = ok ? "ok" : "missing host";
592
+ break;
593
+ case DATASOURCE_TYPE.CSV:
594
+ ok = typeof config.path === "string" && config.path.length > 0;
595
+ reason = ok ? "ok" : "missing path";
596
+ break;
597
+ default:
598
+ reason = "unknown type";
599
+ }
600
+ if (ok) {
601
+ ds.validated = true;
602
+ _v2DataSources.set(dataSourceId, ds);
603
+ }
604
+ return { dataSourceId, type: ds.type, ok, reason };
605
+ }
606
+
607
+ export function updateAppStatus(db, { appId, status }) {
608
+ const valid = Object.values(APP_STATUS);
609
+ if (!valid.includes(status)) {
610
+ throw new Error(
611
+ `Invalid status '${status}'. Expected one of ${valid.join(", ")}`,
612
+ );
613
+ }
614
+ const app = getApp(db, appId);
615
+ if (!app) throw new Error(`App '${appId}' not found`);
616
+
617
+ const currentStatus =
618
+ app.status === "deployed" ? APP_STATUS.PUBLISHED : app.status;
619
+ if (!_isValidStatusTransition(currentStatus, status)) {
620
+ throw new Error(`Invalid status transition: ${currentStatus} → ${status}`);
621
+ }
622
+
623
+ db.prepare(
624
+ `UPDATE lowcode_apps SET status = ?, updated_at = datetime('now') WHERE id = ?`,
625
+ ).run(status, appId);
626
+ if (_apps.has(appId)) _apps.get(appId).status = status;
627
+
628
+ const hist = _v2StatusHistory.get(appId) || [];
629
+ hist.push({
630
+ from: currentStatus,
631
+ to: status,
632
+ at: new Date().toISOString(),
633
+ });
634
+ _v2StatusHistory.set(appId, hist);
635
+ return { appId, status, previous: currentStatus };
636
+ }
637
+
638
+ export function archiveApp(db, appId) {
639
+ return updateAppStatus(db, { appId, status: APP_STATUS.ARCHIVED });
640
+ }
641
+
642
+ export function getStatusHistory(appId) {
643
+ return (_v2StatusHistory.get(appId) || []).slice();
644
+ }
645
+
646
+ export function cloneApp(db, { sourceId, newName }) {
647
+ const source = getApp(db, sourceId);
648
+ if (!source) throw new Error(`App '${sourceId}' not found`);
649
+ const cloned = createApp(db, {
650
+ name: newName || `${source.name} (Copy)`,
651
+ description: source.description,
652
+ platform: source.platform,
653
+ design: source.design,
654
+ });
655
+ // Persist the copied design so version snapshot matches source
656
+ if (
657
+ source.design &&
658
+ source.design.components &&
659
+ source.design.components.length > 0
660
+ ) {
661
+ saveDesign(db, cloned.id, source.design);
662
+ }
663
+ return { sourceId, clonedId: cloned.id, name: cloned.name };
664
+ }
665
+
666
+ export function exportAppJSON(db, appId) {
667
+ const app = getApp(db, appId);
668
+ if (!app) throw new Error(`App '${appId}' not found`);
669
+ const legacy = exportApp(appId);
670
+ return {
671
+ schema: "chainlesschain.lowcode.v2",
672
+ exportedAt: new Date().toISOString(),
673
+ app,
674
+ dataSources: legacy.dataSources,
675
+ versions: legacy.versions,
676
+ };
677
+ }
678
+
679
+ export function importAppJSON(db, json) {
680
+ if (!json || typeof json !== "object") {
681
+ throw new Error("import payload must be a JSON object");
682
+ }
683
+ if (json.schema !== "chainlesschain.lowcode.v2") {
684
+ throw new Error(
685
+ `unsupported schema '${json.schema}'. Expected 'chainlesschain.lowcode.v2'`,
686
+ );
687
+ }
688
+ if (!json.app || !json.app.name) {
689
+ throw new Error("import payload missing app.name");
690
+ }
691
+ const imported = createApp(db, {
692
+ name: json.app.name,
693
+ description: json.app.description || "",
694
+ platform: json.app.platform || "web",
695
+ design: json.app.design || { components: [], layout: {} },
696
+ });
697
+ let dsCount = 0;
698
+ for (const ds of json.dataSources || []) {
699
+ if (!ds.type || !ds.name) continue;
700
+ if (!Object.values(DATASOURCE_TYPE).includes(ds.type)) continue;
701
+ registerDataSourceV2(db, {
702
+ appId: imported.id,
703
+ name: ds.name,
704
+ type: ds.type,
705
+ config: ds.config || {},
706
+ });
707
+ dsCount++;
708
+ }
709
+ return { importedId: imported.id, name: imported.name, dataSources: dsCount };
710
+ }
711
+
712
+ export function getLowcodeStatsV2(db) {
713
+ const rows = db.prepare(`SELECT status, platform FROM lowcode_apps`).all();
714
+ const byStatus = { draft: 0, published: 0, archived: 0, deployed: 0 };
715
+ const byPlatform = {};
716
+ for (const r of rows) {
717
+ const s = r.status || "draft";
718
+ byStatus[s] = (byStatus[s] || 0) + 1;
719
+ const p = r.platform || "web";
720
+ byPlatform[p] = (byPlatform[p] || 0) + 1;
721
+ }
722
+
723
+ const dsRows = db.prepare(`SELECT type FROM lowcode_datasources`).all();
724
+ const byDataSourceType = {};
725
+ for (const r of dsRows) {
726
+ byDataSourceType[r.type] = (byDataSourceType[r.type] || 0) + 1;
727
+ }
728
+
729
+ return {
730
+ totalApps: rows.length,
731
+ byStatus,
732
+ byPlatform,
733
+ dataSources: {
734
+ total: dsRows.length,
735
+ byType: byDataSourceType,
736
+ },
737
+ componentsAvailable: listComponents().length,
738
+ };
739
+ }
740
+
741
+ export function _resetV2State() {
742
+ _v2DataSources.clear();
743
+ _v2StatusHistory.clear();
744
+ }