@synap-core/workspace-templates 0.12.0 → 0.24.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.
- package/README.md +509 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +1496 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +494 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-DqoItRiR.d.ts +1370 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26447 -24075
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +105 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +138 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +349 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1873 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +246 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1563 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +146 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +81 -0
- package/src/retired-goal-kinds.test.ts +52 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/stripe-capability.test.ts +192 -0
- package/src/task-intents.test.ts +293 -0
- package/src/template-documents.test.ts +158 -0
- package/src/templates.ts +20907 -21464
- package/src/types.ts +475 -61
- package/src/unipile-linkedin-find-profile.test.ts +183 -0
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1416 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/views/bookmark-views-pack.view.json +39 -0
- package/views/event-views-pack.view.json +45 -0
- package/views/task-views-pack.view.json +53 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W0 catalog safety — checks that SPAN SOURCES (before this file, no guard
|
|
3
|
+
* spanned sources). Read-only over sibling repos; a sibling that is absent (this
|
|
4
|
+
* package checked out alone, e.g. in synap-app CI) SKIPS its half honestly,
|
|
5
|
+
* with the reason logged, instead of passing vacuously.
|
|
6
|
+
*
|
|
7
|
+
* (a) ONE SOURCE PER PLAYBOOK NAME. The pod reuses playbooks by name (one live
|
|
8
|
+
* row per lower(name) per workspace, migration 0227), so the same name
|
|
9
|
+
* declared by two sources — a workspace YAML and a CP capability, or two
|
|
10
|
+
* capabilities — contend for ONE row, and whichever applies first silently
|
|
11
|
+
* wins. Every name may be declared by exactly one source file; the known
|
|
12
|
+
* exceptions are listed with their reason and must match EXACTLY, so a new
|
|
13
|
+
* duplicate AND a resolved one both turn this red.
|
|
14
|
+
*
|
|
15
|
+
* (b) EVERY SKILL REFERENCE RESOLVES. `dependencies[]` of kind `skill`, every
|
|
16
|
+
* `skills:` link, and every `system/<pkg>/<stem>` named in template or
|
|
17
|
+
* capability prose must name a skill package in this corpus
|
|
18
|
+
* (`skills/*.skill.json` or `skills/<slug>/*.skill.json`) or a system skill on disk under
|
|
19
|
+
* `synap-backend/skills/<pkg>/<stem>.md` (the pod's `ensureSystemSkills`
|
|
20
|
+
* derives `system/<pkg>/<file-stem>` from exactly that layout).
|
|
21
|
+
* Not covered: whether the corpus skill package is PUBLISHED.
|
|
22
|
+
*
|
|
23
|
+
* (c) The catalog's content directories ship in the npm bundle (`files` +
|
|
24
|
+
* the `exports` subpaths, in both `exports` and `publishConfig.exports`),
|
|
25
|
+
* and the build reads none of them (it compiles `src/*.yaml` only).
|
|
26
|
+
*
|
|
27
|
+
* The capability half reads the CATALOG's `capabilities/*.capability.json`
|
|
28
|
+
* (manifests; the CapabilityDefinition is `spec`) — the catalog is the source;
|
|
29
|
+
* the CP's `src/seeds/capability-templates/` is a transition mirror held
|
|
30
|
+
* identical by the CP's `catalog-mirror.tripwire.test.ts`.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { describe, expect, it } from "vitest";
|
|
37
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
38
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
39
|
+
// One duplicate detector, shared with validateCatalog (duplicate-playbook-name).
|
|
40
|
+
import { duplicateNames } from "../scripts/catalog-lib.mjs";
|
|
41
|
+
|
|
42
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const PKG = join(HERE, "..");
|
|
44
|
+
const MONO = join(HERE, "../../../..");
|
|
45
|
+
const CATALOG_CAPS = join(PKG, "capabilities");
|
|
46
|
+
const BACKEND_SKILLS = join(MONO, "synap-backend/skills");
|
|
47
|
+
const CORPUS_SKILLS = join(PKG, "skills");
|
|
48
|
+
|
|
49
|
+
const yamls = readYamlTemplates() as Array<{ file: string; slug: string; data: WorkspaceYaml }>;
|
|
50
|
+
const hasBackend = existsSync(BACKEND_SKILLS);
|
|
51
|
+
if (!hasBackend) console.warn(`[catalog-safety] SKIP the system-skill half: ${BACKEND_SKILLS} is absent`);
|
|
52
|
+
|
|
53
|
+
const capFiles = readdirSync(CATALOG_CAPS)
|
|
54
|
+
.filter((f) => f.endsWith(".capability.json"))
|
|
55
|
+
.map((f) => ({ file: f, text: readFileSync(join(CATALOG_CAPS, f), "utf8") }));
|
|
56
|
+
|
|
57
|
+
// ── (a) one source per playbook name ────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
type Declared = { name: string; source: string };
|
|
60
|
+
|
|
61
|
+
const declared: Declared[] = [
|
|
62
|
+
...yamls.flatMap((t) =>
|
|
63
|
+
(t.data.playbooks ?? []).map((p) => ({ name: p.name, source: `yaml:${t.file}` }))
|
|
64
|
+
),
|
|
65
|
+
...capFiles.flatMap((c) =>
|
|
66
|
+
((JSON.parse(c.text).spec?.playbooks ?? []) as Array<{ name: string }>).map((p) => ({
|
|
67
|
+
name: p.name,
|
|
68
|
+
source: `cap:${c.file}`,
|
|
69
|
+
}))
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Known duplicates, each with its reason. Both are CP-internal (out of this
|
|
75
|
+
* package's lane); giving each one home is wave W4.
|
|
76
|
+
*/
|
|
77
|
+
const KNOWN_DUPLICATES: Record<string, string[]> = {
|
|
78
|
+
"client onboarding": ["cap:agency-skills.capability.json", "cap:discord-bot.capability.json"],
|
|
79
|
+
"lead outreach": ["cap:agency-skills.capability.json", "cap:discord-bot.capability.json"],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe("(a) one source per playbook name", () => {
|
|
83
|
+
it("non-vacuity: the scan sees the YAML playbooks (and the capability ones when present)", () => {
|
|
84
|
+
expect(declared.filter((d) => d.source.startsWith("yaml:")).length).toBeGreaterThanOrEqual(15);
|
|
85
|
+
expect(capFiles.length).toBeGreaterThanOrEqual(30);
|
|
86
|
+
expect(declared.filter((d) => d.source.startsWith("cap:")).length).toBeGreaterThanOrEqual(5);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("self-check: a name declared by two sources, in any case, is reported", () => {
|
|
90
|
+
expect(
|
|
91
|
+
duplicateNames([
|
|
92
|
+
{ name: "Produce Content", source: "yaml:a.yaml" },
|
|
93
|
+
{ name: "produce content ", source: "cap:b.json" },
|
|
94
|
+
{ name: "Solo", source: "yaml:a.yaml" },
|
|
95
|
+
])
|
|
96
|
+
).toEqual({ "produce content": ["cap:b.json", "yaml:a.yaml"] });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("no playbook name has two sources beyond the listed exceptions (exact)", () => {
|
|
100
|
+
expect(duplicateNames(declared)).toEqual(KNOWN_DUPLICATES);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── (b) every skill reference resolves ──────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/** `skills/*.skill.json` and `skills/<slug>/*.skill.json` (one level, as the catalog lib reads them). */
|
|
107
|
+
const corpusSkillFiles = readdirSync(CORPUS_SKILLS, { withFileTypes: true }).flatMap((d) =>
|
|
108
|
+
d.isDirectory()
|
|
109
|
+
? readdirSync(join(CORPUS_SKILLS, d.name))
|
|
110
|
+
.filter((f) => f.endsWith(".skill.json"))
|
|
111
|
+
.map((f) => join(CORPUS_SKILLS, d.name, f))
|
|
112
|
+
: d.name.endsWith(".skill.json")
|
|
113
|
+
? [join(CORPUS_SKILLS, d.name)]
|
|
114
|
+
: []
|
|
115
|
+
);
|
|
116
|
+
const corpusSkillSlugs = new Set(corpusSkillFiles.map((f) => JSON.parse(readFileSync(f, "utf8")).meta.slug as string));
|
|
117
|
+
|
|
118
|
+
/** `system/<pkg>/<stem>` → is there a `synap-backend/skills/<pkg>/<stem>.md` (stem case-insensitive)? */
|
|
119
|
+
function systemSkillExists(ref: string): boolean {
|
|
120
|
+
const [, pkg, stem] = ref.split("/");
|
|
121
|
+
const dir = join(BACKEND_SKILLS, pkg ?? "");
|
|
122
|
+
if (!pkg || !stem || !existsSync(dir)) return false;
|
|
123
|
+
return readdirSync(dir).some((f) => f.toLowerCase() === `${stem.toLowerCase()}.md`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const SYSTEM_REF = /(?<![A-Za-z0-9_/-])system\/[a-z0-9_-]+\/[a-z0-9_-]+/g;
|
|
127
|
+
|
|
128
|
+
const structuredRefs = yamls.flatMap((t) => [
|
|
129
|
+
...(t.data.dependencies ?? [])
|
|
130
|
+
.filter((d) => d.kind === "skill")
|
|
131
|
+
.map((d) => ({ where: `${t.file} dependencies`, slug: d.slug })),
|
|
132
|
+
...(t.data.skills ?? []).map((s) => ({ where: `${t.file} skills:`, slug: s.slug })),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const proseRefs = [
|
|
136
|
+
...yamls.flatMap((t) =>
|
|
137
|
+
[...readFileSync(join(HERE, t.file), "utf8").matchAll(SYSTEM_REF)].map((m) => ({
|
|
138
|
+
where: t.file,
|
|
139
|
+
slug: m[0],
|
|
140
|
+
}))
|
|
141
|
+
),
|
|
142
|
+
...capFiles.flatMap((c) =>
|
|
143
|
+
[...c.text.matchAll(SYSTEM_REF)].map((m) => ({ where: c.file, slug: m[0] }))
|
|
144
|
+
),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
describe("(b) every skill reference resolves", () => {
|
|
148
|
+
it("non-vacuity: structured refs exist, and the resolvers see a literal sample", () => {
|
|
149
|
+
expect(structuredRefs.length).toBeGreaterThanOrEqual(2);
|
|
150
|
+
expect(corpusSkillSlugs.has("creative-director")).toBe(true);
|
|
151
|
+
// The prose matcher catches a system ref and ignores a path that merely ends in one.
|
|
152
|
+
expect("load system/synap/concepts now".match(SYSTEM_REF)).toEqual(["system/synap/concepts"]);
|
|
153
|
+
expect("ecosystem/operations/crm".match(SYSTEM_REF)).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("every skill dependency and skills: link names a corpus or system skill", () => {
|
|
157
|
+
const dangling = structuredRefs.filter((r) =>
|
|
158
|
+
r.slug.startsWith("system/")
|
|
159
|
+
? hasBackend && !systemSkillExists(r.slug)
|
|
160
|
+
: !corpusSkillSlugs.has(r.slug)
|
|
161
|
+
);
|
|
162
|
+
expect(dangling.map((r) => `${r.where}: ${r.slug}`)).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it.skipIf(!hasBackend)("every system/<pkg>/<stem> named in prose exists under synap-backend/skills", () => {
|
|
166
|
+
expect(systemSkillExists("system/synap/concepts")).toBe(true); // literal sample
|
|
167
|
+
expect(systemSkillExists("system/synap/creative-director")).toBe(false); // the stale ref
|
|
168
|
+
const dangling = proseRefs.filter((r) => !systemSkillExists(r.slug));
|
|
169
|
+
expect(dangling.map((r) => `${r.where}: ${r.slug}`)).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── (c) the content directories ship, the build reads none of them ─────────
|
|
174
|
+
|
|
175
|
+
const CONTENT_DIRS = ["capabilities", "skills", "automations", "views"];
|
|
176
|
+
|
|
177
|
+
describe("(c) catalog content ships in the npm bundle; the build reads only src/*.yaml", () => {
|
|
178
|
+
const pkg = JSON.parse(readFileSync(join(PKG, "package.json"), "utf8"));
|
|
179
|
+
|
|
180
|
+
it("non-vacuity: every content directory exists and holds files", () => {
|
|
181
|
+
for (const d of CONTENT_DIRS) expect(readdirSync(join(PKG, d)).length, d).toBeGreaterThan(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("`files` carries every content directory, and both export maps expose it", () => {
|
|
185
|
+
for (const d of CONTENT_DIRS) {
|
|
186
|
+
expect(pkg.files, d).toContain(d);
|
|
187
|
+
expect(pkg.exports[`./${d}/*`], d).toBe(`./${d}/*`);
|
|
188
|
+
expect(pkg.publishConfig.exports[`./${d}/*`], d).toBe(`./${d}/*`);
|
|
189
|
+
}
|
|
190
|
+
expect(pkg.files).toEqual(expect.arrayContaining(["dist", "src"]));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("build.mjs compiles src/*.yaml only", () => {
|
|
194
|
+
expect(yamls.every((t) => t.file.endsWith(".yaml") && !t.file.includes("/"))).toBe(true);
|
|
195
|
+
const build = readFileSync(join(PKG, "build.mjs"), "utf8");
|
|
196
|
+
for (const d of CONTENT_DIRS) expect(build, d).not.toMatch(new RegExp(`["'\`/]${d}/`));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled catalog entries carry DERIVED playbook sub-items — `track-template`
|
|
3
|
+
* for `scope: project`, `work-template` otherwise — the same rule the control
|
|
4
|
+
* plane computes in SQL for remote rows (`packageItemsSql`). A bundle usually
|
|
5
|
+
* WINS the merge for an official template, so without this the marketplace's
|
|
6
|
+
* "Track templates" lens could never see the templates this repo ships.
|
|
7
|
+
*
|
|
8
|
+
* Driven from the REAL bundled corpus through the REAL `mergeCatalog` door, not
|
|
9
|
+
* a hand-built entry.
|
|
10
|
+
*/
|
|
11
|
+
import { test } from "vitest";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mergeCatalog, TRACK_TEMPLATE_ITEM_KIND, WORK_TEMPLATE_ITEM_KIND } from "./catalog.js";
|
|
14
|
+
import { listWorkspaceTemplates } from "./index.js";
|
|
15
|
+
|
|
16
|
+
test("a bundled template with a project-scope playbook reads as shipping a track template", () => {
|
|
17
|
+
const bundled = listWorkspaceTemplates();
|
|
18
|
+
const withMethod = bundled.filter((t) =>
|
|
19
|
+
(t.playbooks ?? []).some((p) => p.scope === "project")
|
|
20
|
+
);
|
|
21
|
+
// Non-vacuity: the corpus really ships at least one track template.
|
|
22
|
+
assert.ok(withMethod.length >= 1, "no bundled template ships a scope:project playbook");
|
|
23
|
+
|
|
24
|
+
const { entries } = mergeCatalog({ bundled, remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
|
|
25
|
+
for (const tpl of withMethod) {
|
|
26
|
+
const entry = entries.find((e) => e.slug === tpl.meta.slug);
|
|
27
|
+
assert.ok(entry, `bundled ${tpl.meta.slug} missing from the merge`);
|
|
28
|
+
const tracks = entry.items.filter((i) => i.kind === TRACK_TEMPLATE_ITEM_KIND);
|
|
29
|
+
const expected = (tpl.playbooks ?? []).filter((p) => p.scope === "project").map((p) => p.name);
|
|
30
|
+
assert.deepEqual(tracks.map((i) => i.name), expected);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a session / unscoped playbook is a work template, never a track template", () => {
|
|
35
|
+
const tpl = listWorkspaceTemplates()[0];
|
|
36
|
+
const synthetic = {
|
|
37
|
+
...tpl,
|
|
38
|
+
meta: { ...tpl.meta, slug: "zz-synthetic" },
|
|
39
|
+
playbooks: [
|
|
40
|
+
{ name: "Digest", goalTemplate: "g", scope: "session" as const },
|
|
41
|
+
{ name: "Enrich", goalTemplate: "g" },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
const { entries } = mergeCatalog({ bundled: [synthetic], remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
entries[0].items
|
|
47
|
+
.filter((i) => i.kind === WORK_TEMPLATE_ITEM_KIND || i.kind === TRACK_TEMPLATE_ITEM_KIND)
|
|
48
|
+
.map((i) => i.kind),
|
|
49
|
+
[WORK_TEMPLATE_ITEM_KIND, WORK_TEMPLATE_ITEM_KIND]
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// G1 (2026-09-27): what a package brings BESIDE its templates — kinds, roles,
|
|
54
|
+
// views — rides the same derived items, so a card can say "+ 4 kinds, 4 views"
|
|
55
|
+
// without the definition. Same rule as the CP `packageItemsSql`.
|
|
56
|
+
test("a bundled template's profiles and views derive kind / role / view items", () => {
|
|
57
|
+
const bundled = listWorkspaceTemplates();
|
|
58
|
+
const { entries } = mergeCatalog({ bundled, remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
|
|
59
|
+
const grp = bundled.find((t) => t.meta.slug === "business-model");
|
|
60
|
+
assert.ok(grp, "business-model is no longer bundled");
|
|
61
|
+
const entry = entries.find((e) => e.slug === "business-model")!;
|
|
62
|
+
const count = (k: string) => entry.items.filter((i) => i.kind === k).length;
|
|
63
|
+
const kinds = grp.profiles.filter((p) => p.profileKind !== "role").length;
|
|
64
|
+
const roles = grp.profiles.filter((p) => p.profileKind === "role").length;
|
|
65
|
+
assert.ok(kinds > 0 && (grp.views ?? []).length > 0, "non-vacuity: GRP ships kinds and views");
|
|
66
|
+
assert.equal(count("kind"), kinds);
|
|
67
|
+
assert.equal(count("role"), roles);
|
|
68
|
+
assert.equal(count("view"), (grp.views ?? []).length);
|
|
69
|
+
// A role-profile reads as a role, never a kind.
|
|
70
|
+
const withRole = bundled.find((t) => t.profiles.some((p) => p.profileKind === "role"));
|
|
71
|
+
assert.ok(withRole, "non-vacuity: some bundled template declares a role");
|
|
72
|
+
const roleEntry = entries.find((e) => e.slug === withRole.meta.slug)!;
|
|
73
|
+
const roleSlug = withRole.profiles.find((p) => p.profileKind === "role")!.slug;
|
|
74
|
+
assert.equal(roleEntry.items.find((i) => i.key === roleSlug)?.kind, "role");
|
|
75
|
+
});
|
package/src/catalog.ts
CHANGED
|
@@ -47,6 +47,68 @@ export interface PackageItemSummary {
|
|
|
47
47
|
name: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The DERIVED template kinds a playbook sub-item carries. A playbook's own
|
|
52
|
+
* `scope` decides it — `"project"` is a method a project runs as a track, any
|
|
53
|
+
* other value (the schema default `session`) is one sitting of work. The author
|
|
54
|
+
* never declares it. The control plane derives the SAME rule in SQL for remote
|
|
55
|
+
* rows (`packageItemsSql`, synap-control-plane-api `routes/packages.ts`); the
|
|
56
|
+
* user-facing noun is `resolveTemplateNoun(scope)` (`@synap-core/types/vocabulary`).
|
|
57
|
+
*/
|
|
58
|
+
export const TRACK_TEMPLATE_ITEM_KIND = "track-template";
|
|
59
|
+
export const WORK_TEMPLATE_ITEM_KIND = "work-template";
|
|
60
|
+
|
|
61
|
+
/** Playbooks → `{kind,key,name}` sub-items. `key` = name (playbooks carry no key). */
|
|
62
|
+
export function derivePlaybookItems(
|
|
63
|
+
playbooks:
|
|
64
|
+
| ReadonlyArray<{ name?: unknown; scope?: unknown } | null | undefined>
|
|
65
|
+
| null
|
|
66
|
+
| undefined
|
|
67
|
+
): PackageItemSummary[] {
|
|
68
|
+
const out: PackageItemSummary[] = [];
|
|
69
|
+
for (const pb of playbooks ?? []) {
|
|
70
|
+
if (!pb || typeof pb.name !== "string" || !pb.name) continue;
|
|
71
|
+
out.push({
|
|
72
|
+
kind: pb.scope === "project" ? TRACK_TEMPLATE_ITEM_KIND : WORK_TEMPLATE_ITEM_KIND,
|
|
73
|
+
key: pb.name,
|
|
74
|
+
name: pb.name,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Schema sub-items: what a package brings BESIDE its templates — each profile as
|
|
82
|
+
* `kind` (or `role` when `profileKind: role`), each view as `view`. The CP derives
|
|
83
|
+
* the SAME rule in SQL (`packageItemsSql`). A card reads them only to say, in a
|
|
84
|
+
* secondary line, what else comes with the package ("+ 4 kinds, 4 views").
|
|
85
|
+
*/
|
|
86
|
+
export function deriveSchemaItems(tpl: {
|
|
87
|
+
profiles?: ReadonlyArray<{ slug?: unknown; displayName?: unknown; profileKind?: unknown } | null | undefined> | null;
|
|
88
|
+
views?: ReadonlyArray<{ name?: unknown; displayName?: unknown; slug?: unknown } | null | undefined> | null;
|
|
89
|
+
}): PackageItemSummary[] {
|
|
90
|
+
const out: PackageItemSummary[] = [];
|
|
91
|
+
for (const p of tpl.profiles ?? []) {
|
|
92
|
+
if (!p || typeof p.slug !== "string" || !p.slug) continue;
|
|
93
|
+
out.push({
|
|
94
|
+
kind: p.profileKind === "role" ? "role" : "kind",
|
|
95
|
+
key: p.slug,
|
|
96
|
+
name: typeof p.displayName === "string" && p.displayName ? p.displayName : p.slug,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const v of tpl.views ?? []) {
|
|
100
|
+
const name =
|
|
101
|
+
v && typeof v.name === "string" && v.name
|
|
102
|
+
? v.name
|
|
103
|
+
: v && typeof v.displayName === "string" && v.displayName
|
|
104
|
+
? v.displayName
|
|
105
|
+
: null;
|
|
106
|
+
if (!v || !name) continue;
|
|
107
|
+
out.push({ kind: "view", key: typeof v.slug === "string" && v.slug ? v.slug : name, name });
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
50
112
|
/**
|
|
51
113
|
* A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
|
|
52
114
|
* pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
|
|
@@ -307,8 +369,10 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
|
|
|
307
369
|
isPrivate: tpl.meta.isPublic === false,
|
|
308
370
|
// Bundled templates ship in-client and are never tier-gated.
|
|
309
371
|
requiredTier: null,
|
|
310
|
-
// Bundled workspace templates carry no cells
|
|
311
|
-
|
|
372
|
+
// Bundled workspace templates carry no cells; their playbooks are summarized
|
|
373
|
+
// with the same derived kind the CP computes for a remote row, so a bundle
|
|
374
|
+
// that wins the merge still reads as shipping a track template.
|
|
375
|
+
items: [...derivePlaybookItems(tpl.playbooks), ...deriveSchemaItems(tpl)],
|
|
312
376
|
template: tpl,
|
|
313
377
|
};
|
|
314
378
|
}
|
|
@@ -45,18 +45,19 @@ describe("concept guard — shipped corpus", () => {
|
|
|
45
45
|
|
|
46
46
|
it("needs NO declared divergence — the CRM fork was resolved, not allowlisted", () => {
|
|
47
47
|
// The allowlist was born carrying `crm-lead|lead` + `client|crm-client`.
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// properties, so there is no fork left to
|
|
48
|
+
// crm.yaml re-declares the SHARED `lead`/`client` roles and carries the
|
|
49
|
+
// qualification/handoff fields (folded in from business-developer,
|
|
50
|
+
// 2026-10-12) as workspace-overlay properties, so there is no fork left to
|
|
51
|
+
// declare. Rule 3 (stale-allowlist)
|
|
51
52
|
// is what forces this to stay true: a leftover entry fails CI.
|
|
52
53
|
expect(Object.keys(DECLARED_DIVERGENCES)).toEqual([]);
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
55
|
+
const crm = WORKSPACE_TEMPLATES["crm"]?.profiles ?? [];
|
|
56
|
+
expect(crm.map((p) => p.slug)).not.toContain("crm-lead");
|
|
57
|
+
expect(crm.map((p) => p.slug)).not.toContain("crm-client");
|
|
57
58
|
for (const slug of ["lead", "client"]) {
|
|
58
|
-
const reused =
|
|
59
|
-
expect(reused, `
|
|
59
|
+
const reused = crm.find((p) => p.slug === slug);
|
|
60
|
+
expect(reused, `crm must reuse shared '${slug}'`).toBeDefined();
|
|
60
61
|
expect(reused?.scope).toBe("shared");
|
|
61
62
|
expect(reused?.profileKind).toBe("role");
|
|
62
63
|
// No alias: the shared slug IS the concept identity.
|
|
@@ -190,3 +191,22 @@ describe("concept guard — stays green on legitimate patterns", () => {
|
|
|
190
191
|
expect(violations).toEqual([]);
|
|
191
192
|
});
|
|
192
193
|
});
|
|
194
|
+
|
|
195
|
+
// "Pipeline" is a VIEW, never a stored kind (work model L3, 2026-10-11): the
|
|
196
|
+
// pipeline is computed from a kind or role + its lifecycle. A stored kind named
|
|
197
|
+
// pipeline (content-studio's old `content-pipeline`) makes "open the pipeline"
|
|
198
|
+
// ambiguous. Scans every profile slug and semanticSlug in the shipped corpus.
|
|
199
|
+
// Does NOT scan property slugs (builder's agent-run `pipeline_phase` is a run
|
|
200
|
+
// phase, a different word) nor view names ("Lead Pipeline" IS the view).
|
|
201
|
+
describe("no stored kind is named pipeline", () => {
|
|
202
|
+
it("the corpus declares no profile whose slug or semanticSlug says pipeline", () => {
|
|
203
|
+
const all = Object.values(WORKSPACE_TEMPLATES).flatMap((t) =>
|
|
204
|
+
(t.profiles ?? []).map((p) => ({ tpl: t.meta?.slug, p }))
|
|
205
|
+
);
|
|
206
|
+
expect(all.length).toBeGreaterThan(100); // non-vacuity
|
|
207
|
+
const hits = all
|
|
208
|
+
.filter(({ p }) => /pipeline/i.test(p.slug) || /pipeline/i.test(p.semanticSlug ?? ""))
|
|
209
|
+
.map(({ tpl, p }) => `${tpl}:${p.slug}`);
|
|
210
|
+
expect(hits).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/concept-guard.ts
CHANGED
|
@@ -51,8 +51,8 @@ export interface ConceptViolation {
|
|
|
51
51
|
*
|
|
52
52
|
* Currently EMPTY, and that is the healthy state. The two entries this list was
|
|
53
53
|
* born for — `crm-lead|lead` and `client|crm-client` — were RESOLVED rather
|
|
54
|
-
* than kept: business-developer
|
|
55
|
-
* roles and carries its business-development fields as workspace-overlay
|
|
54
|
+
* than kept: crm.yaml (business-developer folded into it, 2026-10-12)
|
|
55
|
+
* re-declares the shared `lead`/`client` roles and carries its business-development fields as workspace-overlay
|
|
56
56
|
* PROPERTIES. One concept, one slug, workspace-specific fields. Rule 3 below
|
|
57
57
|
* would have failed CI had the entries been left behind.
|
|
58
58
|
*/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for the Content Creation suite.
|
|
3
|
+
*
|
|
4
|
+
* Reads the generated `templates.ts` corpus (same as every installer).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { getWorkspaceTemplate } from "./templates.js";
|
|
8
|
+
|
|
9
|
+
const template = (() => {
|
|
10
|
+
const resolved = getWorkspaceTemplate("content-creation");
|
|
11
|
+
if (!resolved) throw new Error("content-creation template is not registered");
|
|
12
|
+
return resolved;
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
describe("Content Creation suite", () => {
|
|
16
|
+
it("is a public suite tagged for content creation", () => {
|
|
17
|
+
expect(template.meta.slug).toBe("content-creation");
|
|
18
|
+
expect(template.meta.isPublic).toBe(true);
|
|
19
|
+
expect(template.meta.domain).toBe("content");
|
|
20
|
+
expect(template.meta.tags).toEqual(
|
|
21
|
+
expect.arrayContaining(["suite", "content", "creation"])
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("requires content-os and brand-library because content-os consumes brand; never the retired content-studio", () => {
|
|
26
|
+
const required = (template.dependencies ?? []).map((d) => ({
|
|
27
|
+
slug: d.slug,
|
|
28
|
+
relation: d.relation ?? "require",
|
|
29
|
+
}));
|
|
30
|
+
expect(required).toEqual(
|
|
31
|
+
expect.arrayContaining([
|
|
32
|
+
{ slug: "content-os", relation: "require" },
|
|
33
|
+
{ slug: "brand-library", relation: "require" },
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
// content-studio is retired into content-os (retired.yaml, 2026-11-03).
|
|
37
|
+
expect(required.map((d) => d.slug)).not.toContain("content-studio");
|
|
38
|
+
expect(required.every((d) => d.relation === "require")).toBe(true);
|
|
39
|
+
// Foundation is NOT a direct dep: content-os already requires it, and
|
|
40
|
+
// neither constituent's sourceRoles consume `strategy`.
|
|
41
|
+
expect(required.map((d) => d.slug)).not.toContain("foundation");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not silently skip the brand spine its constituents consume", () => {
|
|
45
|
+
expect(template.workspace.sourceRoles?.brand).toBe("consumer");
|
|
46
|
+
const contentOs = getWorkspaceTemplate("content-os");
|
|
47
|
+
expect(contentOs?.workspace.sourceRoles?.brand).toBe("consumer");
|
|
48
|
+
expect((template.dependencies ?? []).some((d) => d.slug === "brand-library")).toBe(
|
|
49
|
+
true
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Content Creation — the public editorial + production suite
|
|
2
|
+
# ==========================================================
|
|
3
|
+
# A SUITE template: it stands up a content operation in one install by REQUIRING
|
|
4
|
+
# its constituent workspaces (deps-first), then adds a thin "program tower" home
|
|
5
|
+
# of its own — a place to track the editorial programs that cut across the
|
|
6
|
+
# lenses it just installed.
|
|
7
|
+
#
|
|
8
|
+
# What it composes (all public, all bundled):
|
|
9
|
+
# • brand-library — brand identity spine (both constituents consume `brand`)
|
|
10
|
+
# • content-os — the editorial engine (pillars, posts, platforms, newsletters)
|
|
11
|
+
# AND the video / visual pipeline (compositions, assets,
|
|
12
|
+
# renders); transitively requires foundation + marketing-campaign
|
|
13
|
+
#
|
|
14
|
+
# content-studio was retired into content-os (retired.yaml, 2026-11-03): one
|
|
15
|
+
# space per domain, and content-os.yaml said never to install it beside
|
|
16
|
+
# content-os. This suite no longer requires it.
|
|
17
|
+
#
|
|
18
|
+
# Brand is required because content-os declares `sourceRoles.brand: consumer`
|
|
19
|
+
# — skipping it would silently drop the spine it consumes. Foundation is NOT a direct dep: content-os already
|
|
20
|
+
# requires it (and marketing-campaign) via its own `dependencies[]`; sourceRoles
|
|
21
|
+
# do not consume `strategy`.
|
|
22
|
+
#
|
|
23
|
+
# It uses `require` (not `compose`): each constituent is a REAL workspace, not an
|
|
24
|
+
# overlay. Installing Content Creation a second time is idempotent — the resolver
|
|
25
|
+
# reuses existing constituent workspaces by slug.
|
|
26
|
+
|
|
27
|
+
meta:
|
|
28
|
+
slug: content-creation
|
|
29
|
+
domain: content
|
|
30
|
+
name: Content Creation
|
|
31
|
+
description: Stand up a content operation in one install — brand, editorial engine, and video pipeline — plus a program tower to track what you are making across them.
|
|
32
|
+
icon: pen-tool
|
|
33
|
+
color: "#6366F1"
|
|
34
|
+
tags: [suite, content, creation, editorial, video, studio]
|
|
35
|
+
isPublic: true
|
|
36
|
+
|
|
37
|
+
# Deps-first: brand-library is the brand-vocabulary provider content-os
|
|
38
|
+
# consumes, so it must resolve before content-os reuses it. `require` (not
|
|
39
|
+
# `compose`): each is a standalone workspace, not an additive overlay.
|
|
40
|
+
dependencies:
|
|
41
|
+
- slug: brand-library
|
|
42
|
+
kind: workspace
|
|
43
|
+
relation: require
|
|
44
|
+
reason: "Brand identity spine — Content OS consumes `brand`; skipping it would drop the provider that sourceRole requires."
|
|
45
|
+
- slug: content-os
|
|
46
|
+
kind: workspace
|
|
47
|
+
relation: require
|
|
48
|
+
reason: "The editorial engine and the video / visual pipeline — pillars, posts, platforms, newsletters, compositions and renders. Transitively brings foundation (strategy spine) and marketing-campaign."
|
|
49
|
+
|
|
50
|
+
workspace:
|
|
51
|
+
name: Content Creation
|
|
52
|
+
description: The program tower for the content operation — track the editorial programs that cut across the brand, editorial, and production lenses this suite installs.
|
|
53
|
+
subtype: content-creation
|
|
54
|
+
visibility: pod_visible
|
|
55
|
+
sourceRoles:
|
|
56
|
+
brand: consumer
|
|
57
|
+
content: provider
|
|
58
|
+
|
|
59
|
+
profiles:
|
|
60
|
+
# The one native profile the program tower owns: a content program the
|
|
61
|
+
# operation is driving across the lenses this suite installs. Deliberately
|
|
62
|
+
# light — the depth lives in the constituent workspaces; this is the
|
|
63
|
+
# cross-cutting scoreboard that ties them together.
|
|
64
|
+
- slug: content-program
|
|
65
|
+
displayName: Content Program
|
|
66
|
+
icon: target
|
|
67
|
+
color: "#6366F1"
|
|
68
|
+
description: A content program the operation is driving — the cross-cutting outcome the editorial and production lenses serve.
|
|
69
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
70
|
+
lifecycle:
|
|
71
|
+
property: status
|
|
72
|
+
options:
|
|
73
|
+
- { key: on-track, category: active }
|
|
74
|
+
- { key: at-risk, category: active }
|
|
75
|
+
- { key: off-track, category: active }
|
|
76
|
+
- { key: shipped, category: done }
|
|
77
|
+
properties:
|
|
78
|
+
- slug: outcome
|
|
79
|
+
label: Outcome
|
|
80
|
+
valueType: string
|
|
81
|
+
inputType: textarea
|
|
82
|
+
- slug: status
|
|
83
|
+
label: Status
|
|
84
|
+
valueType: string
|
|
85
|
+
inputType: select
|
|
86
|
+
enumValues: [on-track, at-risk, off-track, shipped]
|
|
87
|
+
constraints: { defaultValue: on-track }
|
|
88
|
+
- slug: horizon
|
|
89
|
+
label: Horizon
|
|
90
|
+
valueType: string
|
|
91
|
+
inputType: select
|
|
92
|
+
enumValues: [now, next, later]
|
|
93
|
+
constraints: { defaultValue: now }
|
|
94
|
+
- slug: owner
|
|
95
|
+
label: Owner
|
|
96
|
+
valueType: string
|
|
97
|
+
inputType: text
|
|
98
|
+
- slug: targetDate
|
|
99
|
+
label: Target Date
|
|
100
|
+
valueType: date
|
|
101
|
+
inputType: date
|
|
102
|
+
|
|
103
|
+
views:
|
|
104
|
+
- name: Programs
|
|
105
|
+
slug: programs
|
|
106
|
+
type: table
|
|
107
|
+
scopeProfileSlug: content-program
|
|
108
|
+
description: The content programs across editorial and production, by horizon and status.
|
|
109
|
+
defaultView: true
|
|
110
|
+
config:
|
|
111
|
+
columns: [title, status, horizon, owner, targetDate]
|
|
112
|
+
- name: Program Board
|
|
113
|
+
slug: program-board
|
|
114
|
+
type: kanban
|
|
115
|
+
scopeProfileSlug: content-program
|
|
116
|
+
description: Programs grouped by status — a running scoreboard for the content operation.
|
|
117
|
+
config:
|
|
118
|
+
groupBy: status
|
|
119
|
+
|
|
120
|
+
onboarding:
|
|
121
|
+
goal: >
|
|
122
|
+
Stand up the content operation and name the handful of programs it is
|
|
123
|
+
driving — so brand, editorial, and production are all in place and pointed
|
|
124
|
+
at the same outcomes from day one.
|
|
125
|
+
framing: |
|
|
126
|
+
THE EDITORIAL PRODUCER: Act as a sharp producer standing up the content OS.
|
|
127
|
+
Brand and Content OS are already installed — your job is
|
|
128
|
+
to name the 3-5 programs that cut across them, so the operation has a
|
|
129
|
+
scoreboard, not just structure.
|
|
130
|
+
expertise:
|
|
131
|
+
starters:
|
|
132
|
+
- >
|
|
133
|
+
Name few, real programs. A content operation with 20 programs has
|
|
134
|
+
none; force the 3-5 that actually decide this quarter, each with an
|
|
135
|
+
owner and a horizon (now / next / later).
|
|
136
|
+
- >
|
|
137
|
+
Tie each program to a lens. A good program names where it lives —
|
|
138
|
+
a series (editorial), a production pipeline (video and visual
|
|
139
|
+
posts), or a brand system
|
|
140
|
+
(library) — so the scoreboard maps to the structure underneath it.
|
|
141
|
+
blindSpots:
|
|
142
|
+
- >
|
|
143
|
+
Programs with no owner and no date are wishes. Every one needs a
|
|
144
|
+
single owner and a target date, or it will not move.
|
|
145
|
+
- >
|
|
146
|
+
"Make more content" is not a program — it is a category. Push for
|
|
147
|
+
the specific outcome that would make this quarter a win.
|
|
148
|
+
bar: >
|
|
149
|
+
A short, honest scoreboard: 3-5 programs, each owned, dated, placed on a
|
|
150
|
+
horizon, and tied to the lens that serves it — sitting on top of a fully
|
|
151
|
+
installed content operation.
|
|
152
|
+
collect:
|
|
153
|
+
- profileSlug: content-program
|
|
154
|
+
what: The 3-5 content programs the operation is driving this horizon.
|
|
155
|
+
cardinality: few
|
|
156
|
+
keyFields: [status, horizon, owner]
|
|
157
|
+
openingQuestions:
|
|
158
|
+
- "What are the 3-5 content programs that will decide this quarter?"
|
|
159
|
+
- "Who owns each one, and is it a now, next, or later horizon?"
|
|
160
|
+
doneWhen: A few programs captured, each with an owner and a horizon.
|
|
161
|
+
|
|
162
|
+
app:
|
|
163
|
+
kind: stored
|
|
164
|
+
cellKey: entity-detail
|
|
165
|
+
|
|
166
|
+
layout:
|
|
167
|
+
defaultView: Programs
|
|
168
|
+
defaultApp: null
|
|
169
|
+
pinnedApps: [dashboard, data]
|
|
170
|
+
sidebarItems:
|
|
171
|
+
- kind: view
|
|
172
|
+
viewName: Programs
|
|
173
|
+
label: Programs
|
|
174
|
+
icon: target
|
|
175
|
+
- kind: view
|
|
176
|
+
viewName: Program Board
|
|
177
|
+
label: Board
|
|
178
|
+
icon: kanban-square
|
|
179
|
+
- kind: app
|
|
180
|
+
appId: data
|
|
181
|
+
label: Data
|
|
182
|
+
icon: database
|
|
183
|
+
|
|
184
|
+
bento:
|
|
185
|
+
viewName: Programs
|
|
186
|
+
blocks:
|
|
187
|
+
- kind: view
|
|
188
|
+
viewName: Programs
|
|
189
|
+
pos: { x: 0, y: 0, w: 8, h: 4 }
|
|
190
|
+
- kind: view
|
|
191
|
+
viewName: Program Board
|
|
192
|
+
pos: { x: 8, y: 0, w: 4, h: 4 }
|