@nebutra/founder-cemetery 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/.nebutra/debug/content-store.jsonl +5 -0
- package/.nebutra/debug/event-log.jsonl +1 -0
- package/.nebutra/debug/founder-cemetery.jsonl +8 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +16 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +676 -0
- package/README.md +28 -0
- package/dist/chunk-X5WY4XBS.js +193 -0
- package/dist/chunk-X5WY4XBS.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/examples/doctor.ts +11 -0
- package/examples/memorial.ts +19 -0
- package/examples/quickstart.ts +16 -0
- package/package.json +64 -0
- package/plays/close_company/SKILL.md +23 -0
- package/src/cli.ts +33 -0
- package/src/index.test.ts +104 -0
- package/src/index.ts +309 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +9 -0
|
@@ -0,0 +1,104 @@
|
|
|
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 { FounderCemetery, readFounderCemeteryDebug } from "./index";
|
|
6
|
+
|
|
7
|
+
let root: string | undefined;
|
|
8
|
+
let cemetery: FounderCemetery | undefined;
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
if (cemetery) await cemetery.close();
|
|
12
|
+
if (root) await rm(root, { recursive: true, force: true });
|
|
13
|
+
root = undefined;
|
|
14
|
+
cemetery = undefined;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function open(): Promise<FounderCemetery> {
|
|
18
|
+
root = await mkdtemp(join(tmpdir(), "founder-cemetery-"));
|
|
19
|
+
cemetery = await FounderCemetery.open(root, { tenantId: "tenant_a" });
|
|
20
|
+
return cemetery;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("founder-cemetery", () => {
|
|
24
|
+
it("starts with pause as the safe default instead of one-click deletion", async () => {
|
|
25
|
+
const runtime = await open();
|
|
26
|
+
|
|
27
|
+
const flow = await runtime.startClosingFlow({
|
|
28
|
+
companyId: "loop",
|
|
29
|
+
companyName: "Loop",
|
|
30
|
+
founderIds: ["alice"],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(flow.recommendedMode).toBe("pause");
|
|
34
|
+
expect(flow.availableModes).toEqual(["pause", "archive", "cemetery"]);
|
|
35
|
+
expect(flow.requiresCoolingOff).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("extracts evidence-backed lessons and publishes only with consent", async () => {
|
|
39
|
+
const runtime = await open();
|
|
40
|
+
const analysis = await runtime.analyzeDeath({
|
|
41
|
+
companyId: "loop",
|
|
42
|
+
companyName: "Loop",
|
|
43
|
+
timelineSummaries: [
|
|
44
|
+
"Pivoted from devtools to sales without user interviews",
|
|
45
|
+
"Outbound reply rate stayed below 1 percent",
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
const lessons = await runtime.extractLessons(analysis);
|
|
49
|
+
const memorial = await runtime.publishMemorial({
|
|
50
|
+
analysis,
|
|
51
|
+
lessons,
|
|
52
|
+
publishLevel: "community",
|
|
53
|
+
consentSignatures: ["alice"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(analysis.causes[0]?.category).toBe("pmf");
|
|
57
|
+
expect(lessons[0]?.evidence).toHaveLength(2);
|
|
58
|
+
expect(memorial.path).toBe(`cemetery/${memorial.slug}.json`);
|
|
59
|
+
expect(memorial.coolingOffUntil).toEqual(expect.any(String));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("refuses public memorials without explicit consent", async () => {
|
|
63
|
+
const runtime = await open();
|
|
64
|
+
const analysis = await runtime.analyzeDeath({
|
|
65
|
+
companyId: "loop",
|
|
66
|
+
companyName: "Loop",
|
|
67
|
+
timelineSummaries: ["Ran out of runway after failed raise"],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(
|
|
71
|
+
runtime.publishMemorial({
|
|
72
|
+
analysis,
|
|
73
|
+
lessons: await runtime.extractLessons(analysis),
|
|
74
|
+
publishLevel: "public",
|
|
75
|
+
consentSignatures: [],
|
|
76
|
+
}),
|
|
77
|
+
).rejects.toMatchObject({
|
|
78
|
+
capability: "founder-cemetery",
|
|
79
|
+
suggestion: expect.stringContaining("consent"),
|
|
80
|
+
});
|
|
81
|
+
await expect(readFounderCemeteryDebug(root)).resolves.toEqual(expect.any(Array));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("refuses non-private memorials with sensitive fields unless redactions are explicit", async () => {
|
|
85
|
+
const runtime = await open();
|
|
86
|
+
const analysis = await runtime.analyzeDeath({
|
|
87
|
+
companyId: "loop",
|
|
88
|
+
companyName: "Loop",
|
|
89
|
+
timelineSummaries: ["A customer alice@example.com churned during the final sales push"],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
runtime.publishMemorial({
|
|
94
|
+
analysis,
|
|
95
|
+
lessons: await runtime.extractLessons(analysis),
|
|
96
|
+
publishLevel: "community",
|
|
97
|
+
consentSignatures: ["alice"],
|
|
98
|
+
}),
|
|
99
|
+
).rejects.toMatchObject({
|
|
100
|
+
capability: "founder-cemetery",
|
|
101
|
+
suggestion: expect.stringContaining("redactions"),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
4
|
+
import { ContentStore } from "@nebutra/content-store";
|
|
5
|
+
import { assertPublicDisclosureSafe } from "@nebutra/ecosystem-safety";
|
|
6
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
7
|
+
import { EventLog } from "@nebutra/event-log";
|
|
8
|
+
import { assetId } from "@nebutra/generation-context";
|
|
9
|
+
|
|
10
|
+
export type CloseMode = "pause" | "archive" | "cemetery";
|
|
11
|
+
export type CauseCategory =
|
|
12
|
+
| "pmf"
|
|
13
|
+
| "runway"
|
|
14
|
+
| "fundraising"
|
|
15
|
+
| "competition"
|
|
16
|
+
| "founder_conflict"
|
|
17
|
+
| "pivot";
|
|
18
|
+
export type MemorialPublishLevel = "private" | "community" | "public";
|
|
19
|
+
|
|
20
|
+
export interface ClosingFlowRequest {
|
|
21
|
+
readonly companyId: string;
|
|
22
|
+
readonly companyName: string;
|
|
23
|
+
readonly founderIds: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ClosingFlow {
|
|
27
|
+
readonly companyId: string;
|
|
28
|
+
readonly companyName: string;
|
|
29
|
+
readonly recommendedMode: "pause";
|
|
30
|
+
readonly availableModes: readonly CloseMode[];
|
|
31
|
+
readonly requiresCoolingOff: true;
|
|
32
|
+
readonly startedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DeathAnalysisRequest {
|
|
36
|
+
readonly companyId: string;
|
|
37
|
+
readonly companyName: string;
|
|
38
|
+
readonly timelineSummaries: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DeathCause {
|
|
42
|
+
readonly category: CauseCategory;
|
|
43
|
+
readonly confidence: number;
|
|
44
|
+
readonly evidence: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeathAnalysis {
|
|
48
|
+
readonly companyId: string;
|
|
49
|
+
readonly companyName: string;
|
|
50
|
+
readonly causes: readonly DeathCause[];
|
|
51
|
+
readonly narrative: string;
|
|
52
|
+
readonly evidence: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Lesson {
|
|
56
|
+
readonly title: string;
|
|
57
|
+
readonly lessonText: string;
|
|
58
|
+
readonly evidence: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PublishMemorialRequest {
|
|
62
|
+
readonly analysis: DeathAnalysis;
|
|
63
|
+
readonly lessons: readonly Lesson[];
|
|
64
|
+
readonly publishLevel: MemorialPublishLevel;
|
|
65
|
+
readonly consentSignatures: readonly string[];
|
|
66
|
+
readonly redactions?: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Memorial {
|
|
70
|
+
readonly slug: string;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly publishLevel: MemorialPublishLevel;
|
|
73
|
+
readonly coolingOffUntil: string;
|
|
74
|
+
readonly eventId: string;
|
|
75
|
+
readonly lessons: readonly Lesson[];
|
|
76
|
+
readonly redactions: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface FounderCemeteryDoctorReport {
|
|
80
|
+
readonly capability: "founder-cemetery";
|
|
81
|
+
readonly ok: boolean;
|
|
82
|
+
readonly checkedAt: string;
|
|
83
|
+
readonly features: readonly string[];
|
|
84
|
+
readonly suggestion?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface FounderCemeteryOptions {
|
|
88
|
+
readonly tenantId?: string;
|
|
89
|
+
readonly debugRoot?: string;
|
|
90
|
+
readonly contentStore?: ContentStore;
|
|
91
|
+
readonly eventLog?: EventLog;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireTenant(value: string | undefined): string {
|
|
95
|
+
if (!value?.trim()) {
|
|
96
|
+
throw new CapabilityError("founder-cemetery", "Founder Cemetery requires tenant context", {
|
|
97
|
+
suggestion: "Pass tenantId so closure artifacts stay tenant-scoped.",
|
|
98
|
+
statusCode: 400,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function slugify(value: string): string {
|
|
105
|
+
return value
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
108
|
+
.replace(/^-+|-+$/g, "")
|
|
109
|
+
.slice(0, 64);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inferCause(evidence: readonly string[]): DeathCause {
|
|
113
|
+
const combined = evidence.join("\n").toLowerCase();
|
|
114
|
+
if (combined.includes("runway") || combined.includes("cash")) {
|
|
115
|
+
return { category: "runway", confidence: 0.74, evidence };
|
|
116
|
+
}
|
|
117
|
+
if (combined.includes("raise") || combined.includes("fund")) {
|
|
118
|
+
return { category: "fundraising", confidence: 0.72, evidence };
|
|
119
|
+
}
|
|
120
|
+
if (combined.includes("conflict")) {
|
|
121
|
+
return { category: "founder_conflict", confidence: 0.7, evidence };
|
|
122
|
+
}
|
|
123
|
+
if (combined.includes("pivot")) {
|
|
124
|
+
return { category: "pmf", confidence: 0.78, evidence };
|
|
125
|
+
}
|
|
126
|
+
return { category: "pmf", confidence: 0.68, evidence };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function coolingOffDate(): string {
|
|
130
|
+
return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function memorialDisclosureBody(request: PublishMemorialRequest): string {
|
|
134
|
+
return [
|
|
135
|
+
request.analysis.companyName,
|
|
136
|
+
request.analysis.narrative,
|
|
137
|
+
request.analysis.evidence.join("\n"),
|
|
138
|
+
request.lessons
|
|
139
|
+
.map((lesson) => [lesson.title, lesson.lessonText, lesson.evidence.join("\n")].join("\n"))
|
|
140
|
+
.join("\n"),
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class FounderCemetery {
|
|
145
|
+
readonly #tenantId: string;
|
|
146
|
+
readonly #debugRoot: string;
|
|
147
|
+
readonly #contentStore: ContentStore;
|
|
148
|
+
readonly #eventLog: EventLog;
|
|
149
|
+
|
|
150
|
+
private constructor(
|
|
151
|
+
options: Required<Pick<FounderCemeteryOptions, "tenantId" | "contentStore" | "eventLog">> &
|
|
152
|
+
Pick<FounderCemeteryOptions, "debugRoot">,
|
|
153
|
+
) {
|
|
154
|
+
this.#tenantId = requireTenant(options.tenantId);
|
|
155
|
+
this.#debugRoot = options.debugRoot ?? process.cwd();
|
|
156
|
+
this.#contentStore = options.contentStore;
|
|
157
|
+
this.#eventLog = options.eventLog;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static async open(
|
|
161
|
+
root = ".nebutra/founder-cemetery",
|
|
162
|
+
options: Omit<FounderCemeteryOptions, "contentStore" | "eventLog"> = {},
|
|
163
|
+
): Promise<FounderCemetery> {
|
|
164
|
+
const tenantId = options.tenantId ?? "local";
|
|
165
|
+
await mkdir(root, { recursive: true });
|
|
166
|
+
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
167
|
+
const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
|
|
168
|
+
return new FounderCemetery({ ...options, tenantId, contentStore, eventLog });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async startClosingFlow(request: ClosingFlowRequest): Promise<ClosingFlow> {
|
|
172
|
+
const flow: ClosingFlow = {
|
|
173
|
+
companyId: request.companyId,
|
|
174
|
+
companyName: request.companyName,
|
|
175
|
+
recommendedMode: "pause",
|
|
176
|
+
availableModes: ["pause", "archive", "cemetery"],
|
|
177
|
+
requiresCoolingOff: true,
|
|
178
|
+
startedAt: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
await this.#debug({ type: "start_closing_flow", tenantId: this.#tenantId, ...flow });
|
|
181
|
+
return flow;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async analyzeDeath(request: DeathAnalysisRequest): Promise<DeathAnalysis> {
|
|
185
|
+
const evidence =
|
|
186
|
+
request.timelineSummaries.length > 0
|
|
187
|
+
? request.timelineSummaries
|
|
188
|
+
: ["No timeline summaries were provided."];
|
|
189
|
+
const cause = inferCause(evidence);
|
|
190
|
+
const analysis: DeathAnalysis = {
|
|
191
|
+
companyId: request.companyId,
|
|
192
|
+
companyName: request.companyName,
|
|
193
|
+
causes: [cause],
|
|
194
|
+
evidence,
|
|
195
|
+
narrative: `${request.companyName} appears to have stalled around ${cause.category}.`,
|
|
196
|
+
};
|
|
197
|
+
await this.#debug({
|
|
198
|
+
type: "analyze_death",
|
|
199
|
+
tenantId: this.#tenantId,
|
|
200
|
+
companyId: request.companyId,
|
|
201
|
+
category: cause.category,
|
|
202
|
+
});
|
|
203
|
+
return analysis;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async extractLessons(analysis: DeathAnalysis): Promise<readonly Lesson[]> {
|
|
207
|
+
const title =
|
|
208
|
+
analysis.causes[0]?.category === "runway"
|
|
209
|
+
? "Validate before scaling burn"
|
|
210
|
+
: "Validate demand before widening the pitch";
|
|
211
|
+
const lesson: Lesson = {
|
|
212
|
+
title,
|
|
213
|
+
lessonText: `${analysis.companyName} should preserve the decision context and avoid repeating the highest-impact ${analysis.causes[0]?.category ?? "pmf"} mistake.`,
|
|
214
|
+
evidence: analysis.evidence.slice(0, Math.max(2, analysis.evidence.length)),
|
|
215
|
+
};
|
|
216
|
+
await this.#debug({
|
|
217
|
+
type: "extract_lessons",
|
|
218
|
+
tenantId: this.#tenantId,
|
|
219
|
+
companyId: analysis.companyId,
|
|
220
|
+
lessons: 1,
|
|
221
|
+
});
|
|
222
|
+
return [lesson];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async publishMemorial(request: PublishMemorialRequest): Promise<Memorial> {
|
|
226
|
+
if (request.consentSignatures.length === 0) {
|
|
227
|
+
throw new CapabilityError("founder-cemetery", "Memorial publication requires consent", {
|
|
228
|
+
suggestion:
|
|
229
|
+
"Collect founder consent signatures before publishing or keep the company paused.",
|
|
230
|
+
statusCode: 400,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (request.publishLevel !== "private") {
|
|
234
|
+
assertPublicDisclosureSafe({
|
|
235
|
+
capability: "founder-cemetery",
|
|
236
|
+
content: memorialDisclosureBody(request),
|
|
237
|
+
...(request.redactions !== undefined ? { redactions: request.redactions } : {}),
|
|
238
|
+
suggestion: "Review detected private values and pass redactions before publishing.",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const slug = slugify(request.analysis.companyName) || request.analysis.companyId;
|
|
243
|
+
const path = `cemetery/${slug}.json`;
|
|
244
|
+
const memorial = {
|
|
245
|
+
slug,
|
|
246
|
+
companyId: request.analysis.companyId,
|
|
247
|
+
companyName: request.analysis.companyName,
|
|
248
|
+
publishLevel: request.publishLevel,
|
|
249
|
+
coolingOffUntil: coolingOffDate(),
|
|
250
|
+
consentSignatures: request.consentSignatures,
|
|
251
|
+
redactions: request.redactions ?? [],
|
|
252
|
+
causes: request.analysis.causes,
|
|
253
|
+
lessons: request.lessons,
|
|
254
|
+
narrative: request.analysis.narrative,
|
|
255
|
+
};
|
|
256
|
+
const snapshot = `${JSON.stringify(memorial, null, 2)}\n`;
|
|
257
|
+
await this.#contentStore.write(path, snapshot);
|
|
258
|
+
const eventId = await this.#eventLog.commit({
|
|
259
|
+
traceId: assetId("founder_cemetery_publish", slug),
|
|
260
|
+
kind: "content_write",
|
|
261
|
+
affected: [path],
|
|
262
|
+
parent: null,
|
|
263
|
+
snapshot: { [path]: snapshot },
|
|
264
|
+
});
|
|
265
|
+
await this.#debug({
|
|
266
|
+
type: "publish_memorial",
|
|
267
|
+
tenantId: this.#tenantId,
|
|
268
|
+
companyId: request.analysis.companyId,
|
|
269
|
+
publishLevel: request.publishLevel,
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
slug,
|
|
273
|
+
path,
|
|
274
|
+
publishLevel: request.publishLevel,
|
|
275
|
+
coolingOffUntil: memorial.coolingOffUntil,
|
|
276
|
+
eventId,
|
|
277
|
+
lessons: request.lessons,
|
|
278
|
+
redactions: memorial.redactions,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async doctor(): Promise<FounderCemeteryDoctorReport> {
|
|
283
|
+
const report: FounderCemeteryDoctorReport = {
|
|
284
|
+
capability: "founder-cemetery",
|
|
285
|
+
ok: true,
|
|
286
|
+
checkedAt: new Date().toISOString(),
|
|
287
|
+
features: ["pause-first", "death-analysis", "lessons", "memorial", "cooling-off"],
|
|
288
|
+
suggestion:
|
|
289
|
+
"Local memorial artifacts are active; global search and video recap remain lower-layer handoffs.",
|
|
290
|
+
};
|
|
291
|
+
await this.#debug({ type: "doctor", tenantId: this.#tenantId });
|
|
292
|
+
return report;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async close(): Promise<void> {
|
|
296
|
+
await this.#contentStore.close();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async #debug(entry: Record<string, unknown>): Promise<void> {
|
|
300
|
+
await appendCapabilityDebug("founder-cemetery", entry, { root: this.#debugRoot });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function readFounderCemeteryDebug(
|
|
305
|
+
root = process.cwd(),
|
|
306
|
+
limit = 20,
|
|
307
|
+
): Promise<unknown[]> {
|
|
308
|
+
return readCapabilityDebug("founder-cemetery", { root, limit });
|
|
309
|
+
}
|
package/tsconfig.json
ADDED