@synap-core/workspace-templates 0.9.4 → 0.10.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/dist/index.d.ts +95 -11
- package/dist/index.js +2018 -467
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/default-relation-defs.json +208 -0
- package/src/__fixtures__/report-automation-flow.json +393 -0
- package/src/agent-fleet.yaml +6 -1
- package/src/base.template.test.ts +160 -0
- package/src/base.yaml +839 -0
- package/src/business-developer.template.test.ts +79 -10
- package/src/business-developer.yaml +61 -39
- package/src/concept-guard.test.ts +47 -31
- package/src/concept-guard.ts +23 -19
- package/src/content-os.yaml +22 -0
- package/src/corpus-flow-validity.test.ts +130 -0
- package/src/crm.yaml +59 -8
- package/src/define.primary-surface.test.ts +2 -4
- package/src/define.ts +28 -0
- package/src/ecosystem.yaml +14 -0
- package/src/enterprise-os.yaml +1 -1
- package/src/foundation.yaml +8 -0
- package/src/grants.yaml +8 -0
- package/src/life-os.yaml +6 -1
- package/src/operations.yaml +270 -0
- package/src/outreach-comms.yaml +196 -0
- package/src/project-management.yaml +6 -1
- package/src/templates.ts +1659 -373
- package/src/types.ts +66 -0
- package/src/validate-flow.ts +186 -7
- package/src/validate.ts +26 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORPUS guard: every automation flow we SHIP must pass `validateFlowDefinition`.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists. `validateTemplate` has called `validateFlowDefinition`
|
|
5
|
+
* for a while (`validate.ts:588`), and the package had 83 passing tests — yet
|
|
6
|
+
* `operations.yaml`'s "Pick up handed-off clients" shipped with four write nodes
|
|
7
|
+
* orphaned outside their loop body, creating entities and relations with EMPTY
|
|
8
|
+
* ids on every run of every pod that installed it. The rule existed and the
|
|
9
|
+
* validator was wired; nothing ever ran the two together over the real corpus.
|
|
10
|
+
* A validator no test points at the shipped data is a validator that only ever
|
|
11
|
+
* checks fixtures.
|
|
12
|
+
*
|
|
13
|
+
* So this reads the SAME YAML the build reads (`readYamlTemplates`, the source
|
|
14
|
+
* the generator consumes — not `templates.ts`, which is generated output and
|
|
15
|
+
* could drift) and asserts the shipped flows are clean. It fails loudly with the
|
|
16
|
+
* per-node messages, because "which node, and why" is the whole value.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from "vitest";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
25
|
+
import { validateFlowDefinition } from "./validate-flow.js";
|
|
26
|
+
|
|
27
|
+
interface ShippedAutomation {
|
|
28
|
+
readonly key?: string;
|
|
29
|
+
readonly name?: string;
|
|
30
|
+
readonly flow?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("corpus: every shipped automation flow passes validateFlowDefinition", () => {
|
|
34
|
+
const templates = readYamlTemplates();
|
|
35
|
+
const failures: string[] = [];
|
|
36
|
+
let flowsChecked = 0;
|
|
37
|
+
|
|
38
|
+
// `readYamlTemplates()` yields `{ file, slug, data }` — the parsed YAML is
|
|
39
|
+
// under `data`, NOT spread onto the entry.
|
|
40
|
+
for (const entry of templates) {
|
|
41
|
+
const { slug, data } = entry as {
|
|
42
|
+
slug?: string;
|
|
43
|
+
data?: { automations?: ShippedAutomation[] };
|
|
44
|
+
};
|
|
45
|
+
const automations = data?.automations;
|
|
46
|
+
if (!Array.isArray(automations)) continue;
|
|
47
|
+
|
|
48
|
+
for (const automation of automations) {
|
|
49
|
+
if (!automation?.flow) continue;
|
|
50
|
+
flowsChecked++;
|
|
51
|
+
const { valid, errors } = validateFlowDefinition(automation.flow);
|
|
52
|
+
if (valid) continue;
|
|
53
|
+
const label = automation.key ?? automation.name ?? "<unnamed>";
|
|
54
|
+
for (const error of errors) {
|
|
55
|
+
failures.push(
|
|
56
|
+
`${slug ?? "<template>"} › ${label} › ` +
|
|
57
|
+
`${error.nodeId ?? "-"} [${error.code}] ${error.message}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Guard the guard: if the corpus ever stops yielding flows (a rename in
|
|
64
|
+
// `readYamlTemplates`, a schema move), this test would "pass" while checking
|
|
65
|
+
// nothing — the exact failure mode it was written to prevent.
|
|
66
|
+
assert.ok(
|
|
67
|
+
flowsChecked > 0,
|
|
68
|
+
"no automation flows found in the shipped corpus — this guard is checking nothing"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
failures,
|
|
73
|
+
[],
|
|
74
|
+
`shipped automation flows failed validation:\n ${failures.join("\n ")}`
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── The SECOND corpus: Control-Plane capability seeds ────────────────────────
|
|
79
|
+
// The YAML sweep above was written after a shipped workspace template was found
|
|
80
|
+
// writing entities with empty ids. It did not catch the IDENTICAL defect in
|
|
81
|
+
// `client-comms-report.capability.json`, because that flow lives in a different
|
|
82
|
+
// repo, in JSON, and nothing pointed a validator at it. Same bug, same week,
|
|
83
|
+
// missed for the same reason: a corpus nobody sweeps.
|
|
84
|
+
//
|
|
85
|
+
// These seeds ship automations verbatim into user pods, so they are held to the
|
|
86
|
+
// same contract. Skips when the CP checkout is absent, like the drift guards.
|
|
87
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
88
|
+
const CP_SEEDS = join(
|
|
89
|
+
HERE,
|
|
90
|
+
"../../../../synap-control-plane-api/src/seeds/capability-templates",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
test("corpus: every CP capability seed's automation flow passes validateFlowDefinition", (t) => {
|
|
94
|
+
if (!existsSync(CP_SEEDS)) {
|
|
95
|
+
t.skip();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const failures: string[] = [];
|
|
99
|
+
let flowsChecked = 0;
|
|
100
|
+
|
|
101
|
+
for (const file of readdirSync(CP_SEEDS).filter((f) => f.endsWith(".json"))) {
|
|
102
|
+
const seed = JSON.parse(readFileSync(join(CP_SEEDS, file), "utf8")) as {
|
|
103
|
+
automations?: Array<{ key?: string; name?: string; flowDefinition?: unknown }>;
|
|
104
|
+
};
|
|
105
|
+
for (const automation of seed.automations ?? []) {
|
|
106
|
+
if (!automation?.flowDefinition) continue;
|
|
107
|
+
flowsChecked++;
|
|
108
|
+
const { valid, errors } = validateFlowDefinition(automation.flowDefinition);
|
|
109
|
+
if (valid) continue;
|
|
110
|
+
const label = automation.key ?? automation.name ?? "<unnamed>";
|
|
111
|
+
for (const error of errors) {
|
|
112
|
+
failures.push(
|
|
113
|
+
`${file} › ${label} › ${error.nodeId ?? "-"} [${error.code}] ${error.message}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Same self-guard as above: a rename in the seed shape must fail loudly
|
|
120
|
+
// rather than silently sweep nothing.
|
|
121
|
+
assert.ok(
|
|
122
|
+
flowsChecked > 0,
|
|
123
|
+
"no automation flows found in the CP capability seeds — this guard is checking nothing",
|
|
124
|
+
);
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
failures,
|
|
127
|
+
[],
|
|
128
|
+
`CP capability seed flows failed validation:\n ${failures.join("\n ")}`,
|
|
129
|
+
);
|
|
130
|
+
});
|
package/src/crm.yaml
CHANGED
|
@@ -18,13 +18,13 @@ meta:
|
|
|
18
18
|
- contacts
|
|
19
19
|
isPublic: true
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
# CRM is a website, so Browser opens it in its normal first-party web session.
|
|
22
|
+
# It deliberately does not use the SDK-hosted iframe/auth-broker path.
|
|
23
|
+
layout:
|
|
24
|
+
primarySurface:
|
|
25
|
+
kind: url
|
|
26
|
+
url: https://crm.synap.live
|
|
27
|
+
title: CRM
|
|
28
28
|
|
|
29
29
|
# SSOT ordering guarantee — this template REFERENCES foundation's pod-wide
|
|
30
30
|
# shared `client`/`partner`/`lead`/`churned` roles below. The apply engine seeds
|
|
@@ -63,6 +63,12 @@ profiles:
|
|
|
63
63
|
icon: user
|
|
64
64
|
color: amber
|
|
65
65
|
description: A prospect, stakeholder, or contact.
|
|
66
|
+
# Pod-wide identity: `entityScope: pod` lands the ROWS with `workspace_id =
|
|
67
|
+
# NULL` (one person record visible from every workspace); the CRM lenses onto
|
|
68
|
+
# it via a role facet (lead/client). `person` is a pod-wide SYSTEM kind, so
|
|
69
|
+
# `scope` is omitted (no claim) — the applier reuses the system row and these
|
|
70
|
+
# CRM props land as this workspace's property OVERLAY.
|
|
71
|
+
entityScope: pod
|
|
66
72
|
properties:
|
|
67
73
|
- slug: email
|
|
68
74
|
label: Email
|
|
@@ -123,7 +129,13 @@ profiles:
|
|
|
123
129
|
icon: building-2
|
|
124
130
|
color: "#10B981"
|
|
125
131
|
description: An organisation being qualified or sold to.
|
|
126
|
-
|
|
132
|
+
# Pod-wide identity: `entityScope: pod` lands the ROWS with `workspace_id =
|
|
133
|
+
# NULL` (one company record visible from every workspace, auto-appearing on the
|
|
134
|
+
# Ecosystem graph), lensed onto this CRM by its role facet (lead/client).
|
|
135
|
+
# `company` is a pod-wide SYSTEM kind, so `scope` is omitted (no claim) — the
|
|
136
|
+
# applier reuses the system row and these CRM props land as an OVERLAY.
|
|
137
|
+
# (Was `scope: WORKSPACE`, which contradicted the pod-wide model.)
|
|
138
|
+
entityScope: pod
|
|
127
139
|
properties:
|
|
128
140
|
- slug: contextSummary
|
|
129
141
|
label: Context
|
|
@@ -336,6 +348,14 @@ profiles:
|
|
|
336
348
|
- healthy
|
|
337
349
|
- at_risk
|
|
338
350
|
- critical
|
|
351
|
+
- slug: driveLink
|
|
352
|
+
label: Drive folder
|
|
353
|
+
valueType: string
|
|
354
|
+
inputType: url
|
|
355
|
+
- slug: driveFolderId
|
|
356
|
+
label: Drive folder ID
|
|
357
|
+
valueType: string
|
|
358
|
+
inputType: text
|
|
339
359
|
|
|
340
360
|
# ── Partner (referral source + co-delivery collaborator) ──────────────────
|
|
341
361
|
- slug: partner
|
|
@@ -456,6 +476,21 @@ profiles:
|
|
|
456
476
|
valueType: string
|
|
457
477
|
inputType: textarea
|
|
458
478
|
|
|
479
|
+
# `linked_to_deal` is the GENERIC counterparty edge and stays the SSOT for "who
|
|
480
|
+
# is on this deal" — the handoff flow's `find-party` node reads exactly it and
|
|
481
|
+
# requires exactly one match, so nothing below may replace it.
|
|
482
|
+
#
|
|
483
|
+
# The two ROLE-QUALIFIED edges are additive: some deals are brokered, and the
|
|
484
|
+
# party on the client side is not the party on the partner side. `client` and
|
|
485
|
+
# `partner` are CO-ATTACHABLE facets on the same person/company (a facet is a
|
|
486
|
+
# hat, not an identity), so the side a party plays ON THIS DEAL cannot be read
|
|
487
|
+
# off its facets — the same company can be the client on one deal and the
|
|
488
|
+
# introducing partner on another. That is a property of the EDGE, which is why
|
|
489
|
+
# it is expressed as two relation types rather than promoted to a kind or
|
|
490
|
+
# inferred from a facet.
|
|
491
|
+
#
|
|
492
|
+
# Both are optional. A direct deal carries only `linked_to_deal` (+ optionally
|
|
493
|
+
# `client_on_deal` naming the same party); nothing requires a partner side.
|
|
459
494
|
entityLinks:
|
|
460
495
|
- sourceProfileSlug: person
|
|
461
496
|
targetProfileSlug: deal
|
|
@@ -463,6 +498,22 @@ entityLinks:
|
|
|
463
498
|
- sourceProfileSlug: company
|
|
464
499
|
targetProfileSlug: deal
|
|
465
500
|
type: linked_to_deal
|
|
501
|
+
- sourceProfileSlug: person
|
|
502
|
+
targetProfileSlug: deal
|
|
503
|
+
type: client_on_deal
|
|
504
|
+
label: Client side
|
|
505
|
+
- sourceProfileSlug: company
|
|
506
|
+
targetProfileSlug: deal
|
|
507
|
+
type: client_on_deal
|
|
508
|
+
label: Client side
|
|
509
|
+
- sourceProfileSlug: person
|
|
510
|
+
targetProfileSlug: deal
|
|
511
|
+
type: partner_on_deal
|
|
512
|
+
label: Partner side
|
|
513
|
+
- sourceProfileSlug: company
|
|
514
|
+
targetProfileSlug: deal
|
|
515
|
+
type: partner_on_deal
|
|
516
|
+
label: Partner side
|
|
466
517
|
|
|
467
518
|
onboarding:
|
|
468
519
|
goal: >
|
|
@@ -59,13 +59,11 @@ describe('workspace primary-surface conversion', () => {
|
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('ships the generated CRM template as
|
|
62
|
+
it('ships the generated CRM template as an ordinary website surface', () => {
|
|
63
63
|
expect(
|
|
64
64
|
toWorkspaceDefinition('crm').definition.layoutConfig?.primarySurface
|
|
65
65
|
).toEqual({
|
|
66
|
-
kind: '
|
|
67
|
-
appId: 'crm',
|
|
68
|
-
rendererType: 'external',
|
|
66
|
+
kind: 'url',
|
|
69
67
|
url: 'https://crm.synap.live',
|
|
70
68
|
title: 'CRM',
|
|
71
69
|
});
|
package/src/define.ts
CHANGED
|
@@ -272,6 +272,19 @@ export interface WorkspaceDefinitionInput {
|
|
|
272
272
|
seedEntities: NonNullable<WorkspaceYaml['seedEntities']>;
|
|
273
273
|
suggestedRelations?: WorkspaceYaml['suggestedRelations'];
|
|
274
274
|
displayTemplates?: WorkspaceYaml['displayTemplates'];
|
|
275
|
+
/**
|
|
276
|
+
* Domain-level relation TYPE vocabulary — the door find-or-creates a
|
|
277
|
+
* `relation_defs` row per entry (idempotent by slug within the workspace). NOT
|
|
278
|
+
* `entityLinks` (those are profile→profile edges and drop the rich metadata).
|
|
279
|
+
* Carried verbatim; the authored form IS the wire form.
|
|
280
|
+
*/
|
|
281
|
+
relationDefs?: WorkspaceYaml['relationDefs'];
|
|
282
|
+
/**
|
|
283
|
+
* Workspace intelligence commands — the door find-or-creates an
|
|
284
|
+
* `intelligence_commands` row per entry (idempotent by title). Carried
|
|
285
|
+
* verbatim; the authored form IS the wire form.
|
|
286
|
+
*/
|
|
287
|
+
commands?: WorkspaceYaml['commands'];
|
|
275
288
|
layoutConfig: WorkspaceYaml['layout'] | undefined;
|
|
276
289
|
bentoViewName?: string;
|
|
277
290
|
/**
|
|
@@ -398,6 +411,11 @@ export function toWorkspaceDefinition(
|
|
|
398
411
|
seedEntities: tpl.seedEntities ?? [],
|
|
399
412
|
suggestedRelations: tpl.suggestedRelations,
|
|
400
413
|
displayTemplates: tpl.displayTemplates,
|
|
414
|
+
// Relation-type vocabulary + intelligence commands — carried verbatim so the
|
|
415
|
+
// one reconcile door can seed the universal defaults (the `base` template)
|
|
416
|
+
// that three hardcoded seeders used to. Authored form IS the wire form.
|
|
417
|
+
relationDefs: tpl.relationDefs,
|
|
418
|
+
commands: tpl.commands,
|
|
401
419
|
layoutConfig: normalizeTemplateLayout(tpl.layout, tpl.app),
|
|
402
420
|
bentoViewName: tpl.bento?.viewName,
|
|
403
421
|
bentoLayout: bento.bentoLayout,
|
|
@@ -472,6 +490,8 @@ type WorkspaceDefinitionForwarded =
|
|
|
472
490
|
| 'seedEntities'
|
|
473
491
|
| 'suggestedRelations'
|
|
474
492
|
| 'displayTemplates'
|
|
493
|
+
| 'relationDefs' // → carried verbatim; door find-or-creates relation_defs rows
|
|
494
|
+
| 'commands' // → carried verbatim; door find-or-creates intelligence_commands rows
|
|
475
495
|
| 'layout' // → layoutConfig
|
|
476
496
|
| 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
|
|
477
497
|
| 'profileEntityBentoTemplates'
|
|
@@ -544,6 +564,10 @@ export interface PackageDefinitionOutput {
|
|
|
544
564
|
suggestedRelations?: WorkspaceYaml['suggestedRelations'];
|
|
545
565
|
displayTemplates?: WorkspaceYaml['displayTemplates'];
|
|
546
566
|
entityLinks?: WorkspaceYaml['entityLinks'];
|
|
567
|
+
/** Relation-type vocabulary — carried verbatim (authored form IS the wire form). */
|
|
568
|
+
relationDefs?: WorkspaceYaml['relationDefs'];
|
|
569
|
+
/** Intelligence commands — carried verbatim (authored form IS the wire form). */
|
|
570
|
+
commands?: WorkspaceYaml['commands'];
|
|
547
571
|
/** WIDGET half of the authored bento union — mapped by the pod via `widgetType`. */
|
|
548
572
|
bentoLayout?: BentoWidgetBlockWire[];
|
|
549
573
|
/**
|
|
@@ -619,6 +643,8 @@ export function toPackageDefinition(
|
|
|
619
643
|
suggestedRelations: tpl.suggestedRelations,
|
|
620
644
|
displayTemplates: tpl.displayTemplates,
|
|
621
645
|
entityLinks: tpl.entityLinks,
|
|
646
|
+
relationDefs: tpl.relationDefs,
|
|
647
|
+
commands: tpl.commands,
|
|
622
648
|
bentoLayout: bento.bentoLayout,
|
|
623
649
|
bentoViewBlocks: bento.bentoViewBlocks,
|
|
624
650
|
bentoViewName: tpl.bento?.viewName,
|
|
@@ -650,6 +676,8 @@ type PackageDefinitionForwarded =
|
|
|
650
676
|
| 'suggestedRelations'
|
|
651
677
|
| 'displayTemplates'
|
|
652
678
|
| 'entityLinks'
|
|
679
|
+
| 'relationDefs' // → carried verbatim
|
|
680
|
+
| 'commands' // → carried verbatim
|
|
653
681
|
| 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
|
|
654
682
|
| 'profileEntityBentoTemplates'
|
|
655
683
|
| 'layout' // → layoutConfig
|
package/src/ecosystem.yaml
CHANGED
|
@@ -51,6 +51,14 @@ profiles:
|
|
|
51
51
|
icon: user
|
|
52
52
|
color: amber
|
|
53
53
|
description: A person in the ecosystem — a founder, operator, or contact.
|
|
54
|
+
# Pod-wide identity. `person` is a pod-wide SYSTEM kind, so `entityScope: pod`
|
|
55
|
+
# is the load-bearing axis: the ROWS land with `workspace_id = NULL`, visible
|
|
56
|
+
# from every workspace (auto-appearing on the Ecosystem graph) and lensed onto
|
|
57
|
+
# a CRM only where it wears a role facet (lead/client/partner). `scope` is
|
|
58
|
+
# deliberately OMITTED: it is already a system profile (more pod-wide than
|
|
59
|
+
# `shared`), so any scope claim only forks the row-level SSOT (see
|
|
60
|
+
# shared-role-ssot.test.ts (c)); the applier reuses the existing system row.
|
|
61
|
+
entityScope: pod
|
|
54
62
|
properties:
|
|
55
63
|
- slug: email
|
|
56
64
|
label: Email
|
|
@@ -66,6 +74,12 @@ profiles:
|
|
|
66
74
|
icon: building-2
|
|
67
75
|
color: "#10B981"
|
|
68
76
|
description: A company in the ecosystem — a player you operate around.
|
|
77
|
+
# Pod-wide identity — same rationale as `person` above: `company` is a pod-wide
|
|
78
|
+
# SYSTEM kind, so `entityScope: pod` lands the ROWS with `workspace_id = NULL`
|
|
79
|
+
# (visible from every workspace, auto-appearing on the Ecosystem graph, lensed
|
|
80
|
+
# onto CRM only by its role facet). `scope` omitted so the applier reuses the
|
|
81
|
+
# existing system row without forking the row-level SSOT.
|
|
82
|
+
entityScope: pod
|
|
69
83
|
properties:
|
|
70
84
|
- slug: website
|
|
71
85
|
label: Website
|
package/src/enterprise-os.yaml
CHANGED
|
@@ -51,7 +51,7 @@ dependencies:
|
|
|
51
51
|
- slug: business-developer
|
|
52
52
|
kind: workspace
|
|
53
53
|
relation: require
|
|
54
|
-
reason: "The lead-first sales motion (
|
|
54
|
+
reason: "The lead-first sales motion (the business-development overlay on the pod-wide lead/client roles, commercial proposals, commission handoff) that the CRM app runs on. Composes onto the CRM workspace above — without it the CRM lens has the bare lead/client roles with no lifecycle or handoff fields and the app's pipeline is empty."
|
|
55
55
|
- slug: content-os
|
|
56
56
|
kind: workspace
|
|
57
57
|
relation: require
|
package/src/foundation.yaml
CHANGED
|
@@ -371,6 +371,14 @@ profiles:
|
|
|
371
371
|
- healthy
|
|
372
372
|
- at_risk
|
|
373
373
|
- critical
|
|
374
|
+
- slug: driveLink
|
|
375
|
+
label: Drive folder
|
|
376
|
+
valueType: string
|
|
377
|
+
inputType: url
|
|
378
|
+
- slug: driveFolderId
|
|
379
|
+
label: Drive folder ID
|
|
380
|
+
valueType: string
|
|
381
|
+
inputType: text
|
|
374
382
|
|
|
375
383
|
# ── Partner (referral source + co-delivery collaborator) ──────────────────
|
|
376
384
|
- slug: partner
|
package/src/grants.yaml
CHANGED
|
@@ -730,6 +730,14 @@ profiles:
|
|
|
730
730
|
- healthy
|
|
731
731
|
- at_risk
|
|
732
732
|
- critical
|
|
733
|
+
- slug: driveLink
|
|
734
|
+
label: Drive folder
|
|
735
|
+
valueType: string
|
|
736
|
+
inputType: url
|
|
737
|
+
- slug: driveFolderId
|
|
738
|
+
label: Drive folder ID
|
|
739
|
+
valueType: string
|
|
740
|
+
inputType: text
|
|
733
741
|
# ── Partner (shared ecosystem role — SSOT is foundation.yaml) ─────────────
|
|
734
742
|
- slug: partner
|
|
735
743
|
displayName: Partner
|
package/src/life-os.yaml
CHANGED
|
@@ -207,7 +207,12 @@ profiles:
|
|
|
207
207
|
icon: User
|
|
208
208
|
color: "#06B6D4"
|
|
209
209
|
description: People in your intellectual and professional world.
|
|
210
|
-
|
|
210
|
+
# Pod-wide identity: `person` is ONE pod-wide SYSTEM kind, not a life-os-local
|
|
211
|
+
# copy. `entityScope: pod` lands the ROWS with `workspace_id = NULL` (visible
|
|
212
|
+
# from every workspace); `scope` omitted (no claim) so the applier reuses the
|
|
213
|
+
# system row and these life-os props land as this workspace's OVERLAY.
|
|
214
|
+
# (Was `scope: WORKSPACE`, which contradicted the pod-wide model.)
|
|
215
|
+
entityScope: pod
|
|
211
216
|
properties:
|
|
212
217
|
- slug: name
|
|
213
218
|
label: Name
|