@synap-core/workspace-templates 0.6.0 → 0.6.2

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.
package/dist/index.d.ts CHANGED
@@ -238,6 +238,16 @@ interface TemplatePlaybook {
238
238
  } | null;
239
239
  /** tool/skill keys this playbook grants. */
240
240
  grants?: string[];
241
+ /**
242
+ * The entity kind this playbook operates over, persisted to
243
+ * `playbooks.subject_profile`. Makes the playbook matchable by
244
+ * `playbooks.matchForEntity` (filters `subject_profile->>'profileSlug'`), so a
245
+ * captured entity of this kind can auto-launch the playbook as a session.
246
+ */
247
+ subjectProfile?: {
248
+ profileSlug: string;
249
+ filter?: Record<string, unknown>;
250
+ };
241
251
  status?: 'draft' | 'active' | 'paused';
242
252
  }
243
253
  interface TemplateIntegration {
@@ -269,6 +279,49 @@ interface TemplateExtendsRef {
269
279
  * `grants` → `operations`). Distinct from `extends` (which imports from a live
270
280
  * workspace, tRPC-only) — `dependencies` is template→template.
271
281
  */
282
+ /**
283
+ * PACKAGE_DEPENDENCY_KINDS — the dependency-kind vocabulary, declared ONCE.
284
+ *
285
+ * This tuple exists so consumers can IMPORT the vocabulary instead of
286
+ * hand-copying it. `TemplateDependency` is currently mirrored by hand in six
287
+ * other places, four of which carry a comment promising they match "VERBATIM so
288
+ * the wire shape never drifts" — a comment doing a type's job. A comment cannot
289
+ * fail a build; an import can. Those mirrors live in repos this package does not
290
+ * own, so this tuple does not fix them by existing — it makes the fix possible:
291
+ *
292
+ * - marketplace `types.ts:102`
293
+ * - onboarding `workspace-templates.ts:27`
294
+ * - synap-backend `package-definition.ts:204` (exports its own
295
+ * `PackageDependencyKind` — the closest peer)
296
+ * - hub-protocol REST `rest/packages.ts:140` (zod enum)
297
+ * - browser `InstallPackageDialog.tsx:262`
298
+ * - control-plane-api `routes/packages.ts:436` (zod enum)
299
+ *
300
+ * ⚠️ `'automation'` IS RETIRED VOCABULARY, deliberately still listed here.
301
+ * CP migration `0049_package_vocabulary` retired the `automation` package
302
+ * category in favour of `workflow` ("workflow — a triggered/sequenced run
303
+ * (absorbs 'automation' + 'playbook')", `PACKAGE_TYPES` in CP
304
+ * `db/schema/packages.ts`). A dependency's `kind` names the TYPE of package it
305
+ * points at, so this vocabulary should track `PACKAGE_TYPES` — meaning the live
306
+ * spelling is `workflow`, not `automation`.
307
+ *
308
+ * It is NOT renamed here, on purpose. This is latent, not live: 0/21 templates
309
+ * declare any `kind` other than the default `'workspace'`. And all four wire
310
+ * enums (CP + hub zod, above) still accept `'automation'` and do NOT accept
311
+ * `'workflow'` — so emitting `'workflow'` today would be rejected with a 400 at
312
+ * both doors, and dropping `'automation'` from this tuple would make any
313
+ * consumer that derives a zod enum from it start rejecting a value the wire
314
+ * still legally accepts (a published third-party package may carry it; CP's own
315
+ * schema permits it). Narrowing must therefore follow the wire, not lead it.
316
+ *
317
+ * ORDERED FIX (each step deployable alone, no flag day):
318
+ * 1. Widen the CP + hub zod enums to accept BOTH `'automation'` and `'workflow'`.
319
+ * 2. Add `'workflow'` here; mark `'automation'` deprecated; migrate any authored use.
320
+ * 3. Once no payload carries `'automation'`, drop it from the wire enums and this tuple.
321
+ */
322
+ declare const PACKAGE_DEPENDENCY_KINDS: readonly ["workspace", "capability", "automation"];
323
+ /** What a `TemplateDependency` points at. Derived — never re-declare the list. */
324
+ type PackageDependencyKind = (typeof PACKAGE_DEPENDENCY_KINDS)[number];
272
325
  interface TemplateDependency {
273
326
  /** Slug of the template/package this one depends on. */
274
327
  slug: string;
@@ -277,7 +330,7 @@ interface TemplateDependency {
277
330
  * dependencies can be `compose`d; `'capability'`/`'automation'` are always
278
331
  * `require`d present without merging.
279
332
  */
280
- kind?: 'workspace' | 'capability' | 'automation';
333
+ kind?: PackageDependencyKind;
281
334
  /**
282
335
  * How this template relates to the dependency:
283
336
  * - `'compose'`: this template is an OVERLAY on the dependency's workspace.
@@ -385,8 +438,171 @@ declare const WORKSPACE_TEMPLATES: Record<string, WorkspaceYaml>;
385
438
  type WorkspaceTemplateSlug = keyof typeof WORKSPACE_TEMPLATES;
386
439
  declare function getWorkspaceTemplate(slug: string): WorkspaceYaml | undefined;
387
440
  declare function listWorkspaceTemplates(): WorkspaceYaml[];
441
+ /**
442
+ * Public templates only (`meta.isPublic !== false`). This is a CONVENIENCE
443
+ * filter for the default picker — NOT a security boundary: the private YAML
444
+ * still ships in the published tarball, so anything that must not leak cannot
445
+ * rely on this. `listWorkspaceTemplates()` returns all of them (CLI, seeder,
446
+ * tripwire need the full set). Publicity is also expressed per-entry as
447
+ * `CatalogEntry.isPrivate` by `mergeCatalog()`, so a caller can pass ALL
448
+ * templates and filter at the presentation layer instead of choosing a list
449
+ * function up front — the merge door itself never filters (it stays pure).
450
+ */
388
451
  declare function listPublicTemplates(): WorkspaceYaml[];
389
452
 
453
+ /**
454
+ * mergeCatalog — the ONE merge door for the workspace-template catalog.
455
+ * ====================================================================
456
+ *
457
+ * Both marketplace entry points (browser + CLI) and the pod need to show ONE
458
+ * list of installable workspace templates drawn from TWO sources:
459
+ *
460
+ * 1. BUNDLED — the canonical `@synap-core/workspace-templates` package that
461
+ * ships inside every client (this package). Always present, zero network.
462
+ * 2. REMOTE — rows in the control-plane package registry (`GET /api/packages`),
463
+ * which includes both the official templates re-published by
464
+ * `seed-templates.ts` AND private / third-party packages that never shipped
465
+ * in the bundle.
466
+ *
467
+ * The two overlap: every official template exists in BOTH sources (once bundled,
468
+ * once seeded to the CP). Naively concatenating them shows 21 duplicate cards.
469
+ * `mergeCatalog` de-dupes them into ONE catalog with a deterministic winner.
470
+ *
471
+ * WHY THIS LIVES HERE (and is PURE):
472
+ * This is the ONLY module the root `pnpm-workspace.yaml` lets `synap-cli`,
473
+ * `browser`, `synap-control-plane-api`, AND `synap-backend/packages/api` all
474
+ * resolve. `@synap-core/marketplace` pulls React and is `private: true`, so the
475
+ * Node CLI cannot import it. Keep this file pure data — no fetch, no auth, no
476
+ * React, no Node-only APIs. Each entry point has a DIFFERENT transport (browser
477
+ * cookie/`cpFetch`, CLI Bearer, pod anonymous); a door that fetched would force
478
+ * ONE auth model on all three and become the next lossy seam. Transport lives
479
+ * in the consumers; this door only merges arrays they already fetched.
480
+ */
481
+
482
+ /**
483
+ * A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
484
+ * pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
485
+ * merges whatever it is given and does NOT filter (see the publicity note below).
486
+ */
487
+ type BundledEntry = WorkspaceYaml;
488
+ /**
489
+ * A control-plane registry row, mapped into the minimal shape the door needs.
490
+ * Consumers build this from a `MarketplacePackage` (browser) / the CP
491
+ * `GET /api/packages` JSON (CLI) — a thin, transport-specific projection.
492
+ *
493
+ * `definition` is carried OPAQUE: it is both (a) the install payload the consumer
494
+ * feeds back to the pod, and (b) the home of the ONE comparable version signal,
495
+ * `definition.sourcePackage.version` — the real npm semver of the published
496
+ * `@synap-core/workspace-templates` build that generated an official row
497
+ * (`seed-templates.ts` stamps it). The door only reads that one nested field; the
498
+ * rest of `definition` is passed through untouched.
499
+ */
500
+ interface RemoteEntry {
501
+ slug: string;
502
+ name: string;
503
+ description?: string;
504
+ /** CP package category / type (e.g. `"workspace"`, `"view"`). */
505
+ category?: string;
506
+ tags?: string[];
507
+ icon?: string | null;
508
+ /**
509
+ * `true` when the CP row is not publicly listed (`isPublic === false`). Browse
510
+ * rows leave this `undefined` BY CONSTRUCTION — the browse route hard-filters
511
+ * `WHERE is_public = true`, so every browse row is public; `undefined` ⇒ public,
512
+ * never "unknown-so-hide".
513
+ */
514
+ isPrivate?: boolean;
515
+ /**
516
+ * The CP `definition` jsonb — opaque install payload. The door reads only
517
+ * `definition.sourcePackage.version` (the winner signal). Absent on browse rows
518
+ * (the list route strips `definition`), which is fine: absence ⇒ bundle wins.
519
+ */
520
+ definition?: {
521
+ sourcePackage?: {
522
+ name?: string;
523
+ version?: string;
524
+ };
525
+ [k: string]: unknown;
526
+ };
527
+ }
528
+ /** Whether the caller could reach / authenticate against the remote source. */
529
+ type RemoteSourceStatus = "ok" | "unauthenticated" | "unreachable";
530
+ interface MergeCatalogInput {
531
+ /** Bundled templates (this package). Always "ok" — it is in-process data. */
532
+ bundled: BundledEntry[];
533
+ /** Already-fetched CP rows. Empty/omitted when the fetch failed. */
534
+ remote?: RemoteEntry[];
535
+ /**
536
+ * This client's bundled-package version (`package.json` `version`, live `0.6.0`).
537
+ * Compared against each remote row's `sourcePackage.version` to decide whether
538
+ * THIS client's bundle is stale. Callers pass the compiled-in package version.
539
+ */
540
+ bundleVersion: string;
541
+ /**
542
+ * The transport's verdict on the remote fetch. `"ok"` (default) ⇒ merge the
543
+ * `remote` array. `"unauthenticated"` / `"unreachable"` ⇒ ignore `remote` and
544
+ * surface the degradation in `sources.remote`, so the consumer can render
545
+ * "18 available — private ones need login" instead of FAILING OPEN EMPTY.
546
+ */
547
+ remoteStatus?: RemoteSourceStatus;
548
+ }
549
+ interface CatalogEntryBase {
550
+ slug: string;
551
+ name: string;
552
+ description?: string;
553
+ /** Package category / type. Bundled templates are always `"workspace"`. */
554
+ category?: string;
555
+ tags: string[];
556
+ icon?: string | null;
557
+ /** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
558
+ isPrivate: boolean;
559
+ }
560
+ /** A catalog entry backed by a bundled template. Install via `toWorkspaceDefinition()` / `toPackageDefinition()` on `template`. */
561
+ interface BundledCatalogEntry extends CatalogEntryBase {
562
+ source: "bundled";
563
+ template: WorkspaceYaml;
564
+ }
565
+ /** A catalog entry backed by a CP registry row. Install from `remote.definition`. */
566
+ interface RemoteCatalogEntry extends CatalogEntryBase {
567
+ source: "remote";
568
+ remote: RemoteEntry;
569
+ }
570
+ /** One merged card. `source` discriminates the install path; common fields feed a picker. */
571
+ type CatalogEntry = BundledCatalogEntry | RemoteCatalogEntry;
572
+ interface MergeCatalogResult {
573
+ entries: CatalogEntry[];
574
+ /**
575
+ * Per-source health. LOAD-BEARING, not decoration: it is what lets a consumer
576
+ * distinguish "remote returned nothing" from "remote unreachable" and avoid the
577
+ * FAILS-OPEN-EMPTY trap (agent sees empty catalog → thinks pod is empty → dupes).
578
+ */
579
+ sources: {
580
+ bundled: "ok";
581
+ remote: RemoteSourceStatus;
582
+ };
583
+ }
584
+ /**
585
+ * Merge bundled + remote template catalogs into ONE de-duplicated list.
586
+ *
587
+ * DEDUPE KEY = `slug` — NOT `slug@version`. This is forced by the live data, not
588
+ * a preference: every CP flagship row is hardcoded `version: "1.0.0"`
589
+ * (`seed-templates.ts`) and `TemplateMeta` carries NO version field at all, so a
590
+ * `slug@version` key would collide on nothing → 21 duplicate cards. The real
591
+ * per-build version lives only in `definition.sourcePackage.version`, which is
592
+ * the WINNER signal, never the identity key.
593
+ *
594
+ * CONFLICT RESOLUTION (remote slug already claimed by a bundled template):
595
+ * • remote `sourcePackage.version` strictly > `bundleVersion` → remote wins
596
+ * (this client is stale — the CP has a fresher official build).
597
+ * • otherwise (equal / older / no `sourcePackage`) → BUNDLE wins. The
598
+ * no-`sourcePackage` case is a third-party package SQUATTING an official slug;
599
+ * the bundle wins unconditionally (squatting defense).
600
+ *
601
+ * A remote slug that does NOT collide with any bundled template is a DISTINCT
602
+ * entry (a private or third-party package) and is always kept.
603
+ */
604
+ declare function mergeCatalog(input: MergeCatalogInput): MergeCatalogResult;
605
+
390
606
  /**
391
607
  * @synap-core/workspace-templates — Definition adapter
392
608
  *
@@ -399,6 +615,24 @@ declare function listPublicTemplates(): WorkspaceYaml[];
399
615
  * shape never drifts per call-site.
400
616
  */
401
617
 
618
+ /** A widget block as the wire expects it: `bentoLayout[]`, keyed by `widgetType`. */
619
+ interface BentoWidgetBlockWire {
620
+ widgetType: string;
621
+ pos: TemplateBentoBlock['pos'];
622
+ config?: Record<string, unknown>;
623
+ }
624
+ /** A view block as the wire expects it: `bentoViewBlocks[]`, keyed by `viewName`. */
625
+ interface BentoViewBlockWire {
626
+ kind: 'view';
627
+ viewName: string;
628
+ pos: TemplateBentoBlock['pos'];
629
+ overrides?: Record<string, unknown>;
630
+ }
631
+ interface SplitBentoBlocks {
632
+ bentoLayout: BentoWidgetBlockWire[];
633
+ bentoViewBlocks: BentoViewBlockWire[];
634
+ }
635
+ declare function splitBentoBlocks(blocks: TemplateBentoBlock[] | undefined): SplitBentoBlocks;
402
636
  /** The flat definition object accepted by createFromDefinition. */
403
637
  interface WorkspaceDefinitionInput {
404
638
  workspaceName: string;
@@ -412,7 +646,16 @@ interface WorkspaceDefinitionInput {
412
646
  displayTemplates?: WorkspaceYaml['displayTemplates'];
413
647
  layoutConfig: WorkspaceYaml['layout'] | undefined;
414
648
  bentoViewName?: string;
415
- bentoViewBlocks?: NonNullable<WorkspaceYaml['bento']>['blocks'];
649
+ /**
650
+ * WIDGET half of the authored bento union. The door declares this field
651
+ * (`workspaces.createFromDefinition` zod) and the pod maps it as widgets via
652
+ * `widget.widgetType`. It was missing from this interface entirely, which is
653
+ * why `toWorkspaceDefinition` had nowhere to put widgets and dumped the whole
654
+ * union into `bentoViewBlocks` — where every widget was silently filtered out.
655
+ */
656
+ bentoLayout?: BentoWidgetBlockWire[];
657
+ /** VIEW half of the authored bento union — resolved against the viewMap. */
658
+ bentoViewBlocks?: BentoViewBlockWire[];
416
659
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
417
660
  workspaceSubtype?: string;
418
661
  workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
@@ -477,7 +720,17 @@ interface PackageDefinitionOutput {
477
720
  suggestedRelations?: WorkspaceYaml['suggestedRelations'];
478
721
  displayTemplates?: WorkspaceYaml['displayTemplates'];
479
722
  entityLinks?: WorkspaceYaml['entityLinks'];
480
- bentoLayout?: NonNullable<WorkspaceYaml['bento']>['blocks'];
723
+ /** WIDGET half of the authored bento union — mapped by the pod via `widgetType`. */
724
+ bentoLayout?: BentoWidgetBlockWire[];
725
+ /**
726
+ * VIEW half of the authored bento union. Widgets go to `bentoLayout`, views
727
+ * here — the pod splits by field (create-workspace-from-definition.ts:1373
728
+ * reads `bentoViewBlocks`), so both wire fields must be carried. The apply
729
+ * door's `PackageApplySchema` now declares this field (it is a plain
730
+ * `z.object`, so an undeclared field would be silently stripped) — the inbound
731
+ * seam is closed to match this outbound emit.
732
+ */
733
+ bentoViewBlocks?: BentoViewBlockWire[];
481
734
  bentoViewName?: string;
482
735
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
483
736
  layoutConfig?: WorkspaceYaml['layout'];
@@ -495,4 +748,4 @@ interface PackageDefinitionOutput {
495
748
  */
496
749
  declare function toPackageDefinition(slugOrYaml: string | WorkspaceYaml): PackageDefinitionOutput;
497
750
 
498
- export { type PackageDefinitionOutput, type TemplateAppKind, type TemplateAppRef, type TemplateAutomation, type TemplateBentoBlock, type TemplateDefaultSource, type TemplateDependency, type TemplateDisplayTemplate, type TemplateEntityLink, type TemplateExtendsRef, type TemplateIntegration, type TemplateLayoutConfig, type TemplateMeta, type TemplateOnboarding, type TemplateOnboardingCollectTarget, type TemplatePlaybook, type TemplatePlaybookParam, type TemplateProfile, type TemplateProfileEntityBentoTemplates, type TemplateProperty, type TemplateSeedEntity, type TemplateSidebarItem, type TemplateSidebarSurface, type TemplateSourceRole, type TemplateSuggestedRelation, type TemplateView, type ToDefinitionResult, WORKSPACE_TEMPLATES, type WorkspaceDefinitionInput, type WorkspaceTemplateSlug, type WorkspaceYaml, getWorkspaceTemplate, listPublicTemplates, listWorkspaceTemplates, toPackageDefinition, toWorkspaceDefinition };
751
+ export { type BentoViewBlockWire, type BentoWidgetBlockWire, type BundledCatalogEntry, type BundledEntry, type CatalogEntry, type MergeCatalogInput, type MergeCatalogResult, PACKAGE_DEPENDENCY_KINDS, type PackageDefinitionOutput, type PackageDependencyKind, type RemoteCatalogEntry, type RemoteEntry, type RemoteSourceStatus, type SplitBentoBlocks, type TemplateAppKind, type TemplateAppRef, type TemplateAutomation, type TemplateBentoBlock, type TemplateDefaultSource, type TemplateDependency, type TemplateDisplayTemplate, type TemplateEntityLink, type TemplateExtendsRef, type TemplateIntegration, type TemplateLayoutConfig, type TemplateMeta, type TemplateOnboarding, type TemplateOnboardingCollectTarget, type TemplatePlaybook, type TemplatePlaybookParam, type TemplateProfile, type TemplateProfileEntityBentoTemplates, type TemplateProperty, type TemplateSeedEntity, type TemplateSidebarItem, type TemplateSidebarSurface, type TemplateSourceRole, type TemplateSuggestedRelation, type TemplateView, type ToDefinitionResult, WORKSPACE_TEMPLATES, type WorkspaceDefinitionInput, type WorkspaceTemplateSlug, type WorkspaceYaml, getWorkspaceTemplate, listPublicTemplates, listWorkspaceTemplates, mergeCatalog, splitBentoBlocks, toPackageDefinition, toWorkspaceDefinition };