@synap-core/workspace-templates 0.11.0 → 0.23.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 +483 -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 +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +189 -900
- package/dist/index.js +24654 -21639
- 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 +41 -27
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- 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 +99 -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 +135 -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.template.test.ts +25 -19
- package/src/base.yaml +95 -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 +798 -92
- 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 +598 -0
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +85 -2
- package/src/civic-france.yaml +102 -0
- 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 +322 -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 +1830 -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 +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/dist-freshness.test.ts +97 -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 +130 -28
- 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 +57 -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 +65 -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 +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +176 -0
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +42 -1
- 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/task-intents.test.ts +293 -0
- package/src/templates.ts +21639 -21522
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +32 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +156 -3
- package/src/validate.ts +1384 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/__fixtures__/report-automation-flow.json +0 -393
- 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", "packs", "automations"];
|
|
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
|
|
@@ -83,6 +145,13 @@ export interface RemoteEntry {
|
|
|
83
145
|
* never "unknown-so-hide".
|
|
84
146
|
*/
|
|
85
147
|
isPrivate?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The minimum subscription tier required to INSTALL this package (`solo` | `pro`
|
|
150
|
+
* | `team` | `enterprise`); `null`/absent ⇒ ungated. DISPLAY only here — the real
|
|
151
|
+
* install floor is the pod's `assertPackageTierAccess`. Kept a bare `string` so
|
|
152
|
+
* this pure/shared door stays enum-agnostic (like `category`).
|
|
153
|
+
*/
|
|
154
|
+
requiredTier?: string | null;
|
|
86
155
|
/**
|
|
87
156
|
* Lightweight computed summary of the row's sub-objects (cells), carried on the
|
|
88
157
|
* LIST projection so a surface can surface a matching sub-item without the full
|
|
@@ -149,6 +218,8 @@ export interface CpPackageRow {
|
|
|
149
218
|
icon?: string | null;
|
|
150
219
|
/** Present only on `/mine` rows. `false` ⇒ private. Browse rows omit it. */
|
|
151
220
|
isPublic?: boolean;
|
|
221
|
+
/** The package's `required_tier` gate (`solo`|`pro`|…); `null`/absent ⇒ ungated. */
|
|
222
|
+
requiredTier?: string | null;
|
|
152
223
|
/**
|
|
153
224
|
* Computed sub-object summary from `GET /api/packages` / `/mine` (query-time,
|
|
154
225
|
* not a stored column). Carries `key + name` per cell; never the cell code.
|
|
@@ -180,6 +251,7 @@ export function rowToRemoteEntry(
|
|
|
180
251
|
tags: row.tags ?? [],
|
|
181
252
|
icon: row.icon ?? null,
|
|
182
253
|
isPrivate,
|
|
254
|
+
requiredTier: row.requiredTier ?? null,
|
|
183
255
|
items: row.items,
|
|
184
256
|
// Both list routes strip `definition`; absence ⇒ bundle wins a slug conflict,
|
|
185
257
|
// and the install path fetches the full definition per-slug when needed.
|
|
@@ -202,6 +274,12 @@ interface CatalogEntryBase {
|
|
|
202
274
|
icon?: string | null;
|
|
203
275
|
/** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
|
|
204
276
|
isPrivate: boolean;
|
|
277
|
+
/**
|
|
278
|
+
* Minimum install tier (`solo`|`pro`|…); `null` ⇒ ungated. Bundled templates are
|
|
279
|
+
* always `null` (they ship in-client, never gated). DISPLAY only — a surface
|
|
280
|
+
* shows a "requires <tier>" badge; the real floor is the pod's install check.
|
|
281
|
+
*/
|
|
282
|
+
requiredTier?: string | null;
|
|
205
283
|
/**
|
|
206
284
|
* The entry's sub-object summary (cells). Always present (defaults to `[]`) so a
|
|
207
285
|
* consumer can match sub-items uniformly. Bundled workspace templates carry no
|
|
@@ -289,8 +367,12 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
|
|
|
289
367
|
tags: tpl.meta.tags ?? [],
|
|
290
368
|
icon: tpl.meta.icon,
|
|
291
369
|
isPrivate: tpl.meta.isPublic === false,
|
|
292
|
-
// Bundled
|
|
293
|
-
|
|
370
|
+
// Bundled templates ship in-client and are never tier-gated.
|
|
371
|
+
requiredTier: null,
|
|
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)],
|
|
294
376
|
template: tpl,
|
|
295
377
|
};
|
|
296
378
|
}
|
|
@@ -306,6 +388,7 @@ function remoteToEntry(row: RemoteEntry): RemoteCatalogEntry {
|
|
|
306
388
|
tags: row.tags ?? [],
|
|
307
389
|
icon: row.icon ?? null,
|
|
308
390
|
isPrivate: row.isPrivate ?? false,
|
|
391
|
+
requiredTier: row.requiredTier ?? null,
|
|
309
392
|
items: row.items ?? [],
|
|
310
393
|
remote: row,
|
|
311
394
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# France civic open-data research overlay — STUB (enhance later)
|
|
2
|
+
#
|
|
3
|
+
# Thin overlay that composes onto research-base for investigating French public
|
|
4
|
+
# data (open data / Licence Ouverte). NOT a government product, NOT a marketplace
|
|
5
|
+
# civic-tech app. Private workspace template only.
|
|
6
|
+
#
|
|
7
|
+
# Data access: consume official APIs only — data.gouv.fr, api.gouv.fr, PISTE.
|
|
8
|
+
# Never scrape HTML. Do not invent a public pod host or resource_shares.
|
|
9
|
+
# No automations that fetch the web in this stub.
|
|
10
|
+
|
|
11
|
+
meta:
|
|
12
|
+
slug: civic-france
|
|
13
|
+
domain: knowledge
|
|
14
|
+
name: France Civic Research
|
|
15
|
+
description: Overlay for investigating French public data (open data / Licence Ouverte). Composes onto Research. Not a government product — a private research stub.
|
|
16
|
+
icon: landmark
|
|
17
|
+
color: "#1D4ED8"
|
|
18
|
+
tags: [research, civic, france, open-data, licence-ouverte]
|
|
19
|
+
isPublic: false
|
|
20
|
+
|
|
21
|
+
workspace:
|
|
22
|
+
name: Research
|
|
23
|
+
description: Adds a French civic open-data lens onto the Research substrate — track public datasets consulted during investigations.
|
|
24
|
+
subtype: research-base
|
|
25
|
+
|
|
26
|
+
# Layer additively onto the research-base workspace (no second workspace).
|
|
27
|
+
# Visibility and capabilities OMITTED so the base Research workspace keeps its own.
|
|
28
|
+
dependencies:
|
|
29
|
+
- slug: research-base
|
|
30
|
+
relation: compose
|
|
31
|
+
reason: France civic research is a research method — it needs the Research substrate (question / research / source) to work over.
|
|
32
|
+
|
|
33
|
+
profiles:
|
|
34
|
+
# Slugs this overlay reuses from research-base — declared for the per-file
|
|
35
|
+
# validator (base owns their full schema; the applier skips already-existing
|
|
36
|
+
# properties). Mirror research-decision's reuse pattern.
|
|
37
|
+
- slug: question
|
|
38
|
+
displayName: Question
|
|
39
|
+
icon: help-circle
|
|
40
|
+
color: blue
|
|
41
|
+
description: An open question driving a civic-data investigation.
|
|
42
|
+
properties:
|
|
43
|
+
- slug: questionStatus
|
|
44
|
+
label: Status
|
|
45
|
+
valueType: string
|
|
46
|
+
inputType: select
|
|
47
|
+
enumValues: [open, exploring, answered, abandoned]
|
|
48
|
+
constraints: { defaultValue: open }
|
|
49
|
+
|
|
50
|
+
- slug: research
|
|
51
|
+
displayName: Research
|
|
52
|
+
icon: microscope
|
|
53
|
+
color: violet
|
|
54
|
+
description: An investigation into French public data — sources, findings, conclusion.
|
|
55
|
+
properties:
|
|
56
|
+
- slug: researchStatus
|
|
57
|
+
label: Status
|
|
58
|
+
valueType: string
|
|
59
|
+
inputType: select
|
|
60
|
+
enumValues: [ongoing, concluded, abandoned]
|
|
61
|
+
constraints: { defaultValue: ongoing }
|
|
62
|
+
|
|
63
|
+
- slug: source
|
|
64
|
+
displayName: Source
|
|
65
|
+
icon: file-text
|
|
66
|
+
color: cyan
|
|
67
|
+
description: Anything consulted — API payload, dataset page, decree, report.
|
|
68
|
+
properties:
|
|
69
|
+
- slug: url
|
|
70
|
+
label: URL
|
|
71
|
+
valueType: string
|
|
72
|
+
inputType: url
|
|
73
|
+
|
|
74
|
+
# Workspace-owned catalog entry for a French public dataset (thin stub).
|
|
75
|
+
- slug: public_dataset
|
|
76
|
+
displayName: Public dataset
|
|
77
|
+
icon: database
|
|
78
|
+
color: blue
|
|
79
|
+
description: A French public dataset consulted via official open-data APIs (data.gouv.fr / api.gouv.fr / PISTE) — never scraped HTML.
|
|
80
|
+
scope: WORKSPACE
|
|
81
|
+
properties:
|
|
82
|
+
- slug: sourceUrl
|
|
83
|
+
label: Source URL
|
|
84
|
+
valueType: string
|
|
85
|
+
inputType: url
|
|
86
|
+
- slug: license
|
|
87
|
+
label: License
|
|
88
|
+
valueType: string
|
|
89
|
+
inputType: text
|
|
90
|
+
- slug: publisher
|
|
91
|
+
label: Publisher
|
|
92
|
+
valueType: string
|
|
93
|
+
inputType: text
|
|
94
|
+
|
|
95
|
+
views:
|
|
96
|
+
- name: Public datasets
|
|
97
|
+
slug: civic-france-public-datasets
|
|
98
|
+
type: table
|
|
99
|
+
scopeProfileSlug: public_dataset
|
|
100
|
+
description: French public datasets tracked in this research workspace.
|
|
101
|
+
config:
|
|
102
|
+
columns: [title, publisher, license, sourceUrl]
|
|
@@ -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
|
+
});
|