@synap-core/workspace-templates 0.7.1 → 0.7.3
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 +266 -8
- package/dist/index.js +2314 -1440
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/agent-fleet.yaml +666 -108
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +127 -12
- package/src/builder.yaml +116 -12
- package/src/business-developer.yaml +110 -18
- package/src/catalog.test.ts +2 -2
- package/src/catalog.ts +13 -3
- package/src/communication.yaml +23 -453
- package/src/composition-graph.test.ts +28 -2
- package/src/concept-guard.test.ts +176 -0
- package/src/concept-guard.ts +219 -0
- package/src/content-os.yaml +103 -10
- package/src/content-studio.yaml +1 -0
- package/src/crm.yaml +1 -0
- package/src/define.ts +8 -8
- package/src/dev-dashboard.yaml +43 -9
- package/src/domains.test.ts +117 -0
- package/src/domains.ts +102 -0
- package/src/ecosystem.yaml +25 -5
- package/src/enterprise-os.yaml +1 -0
- package/src/finance.yaml +46 -5
- package/src/foundation.yaml +13 -1
- package/src/generate.d.mts +15 -0
- package/src/generate.mjs +86 -0
- package/src/grants.yaml +1 -0
- package/src/hr.yaml +61 -8
- package/src/index.ts +40 -0
- package/src/internal-runbook.yaml +36 -5
- package/src/legal.yaml +97 -12
- package/src/life-os.yaml +188 -27
- package/src/marketing.yaml +15 -2
- package/src/meetings-knowledge.yaml +143 -23
- package/src/operations.yaml +347 -0
- package/src/outreach-comms.yaml +11 -3
- package/src/project-management.yaml +840 -111
- package/src/research-base.yaml +516 -0
- package/src/research-decision.yaml +158 -0
- package/src/shared-role-ssot.test.ts +268 -0
- package/src/social.yaml +105 -37
- package/src/templates-freshness.test.ts +55 -0
- package/src/templates.ts +4620 -4278
- package/src/the-arch.yaml +1 -0
- package/src/types.ts +9 -0
- package/src/validate.test.ts +81 -0
- package/src/validate.ts +561 -0
- package/src/personal.yaml +0 -177
package/src/dev-dashboard.yaml
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
meta:
|
|
9
9
|
slug: dev-dashboard
|
|
10
|
+
domain: build
|
|
10
11
|
name: "Synap Dev — Infrastructure Command Center"
|
|
11
12
|
description: "Internal DevOps dashboard for monitoring Synap infrastructure. Tracks services, vendors, and environments with health status, architecture flow map, and quick access to admin panels."
|
|
12
13
|
icon: server
|
|
@@ -123,7 +124,8 @@ profiles:
|
|
|
123
124
|
label: Vendor Type
|
|
124
125
|
valueType: string
|
|
125
126
|
inputType: select
|
|
126
|
-
enumValues:
|
|
127
|
+
enumValues:
|
|
128
|
+
[Hosting, AI, Auth, Database, Monitoring, CI/CD, DNS, Email, Other]
|
|
127
129
|
- slug: vendor-url
|
|
128
130
|
label: Vendor URL
|
|
129
131
|
valueType: string
|
|
@@ -271,7 +273,8 @@ profiles:
|
|
|
271
273
|
label: Type
|
|
272
274
|
valueType: string
|
|
273
275
|
inputType: select
|
|
274
|
-
enumValues:
|
|
276
|
+
enumValues:
|
|
277
|
+
[api_key, ssh_key, password, token, certificate, env_variable, other]
|
|
275
278
|
placeholder: "Select credential type"
|
|
276
279
|
- slug: service-name
|
|
277
280
|
label: Service
|
|
@@ -418,7 +421,14 @@ views:
|
|
|
418
421
|
type: table
|
|
419
422
|
scopeProfileSlug: service
|
|
420
423
|
config:
|
|
421
|
-
visibleColumns:
|
|
424
|
+
visibleColumns:
|
|
425
|
+
[
|
|
426
|
+
service-type,
|
|
427
|
+
environment,
|
|
428
|
+
deployment-branch,
|
|
429
|
+
last-deploy,
|
|
430
|
+
health-status,
|
|
431
|
+
]
|
|
422
432
|
sortBy: last-deploy
|
|
423
433
|
sortDirection: desc
|
|
424
434
|
|
|
@@ -854,12 +864,36 @@ bento:
|
|
|
854
864
|
config:
|
|
855
865
|
title: Quick Links
|
|
856
866
|
links:
|
|
857
|
-
- {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
- {
|
|
867
|
+
- {
|
|
868
|
+
label: GitHub,
|
|
869
|
+
url: "https://github.com/synapsh",
|
|
870
|
+
color: "#24292E",
|
|
871
|
+
}
|
|
872
|
+
- {
|
|
873
|
+
label: Hetzner Console,
|
|
874
|
+
url: "https://console.hetzner.cloud",
|
|
875
|
+
color: "#D50C2D",
|
|
876
|
+
}
|
|
877
|
+
- {
|
|
878
|
+
label: Vercel Dashboard,
|
|
879
|
+
url: "https://vercel.com/dashboard",
|
|
880
|
+
color: "#000000",
|
|
881
|
+
}
|
|
882
|
+
- {
|
|
883
|
+
label: Ory Console,
|
|
884
|
+
url: "https://console.ory.sh",
|
|
885
|
+
color: "#5528FF",
|
|
886
|
+
}
|
|
887
|
+
- {
|
|
888
|
+
label: Cloudflare,
|
|
889
|
+
url: "https://dash.cloudflare.com",
|
|
890
|
+
color: "#F38020",
|
|
891
|
+
}
|
|
892
|
+
- {
|
|
893
|
+
label: OpenRouter,
|
|
894
|
+
url: "https://openrouter.ai/dashboard",
|
|
895
|
+
color: "#6366F1",
|
|
896
|
+
}
|
|
863
897
|
- kind: widget
|
|
864
898
|
widgetType: entity-list
|
|
865
899
|
pos: { x: 0, y: 7, w: 6, h: 5 }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the domain taxonomy — the "works for any template" contract.
|
|
3
|
+
* Run: `npx vitest run packages/workspace-templates` (from the repo root).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
groupByDomain,
|
|
9
|
+
domainLabel,
|
|
10
|
+
TEMPLATE_DOMAINS,
|
|
11
|
+
OTHER_DOMAIN_ID,
|
|
12
|
+
isKnownDomain,
|
|
13
|
+
} from "./domains.js";
|
|
14
|
+
|
|
15
|
+
type Row = { slug: string; domain?: string };
|
|
16
|
+
const domainOf = (r: Row) => r.domain;
|
|
17
|
+
|
|
18
|
+
describe("domainLabel", () => {
|
|
19
|
+
it("returns the registry label for a known domain", () => {
|
|
20
|
+
expect(domainLabel("revenue")).toBe("Sales & Marketing");
|
|
21
|
+
});
|
|
22
|
+
it("returns 'More' for a missing domain", () => {
|
|
23
|
+
expect(domainLabel(undefined)).toBe("More");
|
|
24
|
+
expect(domainLabel(null)).toBe("More");
|
|
25
|
+
expect(domainLabel("")).toBe("More");
|
|
26
|
+
});
|
|
27
|
+
it("title-cases an unknown-but-declared domain so it still reads", () => {
|
|
28
|
+
expect(domainLabel("supply-chain")).toBe("Supply Chain");
|
|
29
|
+
expect(domainLabel("bizops")).toBe("Bizops");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("groupByDomain — works for ANY template", () => {
|
|
34
|
+
it("orders known domains by the registry, never alphabetically", () => {
|
|
35
|
+
const rows: Row[] = [
|
|
36
|
+
{ slug: "a", domain: "knowledge" }, // registry order 5
|
|
37
|
+
{ slug: "b", domain: "strategy" }, // registry order 0
|
|
38
|
+
{ slug: "c", domain: "content" }, // registry order 2
|
|
39
|
+
];
|
|
40
|
+
const groups = groupByDomain(rows, domainOf);
|
|
41
|
+
expect(groups.map((g) => g.id)).toEqual(["strategy", "content", "knowledge"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("preserves input order WITHIN a section", () => {
|
|
45
|
+
const rows: Row[] = [
|
|
46
|
+
{ slug: "z", domain: "content" },
|
|
47
|
+
{ slug: "a", domain: "content" },
|
|
48
|
+
];
|
|
49
|
+
const [content] = groupByDomain(rows, domainOf);
|
|
50
|
+
expect(content.items.map((r) => r.slug)).toEqual(["z", "a"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("places an unknown-but-declared domain AFTER all known ones", () => {
|
|
54
|
+
const rows: Row[] = [
|
|
55
|
+
{ slug: "x", domain: "supply-chain" }, // unknown
|
|
56
|
+
{ slug: "y", domain: "build" }, // known
|
|
57
|
+
];
|
|
58
|
+
const groups = groupByDomain(rows, domainOf);
|
|
59
|
+
expect(groups.map((g) => g.id)).toEqual(["build", "supply-chain"]);
|
|
60
|
+
expect(groups[1].label).toBe("Supply Chain");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sorts multiple unknown domains alphabetically, before 'other'", () => {
|
|
64
|
+
const rows: Row[] = [
|
|
65
|
+
{ slug: "1", domain: "zeta" },
|
|
66
|
+
{ slug: "2" }, // missing → other
|
|
67
|
+
{ slug: "3", domain: "alpha" },
|
|
68
|
+
{ slug: "4", domain: "revenue" }, // known
|
|
69
|
+
];
|
|
70
|
+
const groups = groupByDomain(rows, domainOf);
|
|
71
|
+
expect(groups.map((g) => g.id)).toEqual([
|
|
72
|
+
"revenue",
|
|
73
|
+
"alpha",
|
|
74
|
+
"zeta",
|
|
75
|
+
OTHER_DOMAIN_ID,
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("routes a MISSING domain into the 'More' bucket, always LAST", () => {
|
|
80
|
+
const rows: Row[] = [
|
|
81
|
+
{ slug: "1" },
|
|
82
|
+
{ slug: "2", domain: "strategy" },
|
|
83
|
+
];
|
|
84
|
+
const groups = groupByDomain(rows, domainOf);
|
|
85
|
+
expect(groups[groups.length - 1].id).toBe(OTHER_DOMAIN_ID);
|
|
86
|
+
expect(groups[groups.length - 1].label).toBe("More");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("NEVER drops an item — count in equals count out", () => {
|
|
90
|
+
const rows: Row[] = [
|
|
91
|
+
{ slug: "1", domain: "build" },
|
|
92
|
+
{ slug: "2", domain: "made-up" },
|
|
93
|
+
{ slug: "3" },
|
|
94
|
+
{ slug: "4", domain: "build" },
|
|
95
|
+
];
|
|
96
|
+
const total = groupByDomain(rows, domainOf).reduce(
|
|
97
|
+
(n, g) => n + g.items.length,
|
|
98
|
+
0
|
|
99
|
+
);
|
|
100
|
+
expect(total).toBe(rows.length);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("omits empty sections", () => {
|
|
104
|
+
const groups = groupByDomain([{ slug: "1", domain: "build" }], domainOf);
|
|
105
|
+
expect(groups).toHaveLength(1);
|
|
106
|
+
expect(groups[0].id).toBe("build");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("isKnownDomain", () => {
|
|
111
|
+
it("recognises every registry id and rejects the rest", () => {
|
|
112
|
+
for (const d of TEMPLATE_DOMAINS) expect(isKnownDomain(d.id)).toBe(true);
|
|
113
|
+
expect(isKnownDomain("made-up")).toBe(false);
|
|
114
|
+
expect(isKnownDomain(undefined)).toBe(false);
|
|
115
|
+
expect(isKnownDomain(OTHER_DOMAIN_ID)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/domains.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template DOMAIN taxonomy — the authored, cross-surface grouping that sections
|
|
3
|
+
* the marketplace (CLI `market list`, browser New Space + catalog, marketplace).
|
|
4
|
+
*
|
|
5
|
+
* WHY A FIELD, NOT DERIVED FROM TAGS. Grouping is a property of the template, so
|
|
6
|
+
* it lives ON the template (`meta.domain`) — authored once, read by every
|
|
7
|
+
* surface. The alternative was a hardcoded slug→group map (or a tag heuristic)
|
|
8
|
+
* inside each consumer; that map drifts the instant a template is added, and no
|
|
9
|
+
* one authoring a template can see it. Same lesson as the vocabulary work: a
|
|
10
|
+
* declared relationship should live where it can be verified, not re-guessed.
|
|
11
|
+
*
|
|
12
|
+
* NOT `category`. The catalog already uses `category` for the PACKAGE TYPE
|
|
13
|
+
* (`workspace`/`capability`/`skill`/…). `domain` is the human subject area.
|
|
14
|
+
*
|
|
15
|
+
* WORKS FOR ANY TEMPLATE. A template may declare a domain absent from this
|
|
16
|
+
* registry, or none at all — `groupByDomain` never drops one. Known domains
|
|
17
|
+
* render first in registry order; an unknown-but-declared domain renders after
|
|
18
|
+
* them (its id title-cased); a missing domain falls into the `other` bucket,
|
|
19
|
+
* always last. So a NEW template appears correctly without touching this file —
|
|
20
|
+
* adding it here only controls a domain's ORDER and LABEL.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface TemplateDomain {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const OTHER_DOMAIN_ID = "other";
|
|
29
|
+
export const OTHER_DOMAIN_LABEL = "More";
|
|
30
|
+
|
|
31
|
+
/** Known domains, in display order (the order sections appear in a listing). */
|
|
32
|
+
export const TEMPLATE_DOMAINS: readonly TemplateDomain[] = [
|
|
33
|
+
{ id: "strategy", label: "Strategy & Market" },
|
|
34
|
+
{ id: "revenue", label: "Sales & Marketing" },
|
|
35
|
+
{ id: "content", label: "Content & Social" },
|
|
36
|
+
{ id: "company", label: "Company & Operations" },
|
|
37
|
+
{ id: "build", label: "Build & Operate" },
|
|
38
|
+
{ id: "knowledge", label: "Knowledge & Research" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const KNOWN = new Map(
|
|
42
|
+
TEMPLATE_DOMAINS.map((d, i) => [d.id, { label: d.label, order: i }] as const)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Every valid domain id the registry knows about (excludes `other`). */
|
|
46
|
+
export function isKnownDomain(id: string | undefined | null): boolean {
|
|
47
|
+
return id != null && KNOWN.has(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Display label for a domain id. Missing → "More"; unknown-declared → title-cased id. */
|
|
51
|
+
export function domainLabel(id: string | undefined | null): string {
|
|
52
|
+
if (!id) return OTHER_DOMAIN_LABEL;
|
|
53
|
+
const known = KNOWN.get(id);
|
|
54
|
+
if (known) return known.label;
|
|
55
|
+
return id
|
|
56
|
+
.replace(/[-_]/g, " ")
|
|
57
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DomainGroup<T> {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
items: T[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Partition `items` into ordered domain sections: known domains first (registry
|
|
68
|
+
* order), then unknown-but-declared domains (alphabetical), then the missing
|
|
69
|
+
* "More" bucket, always last. Empty sections are omitted; nothing is dropped.
|
|
70
|
+
* Item order WITHIN a section is preserved from the input.
|
|
71
|
+
*/
|
|
72
|
+
export function groupByDomain<T>(
|
|
73
|
+
items: readonly T[],
|
|
74
|
+
domainOf: (item: T) => string | undefined | null
|
|
75
|
+
): DomainGroup<T>[] {
|
|
76
|
+
const buckets = new Map<string, T[]>();
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const id = domainOf(item) || OTHER_DOMAIN_ID;
|
|
79
|
+
const bucket = buckets.get(id);
|
|
80
|
+
if (bucket) bucket.push(item);
|
|
81
|
+
else buckets.set(id, [item]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rank = (id: string): number => {
|
|
85
|
+
if (id === OTHER_DOMAIN_ID) return Number.MAX_SAFE_INTEGER;
|
|
86
|
+
const known = KNOWN.get(id);
|
|
87
|
+
return known ? known.order : 1_000_000; // unknown-declared: after known, before "other"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return [...buckets.entries()]
|
|
91
|
+
.sort(([a], [b]) => {
|
|
92
|
+
const ra = rank(a);
|
|
93
|
+
const rb = rank(b);
|
|
94
|
+
if (ra !== rb) return ra - rb;
|
|
95
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
96
|
+
})
|
|
97
|
+
.map(([id, groupItems]) => ({
|
|
98
|
+
id,
|
|
99
|
+
label: id === OTHER_DOMAIN_ID ? OTHER_DOMAIN_LABEL : domainLabel(id),
|
|
100
|
+
items: groupItems,
|
|
101
|
+
}));
|
|
102
|
+
}
|
package/src/ecosystem.yaml
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
meta:
|
|
10
10
|
slug: ecosystem
|
|
11
|
+
domain: strategy
|
|
11
12
|
name: Ecosystem
|
|
12
13
|
description: Map the market you operate in — the partners, competitors, and players, the segments and trends, and the relationships between them.
|
|
13
14
|
icon: radar
|
|
@@ -170,7 +171,8 @@ profiles:
|
|
|
170
171
|
label: Type
|
|
171
172
|
valueType: string
|
|
172
173
|
inputType: select
|
|
173
|
-
enumValues:
|
|
174
|
+
enumValues:
|
|
175
|
+
[raise, partnership, launch, acquisition, listing, conference, other]
|
|
174
176
|
- slug: event-date
|
|
175
177
|
label: Date
|
|
176
178
|
valueType: date
|
|
@@ -390,9 +392,19 @@ playbooks:
|
|
|
390
392
|
profileSlug: competitor
|
|
391
393
|
params:
|
|
392
394
|
- { name: competitor, type: entity, label: Competitor, required: true }
|
|
393
|
-
- {
|
|
395
|
+
- {
|
|
396
|
+
name: focus,
|
|
397
|
+
type: text,
|
|
398
|
+
label: Focus area,
|
|
399
|
+
defaultValue: "product, pricing, and positioning",
|
|
400
|
+
}
|
|
394
401
|
- { name: companyContext, type: text, label: Our company context }
|
|
395
|
-
- {
|
|
402
|
+
- {
|
|
403
|
+
name: outputTypes,
|
|
404
|
+
type: text,
|
|
405
|
+
label: Outputs,
|
|
406
|
+
defaultValue: "a head-to-head comparison summary and an updated competitor profile",
|
|
407
|
+
}
|
|
396
408
|
executor: is-agent
|
|
397
409
|
# A radar MONITOR: bound to the competitor KIND + a schedule → the backing
|
|
398
410
|
# cron automation fans out (query→loop→playbook_run) and runs this once per
|
|
@@ -423,7 +435,12 @@ playbooks:
|
|
|
423
435
|
angles you covered, and why each new one matters.
|
|
424
436
|
schedule: { cron: "0 9 * * 1", enabled: false }
|
|
425
437
|
params:
|
|
426
|
-
- {
|
|
438
|
+
- {
|
|
439
|
+
name: ourSpace,
|
|
440
|
+
type: text,
|
|
441
|
+
label: Our market / category,
|
|
442
|
+
required: true,
|
|
443
|
+
}
|
|
427
444
|
executor: is-agent
|
|
428
445
|
grants:
|
|
429
446
|
- exa_search
|
|
@@ -536,7 +553,10 @@ layout:
|
|
|
536
553
|
icon: database
|
|
537
554
|
|
|
538
555
|
bento:
|
|
539
|
-
|
|
556
|
+
# Distinct from the "Ecosystem Map" graph view it embeds — the bento is created
|
|
557
|
+
# as its OWN view row, so reusing an embedded view's name puts two identically
|
|
558
|
+
# named views in the switcher.
|
|
559
|
+
viewName: Ecosystem Home
|
|
540
560
|
blocks:
|
|
541
561
|
- kind: view
|
|
542
562
|
viewName: Ecosystem Map
|
package/src/enterprise-os.yaml
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
meta:
|
|
22
22
|
slug: enterprise-os
|
|
23
|
+
domain: company
|
|
23
24
|
name: Enterprise OS
|
|
24
25
|
description: Stand up a company's public operating core in one install — strategy, market, operations, and pipeline — plus a command tower to track objectives across them.
|
|
25
26
|
icon: layout-dashboard
|
package/src/finance.yaml
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
meta:
|
|
7
7
|
slug: finance
|
|
8
|
+
domain: company
|
|
8
9
|
name: Finance
|
|
9
10
|
description: The company's money model — accounts, receivables, spend, revenue streams, budgets, and runway.
|
|
10
11
|
icon: banknote
|
|
@@ -123,7 +124,17 @@ profiles:
|
|
|
123
124
|
label: Category
|
|
124
125
|
valueType: string
|
|
125
126
|
inputType: select
|
|
126
|
-
enumValues:
|
|
127
|
+
enumValues:
|
|
128
|
+
[
|
|
129
|
+
payroll,
|
|
130
|
+
software,
|
|
131
|
+
marketing,
|
|
132
|
+
rent,
|
|
133
|
+
travel,
|
|
134
|
+
contractors,
|
|
135
|
+
taxes,
|
|
136
|
+
other,
|
|
137
|
+
]
|
|
127
138
|
constraints: { defaultValue: other }
|
|
128
139
|
- slug: status
|
|
129
140
|
label: Status
|
|
@@ -245,7 +256,18 @@ profiles:
|
|
|
245
256
|
label: Metric
|
|
246
257
|
valueType: string
|
|
247
258
|
inputType: select
|
|
248
|
-
enumValues:
|
|
259
|
+
enumValues:
|
|
260
|
+
[
|
|
261
|
+
runway,
|
|
262
|
+
burn-rate,
|
|
263
|
+
mrr,
|
|
264
|
+
arr,
|
|
265
|
+
cash-balance,
|
|
266
|
+
gross-margin,
|
|
267
|
+
cac,
|
|
268
|
+
ltv,
|
|
269
|
+
other,
|
|
270
|
+
]
|
|
249
271
|
constraints: { defaultValue: runway }
|
|
250
272
|
- slug: value
|
|
251
273
|
label: Value
|
|
@@ -292,7 +314,8 @@ views:
|
|
|
292
314
|
defaultView: true
|
|
293
315
|
description: Accounts receivable — who owes us, how much, and when it's due.
|
|
294
316
|
config:
|
|
295
|
-
columns:
|
|
317
|
+
columns:
|
|
318
|
+
[title, invoice-number, status, amount, customer, due-date, paid-date]
|
|
296
319
|
defaultSort:
|
|
297
320
|
field: due-date
|
|
298
321
|
direction: asc
|
|
@@ -311,7 +334,16 @@ views:
|
|
|
311
334
|
scopeProfileSlug: revenue-stream
|
|
312
335
|
description: Revenue streams with MRR/ARR and billing cadence.
|
|
313
336
|
config:
|
|
314
|
-
columns:
|
|
337
|
+
columns:
|
|
338
|
+
[
|
|
339
|
+
title,
|
|
340
|
+
stream-type,
|
|
341
|
+
status,
|
|
342
|
+
mrr,
|
|
343
|
+
arr,
|
|
344
|
+
billing-cadence,
|
|
345
|
+
customer-segment,
|
|
346
|
+
]
|
|
315
347
|
defaultSort:
|
|
316
348
|
field: mrr
|
|
317
349
|
direction: desc
|
|
@@ -321,7 +353,16 @@ views:
|
|
|
321
353
|
scopeProfileSlug: budget
|
|
322
354
|
description: Planned spend against actuals, by category and period.
|
|
323
355
|
config:
|
|
324
|
-
columns:
|
|
356
|
+
columns:
|
|
357
|
+
[
|
|
358
|
+
title,
|
|
359
|
+
category,
|
|
360
|
+
period,
|
|
361
|
+
fiscal-year,
|
|
362
|
+
budgeted-amount,
|
|
363
|
+
actual-amount,
|
|
364
|
+
status,
|
|
365
|
+
]
|
|
325
366
|
- name: Runway & Metrics
|
|
326
367
|
slug: runway-metrics
|
|
327
368
|
type: table
|
package/src/foundation.yaml
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
meta:
|
|
8
8
|
slug: foundation
|
|
9
|
+
domain: strategy
|
|
9
10
|
name: Foundation
|
|
10
11
|
description: The strategic DNA — mission, audience, positioning, and principles — that every workspace inherits.
|
|
11
12
|
icon: compass
|
|
@@ -455,7 +456,18 @@ views:
|
|
|
455
456
|
- name: Strategy Map
|
|
456
457
|
slug: strategy-map
|
|
457
458
|
type: graph
|
|
458
|
-
scopeProfileSlugs:
|
|
459
|
+
scopeProfileSlugs:
|
|
460
|
+
[
|
|
461
|
+
mission,
|
|
462
|
+
vision,
|
|
463
|
+
value,
|
|
464
|
+
audience,
|
|
465
|
+
pain-point,
|
|
466
|
+
positioning,
|
|
467
|
+
narrative,
|
|
468
|
+
differentiator,
|
|
469
|
+
non-goal,
|
|
470
|
+
]
|
|
459
471
|
description: The strategic DNA as a connected graph.
|
|
460
472
|
|
|
461
473
|
# Schema-level relations — give the Strategy Map and the relation picker
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Types for `generate.mjs` — the shared renderer for `src/templates.ts`. */
|
|
2
|
+
|
|
3
|
+
export interface RawTemplateEntry {
|
|
4
|
+
/** Source file name, e.g. `business-developer.yaml`. */
|
|
5
|
+
file: string;
|
|
6
|
+
/** `meta.slug` as declared in the YAML. */
|
|
7
|
+
slug: string;
|
|
8
|
+
/** The parsed YAML document, untyped on purpose (build-time shape). */
|
|
9
|
+
data: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare const SRC_DIR: string;
|
|
13
|
+
export declare const TEMPLATES_FILE: string;
|
|
14
|
+
export declare function readYamlTemplates(): RawTemplateEntry[];
|
|
15
|
+
export declare function renderTemplatesModule(templates: RawTemplateEntry[]): string;
|
package/src/generate.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE renderer for the generated `src/templates.ts` corpus.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS (read before inlining it back into build.mjs):
|
|
5
|
+
* `src/templates.ts` is auto-generated from `src/*.yaml` AND committed to git
|
|
6
|
+
* (it has to be — `browser/tsconfig.json` aliases this package straight at
|
|
7
|
+
* `src/`, with no build step, and synap-backend's
|
|
8
|
+
* `__tripwires__/template-shared-role-ssot.test.ts` reads the file by path).
|
|
9
|
+
* A committed artifact can go stale: edit a YAML, run `pnpm test`, and every
|
|
10
|
+
* template-corpus check — including `concept-guard` — passes against
|
|
11
|
+
* vocabulary that no longer exists on disk. Only `pnpm build` regenerated.
|
|
12
|
+
*
|
|
13
|
+
* So the render logic lives HERE, and two callers share it:
|
|
14
|
+
* • `build.mjs` — validates, renders, WRITES the file.
|
|
15
|
+
* • `src/templates-freshness.test.ts` — renders in memory and asserts the
|
|
16
|
+
* committed file matches byte-for-byte. Runs in `pnpm test`, i.e. in CI.
|
|
17
|
+
*
|
|
18
|
+
* Duplicating the template string into the test would recreate the same class
|
|
19
|
+
* of drift one level up, so don't.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
23
|
+
import { dirname, join } from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import yaml from 'js-yaml';
|
|
26
|
+
|
|
27
|
+
export const SRC_DIR = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
export const TEMPLATES_FILE = join(SRC_DIR, 'templates.ts');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read + parse every `src/*.yaml`, in the same (readdir) order build.mjs used.
|
|
32
|
+
* No validation here — validation stays in build.mjs, which is the writer.
|
|
33
|
+
*/
|
|
34
|
+
export function readYamlTemplates() {
|
|
35
|
+
return readdirSync(SRC_DIR)
|
|
36
|
+
.filter((f) => f.endsWith('.yaml'))
|
|
37
|
+
.map((file) => {
|
|
38
|
+
const parsed = yaml.load(readFileSync(join(SRC_DIR, file), 'utf-8'));
|
|
39
|
+
return { file, slug: parsed?.meta?.slug, data: parsed };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Render the full text of `src/templates.ts` for a corpus. Pure. */
|
|
44
|
+
export function renderTemplatesModule(templates) {
|
|
45
|
+
const data = JSON.stringify(
|
|
46
|
+
Object.fromEntries(templates.map((t) => [t.slug, t.data])),
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return `// ═══════════════════════════════════════════════════════════════════════
|
|
52
|
+
// AUTO-GENERATED from YAML source files in src/*.yaml
|
|
53
|
+
// DO NOT EDIT — run \`node build.mjs\` or \`pnpm build\` to regenerate
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
import type { WorkspaceYaml } from './types.js';
|
|
57
|
+
|
|
58
|
+
const DATA = ${data} as unknown as Record<string, WorkspaceYaml>;
|
|
59
|
+
|
|
60
|
+
export const WORKSPACE_TEMPLATES: Record<string, WorkspaceYaml> = DATA;
|
|
61
|
+
|
|
62
|
+
export type WorkspaceTemplateSlug = keyof typeof WORKSPACE_TEMPLATES;
|
|
63
|
+
|
|
64
|
+
export function getWorkspaceTemplate(slug: string): WorkspaceYaml | undefined {
|
|
65
|
+
return WORKSPACE_TEMPLATES[slug];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function listWorkspaceTemplates(): WorkspaceYaml[] {
|
|
69
|
+
return Object.values(WORKSPACE_TEMPLATES);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Public templates only (\`meta.isPublic !== false\`). This is a CONVENIENCE
|
|
74
|
+
* filter for the default picker — NOT a security boundary: the private YAML
|
|
75
|
+
* still ships in the published tarball, so anything that must not leak cannot
|
|
76
|
+
* rely on this. \`listWorkspaceTemplates()\` returns all of them (CLI, seeder,
|
|
77
|
+
* tripwire need the full set). Publicity is also expressed per-entry as
|
|
78
|
+
* \`CatalogEntry.isPrivate\` by \`mergeCatalog()\`, so a caller can pass ALL
|
|
79
|
+
* templates and filter at the presentation layer instead of choosing a list
|
|
80
|
+
* function up front — the merge door itself never filters (it stays pure).
|
|
81
|
+
*/
|
|
82
|
+
export function listPublicTemplates(): WorkspaceYaml[] {
|
|
83
|
+
return Object.values(WORKSPACE_TEMPLATES).filter(t => t.meta?.isPublic !== false);
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
}
|