@nebutra/cofounder-match 0.1.1

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 ADDED
@@ -0,0 +1,28 @@
1
+ # @nebutra/cofounder-match
2
+
3
+ Status: **WIP**
4
+
5
+ Behavior-derived founder profiles, explainable cofounder matching, and
6
+ mutual-consent introduction records.
7
+
8
+ ## Boundary
9
+
10
+ This package does not own chat transport, identity providers, or social graph
11
+ truth. It writes profile and match artifacts only after real activity and mutual
12
+ consent checks.
13
+
14
+ ## Commands
15
+
16
+ ```bash
17
+ pnpm cofounder-match:doctor
18
+ pnpm cofounder-match:quickstart
19
+ pnpm cofounder-match:debug
20
+ ```
21
+
22
+ ## Current Closure
23
+
24
+ - 30-day activity gate.
25
+ - Skill complementarity scoring.
26
+ - Visible `why` explanations.
27
+ - Mutual-interest-only chat handoff.
28
+ - Local profile and intro artifacts.
@@ -0,0 +1,179 @@
1
+ // src/index.ts
2
+ import { mkdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
5
+ import { ContentStore } from "@nebutra/content-store";
6
+ import { CapabilityError } from "@nebutra/errors";
7
+ import { EventLog } from "@nebutra/event-log";
8
+ import { assetId } from "@nebutra/generation-context";
9
+ function requireTenant(value) {
10
+ if (!value?.trim()) {
11
+ throw new CapabilityError("cofounder-match", "Cofounder Match requires tenant context", {
12
+ suggestion: "Pass tenantId so profile and consent artifacts stay tenant-scoped.",
13
+ statusCode: 400
14
+ });
15
+ }
16
+ return value;
17
+ }
18
+ function skillVector(input) {
19
+ return {
20
+ code: input.code ?? 0,
21
+ growth: input.growth ?? 0,
22
+ brand: input.brand ?? 0,
23
+ support: input.support ?? 0
24
+ };
25
+ }
26
+ function strongestSkill(vector) {
27
+ return Object.entries(vector).sort(
28
+ ([leftKey, left], [rightKey, right]) => right - left || leftKey.localeCompare(rightKey)
29
+ )[0]?.[0] ?? "code";
30
+ }
31
+ function weakestSkill(vector) {
32
+ return Object.entries(vector).sort(
33
+ ([leftKey, left], [rightKey, right]) => left - right || leftKey.localeCompare(rightKey)
34
+ )[0]?.[0] ?? "growth";
35
+ }
36
+ function sharedCount(left, right) {
37
+ const rightSet = new Set(right.map((item) => item.toLowerCase()));
38
+ return left.filter((item) => rightSet.has(item.toLowerCase())).length;
39
+ }
40
+ function consentKey(from, to) {
41
+ return `${from}->${to}`;
42
+ }
43
+ var CofounderMatch = class _CofounderMatch {
44
+ #tenantId;
45
+ #debugRoot;
46
+ #contentStore;
47
+ #eventLog;
48
+ #profiles = /* @__PURE__ */ new Map();
49
+ #interests = /* @__PURE__ */ new Set();
50
+ constructor(options) {
51
+ this.#tenantId = requireTenant(options.tenantId);
52
+ this.#debugRoot = options.debugRoot ?? process.cwd();
53
+ this.#contentStore = options.contentStore;
54
+ this.#eventLog = options.eventLog;
55
+ }
56
+ static async open(root = ".nebutra/cofounder-match", options = {}) {
57
+ const tenantId = options.tenantId ?? "local";
58
+ await mkdir(root, { recursive: true });
59
+ const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
60
+ const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
61
+ return new _CofounderMatch({ ...options, tenantId, contentStore, eventLog });
62
+ }
63
+ async buildProfileFromActivity(input) {
64
+ const profile = {
65
+ founderId: input.founderId,
66
+ activeDays: input.activeDays,
67
+ activityVerified: input.activeDays >= 30,
68
+ ...input.activeDays < 30 ? { lockedReason: "Cofounder matching unlocks after 30 days of real activity." } : {},
69
+ skillVector: skillVector(input.playCounts),
70
+ domainTags: input.domainTags,
71
+ communicationStyle: input.communicationSamples
72
+ };
73
+ const path = `cofounder-match/profiles/${input.founderId}.json`;
74
+ const snapshot = `${JSON.stringify(profile, null, 2)}
75
+ `;
76
+ await this.#contentStore.write(path, snapshot);
77
+ this.#profiles.set(input.founderId, profile);
78
+ await this.#debug({
79
+ type: "profile",
80
+ tenantId: this.#tenantId,
81
+ founderId: input.founderId,
82
+ activityVerified: profile.activityVerified
83
+ });
84
+ return profile;
85
+ }
86
+ scoreMatch(left, right) {
87
+ const leftWeak = weakestSkill(left.skillVector);
88
+ const rightStrong = strongestSkill(right.skillVector);
89
+ const rightWeak = weakestSkill(right.skillVector);
90
+ const leftStrong = strongestSkill(left.skillVector);
91
+ const growthFill = left.skillVector.growth <= 10 && right.skillVector.growth >= 30 ? 45 : 0;
92
+ const codeFill = right.skillVector.code <= 10 && left.skillVector.code >= 30 ? 25 : 0;
93
+ const supportFill = left.skillVector.support <= 5 && right.skillVector.support >= 15 ? 10 : 0;
94
+ const brandFill = right.skillVector.brand <= 5 && left.skillVector.brand >= 10 ? 10 : 0;
95
+ const complements = Math.min(100, 20 + growthFill + codeFill + supportFill + brandFill);
96
+ const domainAdjacency = Math.min(100, 55 + sharedCount(left.domainTags, right.domainTags) * 20);
97
+ const paceCompatibility = Math.max(40, 100 - Math.abs(left.activeDays - right.activeDays));
98
+ const overall = Math.round(
99
+ Math.min(100, complements * 0.55 + domainAdjacency * 0.25 + paceCompatibility * 0.2)
100
+ );
101
+ const why = [
102
+ `${right.founderId} is strong in ${rightStrong[0]?.toUpperCase()}${rightStrong.slice(
103
+ 1
104
+ )}, which complements ${left.founderId}'s ${leftWeak} gap.`,
105
+ `${left.founderId} is strong in ${leftStrong}, which balances ${right.founderId}'s ${rightWeak} gap.`,
106
+ `Shared domains: ${sharedCount(left.domainTags, right.domainTags)}.`
107
+ ];
108
+ return {
109
+ leftFounderId: left.founderId,
110
+ rightFounderId: right.founderId,
111
+ overall,
112
+ skillComplementarity: Math.min(100, complements),
113
+ domainAdjacency,
114
+ paceCompatibility,
115
+ why
116
+ };
117
+ }
118
+ async expressInterest(fromFounderId, toFounderId) {
119
+ this.#interests.add(consentKey(fromFounderId, toFounderId));
120
+ await this.#debug({ type: "interest", tenantId: this.#tenantId, fromFounderId, toFounderId });
121
+ }
122
+ async startChat(leftFounderId, rightFounderId) {
123
+ if (!this.#interests.has(consentKey(leftFounderId, rightFounderId)) || !this.#interests.has(consentKey(rightFounderId, leftFounderId))) {
124
+ throw new CapabilityError("cofounder-match", "Mutual consent is required before chat", {
125
+ suggestion: "Wait for mutual interest before opening a cofounder intro thread.",
126
+ statusCode: 403
127
+ });
128
+ }
129
+ const thread = {
130
+ threadId: assetId("cofounder_chat", `${leftFounderId}_${rightFounderId}`),
131
+ threadKind: "mutual-consent-intro",
132
+ founders: [leftFounderId, rightFounderId],
133
+ icebreaker: `${leftFounderId}, start with the specific work you admired in ${rightFounderId}'s profile.`
134
+ };
135
+ const path = `cofounder-match/chats/${thread.threadId}.json`;
136
+ const snapshot = `${JSON.stringify(thread, null, 2)}
137
+ `;
138
+ await this.#contentStore.write(path, snapshot);
139
+ await this.#eventLog.commit({
140
+ traceId: assetId("cofounder_match_chat", thread.threadId),
141
+ kind: "content_write",
142
+ affected: [path],
143
+ parent: null,
144
+ snapshot: { [path]: snapshot }
145
+ });
146
+ await this.#debug({
147
+ type: "start_chat",
148
+ tenantId: this.#tenantId,
149
+ founders: thread.founders
150
+ });
151
+ return thread;
152
+ }
153
+ async doctor() {
154
+ const report = {
155
+ capability: "cofounder-match",
156
+ ok: true,
157
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
158
+ features: ["activity-verified-profile", "explainable-score", "mutual-consent-intro"],
159
+ suggestion: "Local matching contracts are ready; encrypted chat and identity providers remain adapter handoffs."
160
+ };
161
+ await this.#debug({ type: "doctor", tenantId: this.#tenantId, profiles: this.#profiles.size });
162
+ return report;
163
+ }
164
+ async close() {
165
+ await this.#contentStore.close();
166
+ }
167
+ async #debug(entry) {
168
+ await appendCapabilityDebug("cofounder-match", entry, { root: this.#debugRoot });
169
+ }
170
+ };
171
+ async function readCofounderMatchDebug(root = process.cwd(), limit = 20) {
172
+ return readCapabilityDebug("cofounder-match", { root, limit });
173
+ }
174
+
175
+ export {
176
+ CofounderMatch,
177
+ readCofounderMatchDebug
178
+ };
179
+ //# sourceMappingURL=chunk-ZXTCDLEE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { EventLog } from \"@nebutra/event-log\";\nimport { assetId } from \"@nebutra/generation-context\";\n\nexport type SkillArea = \"code\" | \"growth\" | \"brand\" | \"support\";\n\nexport interface ActivityProfileInput {\n readonly founderId: string;\n readonly activeDays: number;\n readonly playCounts: Partial<Record<SkillArea, number>>;\n readonly domainTags: readonly string[];\n readonly communicationSamples: readonly string[];\n}\n\nexport interface FounderProfile {\n readonly founderId: string;\n readonly activityVerified: boolean;\n readonly lockedReason?: string;\n readonly activeDays: number;\n readonly skillVector: Record<SkillArea, number>;\n readonly domainTags: readonly string[];\n readonly communicationStyle: readonly string[];\n}\n\nexport interface MatchScore {\n readonly leftFounderId: string;\n readonly rightFounderId: string;\n readonly overall: number;\n readonly skillComplementarity: number;\n readonly domainAdjacency: number;\n readonly paceCompatibility: number;\n readonly why: readonly string[];\n}\n\nexport interface ChatThread {\n readonly threadId: string;\n readonly threadKind: \"mutual-consent-intro\";\n readonly founders: readonly [string, string];\n readonly icebreaker: string;\n}\n\nexport interface CofounderMatchDoctorReport {\n readonly capability: \"cofounder-match\";\n readonly ok: boolean;\n readonly checkedAt: string;\n readonly features: readonly string[];\n readonly suggestion?: string;\n}\n\nexport interface CofounderMatchOptions {\n readonly tenantId?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly eventLog?: EventLog;\n}\n\nfunction requireTenant(value: string | undefined): string {\n if (!value?.trim()) {\n throw new CapabilityError(\"cofounder-match\", \"Cofounder Match requires tenant context\", {\n suggestion: \"Pass tenantId so profile and consent artifacts stay tenant-scoped.\",\n statusCode: 400,\n });\n }\n return value;\n}\n\nfunction skillVector(input: Partial<Record<SkillArea, number>>): Record<SkillArea, number> {\n return {\n code: input.code ?? 0,\n growth: input.growth ?? 0,\n brand: input.brand ?? 0,\n support: input.support ?? 0,\n };\n}\n\nfunction strongestSkill(vector: Record<SkillArea, number>): SkillArea {\n return (\n (Object.entries(vector) as Array<[SkillArea, number]>).sort(\n ([leftKey, left], [rightKey, right]) => right - left || leftKey.localeCompare(rightKey),\n )[0]?.[0] ?? \"code\"\n );\n}\n\nfunction weakestSkill(vector: Record<SkillArea, number>): SkillArea {\n return (\n (Object.entries(vector) as Array<[SkillArea, number]>).sort(\n ([leftKey, left], [rightKey, right]) => left - right || leftKey.localeCompare(rightKey),\n )[0]?.[0] ?? \"growth\"\n );\n}\n\nfunction sharedCount(left: readonly string[], right: readonly string[]): number {\n const rightSet = new Set(right.map((item) => item.toLowerCase()));\n return left.filter((item) => rightSet.has(item.toLowerCase())).length;\n}\n\nfunction consentKey(from: string, to: string): string {\n return `${from}->${to}`;\n}\n\nexport class CofounderMatch {\n readonly #tenantId: string;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore;\n readonly #eventLog: EventLog;\n readonly #profiles = new Map<string, FounderProfile>();\n readonly #interests = new Set<string>();\n\n private constructor(\n options: Required<Pick<CofounderMatchOptions, \"tenantId\" | \"contentStore\" | \"eventLog\">> &\n Pick<CofounderMatchOptions, \"debugRoot\">,\n ) {\n this.#tenantId = requireTenant(options.tenantId);\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#eventLog = options.eventLog;\n }\n\n static async open(\n root = \".nebutra/cofounder-match\",\n options: Omit<CofounderMatchOptions, \"contentStore\" | \"eventLog\"> = {},\n ): Promise<CofounderMatch> {\n const tenantId = options.tenantId ?? \"local\";\n await mkdir(root, { recursive: true });\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n const eventLog = await EventLog.open(join(root, \"event-log\"), { tenantId });\n return new CofounderMatch({ ...options, tenantId, contentStore, eventLog });\n }\n\n async buildProfileFromActivity(input: ActivityProfileInput): Promise<FounderProfile> {\n const profile: FounderProfile = {\n founderId: input.founderId,\n activeDays: input.activeDays,\n activityVerified: input.activeDays >= 30,\n ...(input.activeDays < 30\n ? { lockedReason: \"Cofounder matching unlocks after 30 days of real activity.\" }\n : {}),\n skillVector: skillVector(input.playCounts),\n domainTags: input.domainTags,\n communicationStyle: input.communicationSamples,\n };\n const path = `cofounder-match/profiles/${input.founderId}.json`;\n const snapshot = `${JSON.stringify(profile, null, 2)}\\n`;\n await this.#contentStore.write(path, snapshot);\n this.#profiles.set(input.founderId, profile);\n await this.#debug({\n type: \"profile\",\n tenantId: this.#tenantId,\n founderId: input.founderId,\n activityVerified: profile.activityVerified,\n });\n return profile;\n }\n\n scoreMatch(left: FounderProfile, right: FounderProfile): MatchScore {\n const leftWeak = weakestSkill(left.skillVector);\n const rightStrong = strongestSkill(right.skillVector);\n const rightWeak = weakestSkill(right.skillVector);\n const leftStrong = strongestSkill(left.skillVector);\n const growthFill = left.skillVector.growth <= 10 && right.skillVector.growth >= 30 ? 45 : 0;\n const codeFill = right.skillVector.code <= 10 && left.skillVector.code >= 30 ? 25 : 0;\n const supportFill = left.skillVector.support <= 5 && right.skillVector.support >= 15 ? 10 : 0;\n const brandFill = right.skillVector.brand <= 5 && left.skillVector.brand >= 10 ? 10 : 0;\n const complements = Math.min(100, 20 + growthFill + codeFill + supportFill + brandFill);\n const domainAdjacency = Math.min(100, 55 + sharedCount(left.domainTags, right.domainTags) * 20);\n const paceCompatibility = Math.max(40, 100 - Math.abs(left.activeDays - right.activeDays));\n const overall = Math.round(\n Math.min(100, complements * 0.55 + domainAdjacency * 0.25 + paceCompatibility * 0.2),\n );\n const why = [\n `${right.founderId} is strong in ${rightStrong[0]?.toUpperCase()}${rightStrong.slice(\n 1,\n )}, which complements ${left.founderId}'s ${leftWeak} gap.`,\n `${left.founderId} is strong in ${leftStrong}, which balances ${right.founderId}'s ${rightWeak} gap.`,\n `Shared domains: ${sharedCount(left.domainTags, right.domainTags)}.`,\n ];\n return {\n leftFounderId: left.founderId,\n rightFounderId: right.founderId,\n overall,\n skillComplementarity: Math.min(100, complements),\n domainAdjacency,\n paceCompatibility,\n why,\n };\n }\n\n async expressInterest(fromFounderId: string, toFounderId: string): Promise<void> {\n this.#interests.add(consentKey(fromFounderId, toFounderId));\n await this.#debug({ type: \"interest\", tenantId: this.#tenantId, fromFounderId, toFounderId });\n }\n\n async startChat(leftFounderId: string, rightFounderId: string): Promise<ChatThread> {\n if (\n !this.#interests.has(consentKey(leftFounderId, rightFounderId)) ||\n !this.#interests.has(consentKey(rightFounderId, leftFounderId))\n ) {\n throw new CapabilityError(\"cofounder-match\", \"Mutual consent is required before chat\", {\n suggestion: \"Wait for mutual interest before opening a cofounder intro thread.\",\n statusCode: 403,\n });\n }\n const thread: ChatThread = {\n threadId: assetId(\"cofounder_chat\", `${leftFounderId}_${rightFounderId}`),\n threadKind: \"mutual-consent-intro\",\n founders: [leftFounderId, rightFounderId],\n icebreaker: `${leftFounderId}, start with the specific work you admired in ${rightFounderId}'s profile.`,\n };\n const path = `cofounder-match/chats/${thread.threadId}.json`;\n const snapshot = `${JSON.stringify(thread, null, 2)}\\n`;\n await this.#contentStore.write(path, snapshot);\n await this.#eventLog.commit({\n traceId: assetId(\"cofounder_match_chat\", thread.threadId),\n kind: \"content_write\",\n affected: [path],\n parent: null,\n snapshot: { [path]: snapshot },\n });\n await this.#debug({\n type: \"start_chat\",\n tenantId: this.#tenantId,\n founders: thread.founders,\n });\n return thread;\n }\n\n async doctor(): Promise<CofounderMatchDoctorReport> {\n const report: CofounderMatchDoctorReport = {\n capability: \"cofounder-match\",\n ok: true,\n checkedAt: new Date().toISOString(),\n features: [\"activity-verified-profile\", \"explainable-score\", \"mutual-consent-intro\"],\n suggestion:\n \"Local matching contracts are ready; encrypted chat and identity providers remain adapter handoffs.\",\n };\n await this.#debug({ type: \"doctor\", tenantId: this.#tenantId, profiles: this.#profiles.size });\n return report;\n }\n\n async close(): Promise<void> {\n await this.#contentStore.close();\n }\n\n async #debug(entry: Record<string, unknown>): Promise<void> {\n await appendCapabilityDebug(\"cofounder-match\", entry, { root: this.#debugRoot });\n }\n}\n\nexport async function readCofounderMatchDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n return readCapabilityDebug(\"cofounder-match\", { root, limit });\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,43 @@
1
+ import {
2
+ CofounderMatch,
3
+ readCofounderMatchDebug
4
+ } from "./chunk-ZXTCDLEE.js";
5
+
6
+ // src/cli.ts
7
+ var command = process.argv[2] ?? "doctor";
8
+ var root = process.env.COFOUNDER_MATCH_ROOT ?? ".nebutra/cofounder-match";
9
+ var tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
10
+ if (command === "doctor") {
11
+ const match = await CofounderMatch.open(root, { tenantId });
12
+ try {
13
+ process.stdout.write(`${JSON.stringify(await match.doctor(), null, 2)}
14
+ `);
15
+ } finally {
16
+ await match.close();
17
+ }
18
+ } else if (command === "quickstart") {
19
+ const match = await CofounderMatch.open(root, { tenantId });
20
+ try {
21
+ const profile = await match.buildProfileFromActivity({
22
+ founderId: "alice",
23
+ activeDays: 31,
24
+ playCounts: { code: 20, growth: 2, brand: 4, support: 1 },
25
+ domainTags: ["developer-tools"],
26
+ communicationSamples: ["direct technical updates"]
27
+ });
28
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}
29
+ `);
30
+ } finally {
31
+ await match.close();
32
+ }
33
+ } else if (command === "debug") {
34
+ process.stdout.write(
35
+ `${JSON.stringify({ capability: "cofounder-match", entries: await readCofounderMatchDebug() }, null, 2)}
36
+ `
37
+ );
38
+ } else {
39
+ process.stderr.write(`Unknown cofounder-match command: ${command}
40
+ `);
41
+ process.exitCode = 1;
42
+ }
43
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { CofounderMatch, readCofounderMatchDebug } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst root = process.env.COFOUNDER_MATCH_ROOT ?? \".nebutra/cofounder-match\";\nconst tenantId = process.env.NEBUTRA_TENANT_ID ?? \"local\";\n\nif (command === \"doctor\") {\n const match = await CofounderMatch.open(root, { tenantId });\n try {\n process.stdout.write(`${JSON.stringify(await match.doctor(), null, 2)}\\n`);\n } finally {\n await match.close();\n }\n} else if (command === \"quickstart\") {\n const match = await CofounderMatch.open(root, { tenantId });\n try {\n const profile = await match.buildProfileFromActivity({\n founderId: \"alice\",\n activeDays: 31,\n playCounts: { code: 20, growth: 2, brand: 4, support: 1 },\n domainTags: [\"developer-tools\"],\n communicationSamples: [\"direct technical updates\"],\n });\n process.stdout.write(`${JSON.stringify(profile, null, 2)}\\n`);\n } finally {\n await match.close();\n }\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"cofounder-match\", entries: await readCofounderMatchDebug() }, null, 2)}\\n`,\n );\n} else {\n process.stderr.write(`Unknown cofounder-match command: ${command}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;AAEA,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,OAAO,QAAQ,IAAI,wBAAwB;AACjD,IAAM,WAAW,QAAQ,IAAI,qBAAqB;AAElD,IAAI,YAAY,UAAU;AACxB,QAAM,QAAQ,MAAM,eAAe,KAAK,MAAM,EAAE,SAAS,CAAC;AAC1D,MAAI;AACF,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAC3E,UAAE;AACA,UAAM,MAAM,MAAM;AAAA,EACpB;AACF,WAAW,YAAY,cAAc;AACnC,QAAM,QAAQ,MAAM,eAAe,KAAK,MAAM,EAAE,SAAS,CAAC;AAC1D,MAAI;AACF,UAAM,UAAU,MAAM,MAAM,yBAAyB;AAAA,MACnD,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,YAAY,EAAE,MAAM,IAAI,QAAQ,GAAG,OAAO,GAAG,SAAS,EAAE;AAAA,MACxD,YAAY,CAAC,iBAAiB;AAAA,MAC9B,sBAAsB,CAAC,0BAA0B;AAAA,IACnD,CAAC;AACD,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAC9D,UAAE;AACA,UAAM,MAAM,MAAM;AAAA,EACpB;AACF,WAAW,YAAY,SAAS;AAC9B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,mBAAmB,SAAS,MAAM,wBAAwB,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EACzG;AACF,OAAO;AACL,UAAQ,OAAO,MAAM,oCAAoC,OAAO;AAAA,CAAI;AACpE,UAAQ,WAAW;AACrB;","names":[]}
@@ -0,0 +1,62 @@
1
+ import { ContentStore } from '@nebutra/content-store';
2
+ import { EventLog } from '@nebutra/event-log';
3
+
4
+ type SkillArea = "code" | "growth" | "brand" | "support";
5
+ interface ActivityProfileInput {
6
+ readonly founderId: string;
7
+ readonly activeDays: number;
8
+ readonly playCounts: Partial<Record<SkillArea, number>>;
9
+ readonly domainTags: readonly string[];
10
+ readonly communicationSamples: readonly string[];
11
+ }
12
+ interface FounderProfile {
13
+ readonly founderId: string;
14
+ readonly activityVerified: boolean;
15
+ readonly lockedReason?: string;
16
+ readonly activeDays: number;
17
+ readonly skillVector: Record<SkillArea, number>;
18
+ readonly domainTags: readonly string[];
19
+ readonly communicationStyle: readonly string[];
20
+ }
21
+ interface MatchScore {
22
+ readonly leftFounderId: string;
23
+ readonly rightFounderId: string;
24
+ readonly overall: number;
25
+ readonly skillComplementarity: number;
26
+ readonly domainAdjacency: number;
27
+ readonly paceCompatibility: number;
28
+ readonly why: readonly string[];
29
+ }
30
+ interface ChatThread {
31
+ readonly threadId: string;
32
+ readonly threadKind: "mutual-consent-intro";
33
+ readonly founders: readonly [string, string];
34
+ readonly icebreaker: string;
35
+ }
36
+ interface CofounderMatchDoctorReport {
37
+ readonly capability: "cofounder-match";
38
+ readonly ok: boolean;
39
+ readonly checkedAt: string;
40
+ readonly features: readonly string[];
41
+ readonly suggestion?: string;
42
+ }
43
+ interface CofounderMatchOptions {
44
+ readonly tenantId?: string;
45
+ readonly debugRoot?: string;
46
+ readonly contentStore?: ContentStore;
47
+ readonly eventLog?: EventLog;
48
+ }
49
+ declare class CofounderMatch {
50
+ #private;
51
+ private constructor();
52
+ static open(root?: string, options?: Omit<CofounderMatchOptions, "contentStore" | "eventLog">): Promise<CofounderMatch>;
53
+ buildProfileFromActivity(input: ActivityProfileInput): Promise<FounderProfile>;
54
+ scoreMatch(left: FounderProfile, right: FounderProfile): MatchScore;
55
+ expressInterest(fromFounderId: string, toFounderId: string): Promise<void>;
56
+ startChat(leftFounderId: string, rightFounderId: string): Promise<ChatThread>;
57
+ doctor(): Promise<CofounderMatchDoctorReport>;
58
+ close(): Promise<void>;
59
+ }
60
+ declare function readCofounderMatchDebug(root?: string, limit?: number): Promise<unknown[]>;
61
+
62
+ export { type ActivityProfileInput, type ChatThread, CofounderMatch, type CofounderMatchDoctorReport, type CofounderMatchOptions, type FounderProfile, type MatchScore, type SkillArea, readCofounderMatchDebug };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ CofounderMatch,
3
+ readCofounderMatchDebug
4
+ } from "./chunk-ZXTCDLEE.js";
5
+ export {
6
+ CofounderMatch,
7
+ readCofounderMatchDebug
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,11 @@
1
+ import { CofounderMatch } from "../src/index";
2
+
3
+ const match = await CofounderMatch.open(".nebutra/cofounder-match-example", {
4
+ tenantId: "local",
5
+ });
6
+
7
+ try {
8
+ process.stdout.write(`${JSON.stringify(await match.doctor(), null, 2)}\n`);
9
+ } finally {
10
+ await match.close();
11
+ }
@@ -0,0 +1,13 @@
1
+ import { CofounderMatch } from "../src/index";
2
+
3
+ const match = await CofounderMatch.open(".nebutra/cofounder-match-consent-example", {
4
+ tenantId: "local",
5
+ });
6
+
7
+ try {
8
+ await match.expressInterest("alice", "sarah");
9
+ await match.expressInterest("sarah", "alice");
10
+ process.stdout.write(`${JSON.stringify(await match.startChat("alice", "sarah"), null, 2)}\n`);
11
+ } finally {
12
+ await match.close();
13
+ }
@@ -0,0 +1,18 @@
1
+ import { CofounderMatch } from "../src/index";
2
+
3
+ const match = await CofounderMatch.open(".nebutra/cofounder-match-example", {
4
+ tenantId: "local",
5
+ });
6
+
7
+ try {
8
+ const profile = await match.buildProfileFromActivity({
9
+ founderId: "alice",
10
+ activeDays: 31,
11
+ playCounts: { code: 40, growth: 3, brand: 8, support: 2 },
12
+ domainTags: ["developer-tools"],
13
+ communicationSamples: ["direct technical updates"],
14
+ });
15
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
16
+ } finally {
17
+ await match.close();
18
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@nebutra/cofounder-match",
3
+ "version": "0.1.1",
4
+ "description": "Behavior-verified cofounder profile, matching, and mutual-consent introduction surface",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "nebutra": {
9
+ "status": "wip",
10
+ "productionReady": false,
11
+ "surface": "ecosystem-product",
12
+ "requires": [
13
+ "@nebutra/content-store for profile and match artifacts",
14
+ "@nebutra/event-log for match lifecycle checkpoints",
15
+ "@nebutra/capability-kit for debug and doctor surfaces"
16
+ ],
17
+ "gaps": [
18
+ "Encrypted chat transport is represented as a mutual-consent handoff",
19
+ "Identity verification is represented as profile metadata until live providers are wired",
20
+ "Reputation and abuse review remain deterministic local records"
21
+ ],
22
+ "featureId": "cofounder-match",
23
+ "category": "ai",
24
+ "summary": "Real-activity-based founder matching with explainable complementarity"
25
+ },
26
+ "main": "./src/index.ts",
27
+ "types": "./src/index.ts",
28
+ "exports": {
29
+ ".": "./src/index.ts"
30
+ },
31
+ "dependencies": {
32
+ "@nebutra/capability-kit": "0.2.0",
33
+ "@nebutra/content-store": "0.1.0",
34
+ "@nebutra/errors": "0.1.0",
35
+ "@nebutra/event-log": "0.1.0",
36
+ "@nebutra/generation-context": "0.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.19.15",
40
+ "tsup": "^8.5.1",
41
+ "tsx": "^4.21.0",
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^4.1.4"
44
+ },
45
+ "homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/cofounder-match#readme",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
49
+ "directory": "packages/ai/cofounder-match"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "test": "vitest run",
60
+ "typecheck": "tsc --noEmit"
61
+ }
62
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: seek_cofounder
3
+ kind: play
4
+ version: 1.0.0
5
+ description: Build an activity-verified profile and surface explainable cofounder matches
6
+ required_skills:
7
+ - content_store.write
8
+ - event_log.commit
9
+ sub_agents:
10
+ - role: match_explainer
11
+ allowed_skills: [content_store.write]
12
+ depends_on_plays: []
13
+ ---
14
+
15
+ ## What this play does
16
+
17
+ Builds a behavior-derived founder profile and explains complementary matches.
18
+
19
+ ## Boundaries
20
+
21
+ - Matching is locked until the founder has at least 30 days of real activity.
22
+ - Chat opens only after mutual consent.
23
+ - Match explanations must be visible; hidden algorithmic matching is not allowed.
package/src/cli.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { CofounderMatch, readCofounderMatchDebug } from "./index";
2
+
3
+ const command = process.argv[2] ?? "doctor";
4
+ const root = process.env.COFOUNDER_MATCH_ROOT ?? ".nebutra/cofounder-match";
5
+ const tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
6
+
7
+ if (command === "doctor") {
8
+ const match = await CofounderMatch.open(root, { tenantId });
9
+ try {
10
+ process.stdout.write(`${JSON.stringify(await match.doctor(), null, 2)}\n`);
11
+ } finally {
12
+ await match.close();
13
+ }
14
+ } else if (command === "quickstart") {
15
+ const match = await CofounderMatch.open(root, { tenantId });
16
+ try {
17
+ const profile = await match.buildProfileFromActivity({
18
+ founderId: "alice",
19
+ activeDays: 31,
20
+ playCounts: { code: 20, growth: 2, brand: 4, support: 1 },
21
+ domainTags: ["developer-tools"],
22
+ communicationSamples: ["direct technical updates"],
23
+ });
24
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
25
+ } finally {
26
+ await match.close();
27
+ }
28
+ } else if (command === "debug") {
29
+ process.stdout.write(
30
+ `${JSON.stringify({ capability: "cofounder-match", entries: await readCofounderMatchDebug() }, null, 2)}\n`,
31
+ );
32
+ } else {
33
+ process.stderr.write(`Unknown cofounder-match command: ${command}\n`);
34
+ process.exitCode = 1;
35
+ }
@@ -0,0 +1,78 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { CofounderMatch, readCofounderMatchDebug } from "./index";
6
+
7
+ let root: string | undefined;
8
+ let matcher: CofounderMatch | undefined;
9
+
10
+ afterEach(async () => {
11
+ if (matcher) await matcher.close();
12
+ if (root) await rm(root, { recursive: true, force: true });
13
+ root = undefined;
14
+ matcher = undefined;
15
+ });
16
+
17
+ async function open(): Promise<CofounderMatch> {
18
+ root = await mkdtemp(join(tmpdir(), "cofounder-match-"));
19
+ matcher = await CofounderMatch.open(root, { tenantId: "tenant_a" });
20
+ return matcher;
21
+ }
22
+
23
+ describe("cofounder-match", () => {
24
+ it("requires real activity before a founder can enter matching", async () => {
25
+ const runtime = await open();
26
+
27
+ const profile = await runtime.buildProfileFromActivity({
28
+ founderId: "alice",
29
+ activeDays: 12,
30
+ playCounts: { code: 12, growth: 0, brand: 3, support: 0 },
31
+ domainTags: ["developer-tools"],
32
+ communicationSamples: ["direct and technical"],
33
+ });
34
+
35
+ expect(profile.activityVerified).toBe(false);
36
+ expect(profile.lockedReason).toContain("30 days");
37
+ });
38
+
39
+ it("scores complementary founders with a transparent why explanation", async () => {
40
+ const runtime = await open();
41
+ const builder = await runtime.buildProfileFromActivity({
42
+ founderId: "alice",
43
+ activeDays: 45,
44
+ playCounts: { code: 120, growth: 4, brand: 12, support: 3 },
45
+ domainTags: ["developer-tools", "debugging"],
46
+ communicationSamples: ["short technical updates"],
47
+ });
48
+ const seller = await runtime.buildProfileFromActivity({
49
+ founderId: "sarah",
50
+ activeDays: 60,
51
+ playCounts: { code: 3, growth: 90, brand: 6, support: 18 },
52
+ domainTags: ["developer-tools", "sales"],
53
+ communicationSamples: ["warm customer notes"],
54
+ });
55
+
56
+ const score = runtime.scoreMatch(builder, seller);
57
+
58
+ expect(builder.activityVerified).toBe(true);
59
+ expect(score.overall).toBeGreaterThan(70);
60
+ expect(score.why.some((reason) => reason.includes("Growth"))).toBe(true);
61
+ });
62
+
63
+ it("opens chat only after mutual consent", async () => {
64
+ const runtime = await open();
65
+
66
+ await runtime.expressInterest("alice", "sarah");
67
+ await expect(runtime.startChat("alice", "sarah")).rejects.toMatchObject({
68
+ capability: "cofounder-match",
69
+ suggestion: expect.stringContaining("mutual"),
70
+ });
71
+ await runtime.expressInterest("sarah", "alice");
72
+ const chat = await runtime.startChat("alice", "sarah");
73
+
74
+ expect(chat.threadKind).toBe("mutual-consent-intro");
75
+ expect(chat.icebreaker).toContain("alice");
76
+ await expect(readCofounderMatchDebug(root)).resolves.toEqual(expect.any(Array));
77
+ });
78
+ });