@nebutra/idea-plaza 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 +2 -0
- package/.nebutra/debug/idea-plaza.jsonl +3 -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 +27 -0
- package/dist/chunk-2WSISCFT.js +147 -0
- package/dist/chunk-2WSISCFT.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/examples/doctor.ts +9 -0
- package/examples/fork.ts +19 -0
- package/examples/quickstart.ts +16 -0
- package/package.json +64 -0
- package/plays/publish_idea/SKILL.md +23 -0
- package/src/cli.ts +36 -0
- package/src/index.test.ts +73 -0
- package/src/index.ts +235 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +9 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
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 IdeaPublishLevel = "surface" | "detail" | "cloneable";
|
|
11
|
+
export type FeedSort = "hot" | "new" | "near_you";
|
|
12
|
+
export type { SensitiveField } from "@nebutra/ecosystem-safety";
|
|
13
|
+
export { scanForSensitiveFields } from "@nebutra/ecosystem-safety";
|
|
14
|
+
|
|
15
|
+
export interface CemeteryWarning {
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly cause: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PublishIdeaRequest {
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly oneLine: string;
|
|
23
|
+
readonly level: IdeaPublishLevel;
|
|
24
|
+
readonly body?: string;
|
|
25
|
+
readonly tags: readonly string[];
|
|
26
|
+
readonly redactions?: readonly string[];
|
|
27
|
+
readonly cemeteryWarnings?: readonly CemeteryWarning[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PublishedIdea {
|
|
31
|
+
readonly ideaId: string;
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly oneLine: string;
|
|
34
|
+
readonly level: IdeaPublishLevel;
|
|
35
|
+
readonly tags: readonly string[];
|
|
36
|
+
readonly redactions: readonly string[];
|
|
37
|
+
readonly cemeteryWarnings: readonly CemeteryWarning[];
|
|
38
|
+
readonly publicationPath: string;
|
|
39
|
+
readonly eventId: string;
|
|
40
|
+
readonly publishedAt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ForkRequest {
|
|
44
|
+
readonly newFounderId: string;
|
|
45
|
+
readonly inheritPlayPreferences?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface IdeaFork {
|
|
49
|
+
readonly forkId: string;
|
|
50
|
+
readonly sourceIdeaId: string;
|
|
51
|
+
readonly newFounderId: string;
|
|
52
|
+
readonly inheritPlayPreferences: boolean;
|
|
53
|
+
readonly attribution: {
|
|
54
|
+
readonly displayInNewCompany: true;
|
|
55
|
+
readonly publicLineage: true;
|
|
56
|
+
};
|
|
57
|
+
readonly createdAt: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface FeedRequest {
|
|
61
|
+
readonly sort: FeedSort;
|
|
62
|
+
readonly limit: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IdeaFeed {
|
|
66
|
+
readonly items: readonly PublishedIdea[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface IdeaPlazaDoctorReport {
|
|
70
|
+
readonly capability: "idea-plaza";
|
|
71
|
+
readonly ok: boolean;
|
|
72
|
+
readonly checkedAt: string;
|
|
73
|
+
readonly features: readonly string[];
|
|
74
|
+
readonly suggestion?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IdeaPlazaOptions {
|
|
78
|
+
readonly tenantId?: string;
|
|
79
|
+
readonly debugRoot?: string;
|
|
80
|
+
readonly contentStore?: ContentStore;
|
|
81
|
+
readonly eventLog?: EventLog;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requireTenant(value: string | undefined): string {
|
|
85
|
+
if (!value?.trim()) {
|
|
86
|
+
throw new CapabilityError("idea-plaza", "Idea Plaza requires tenant context", {
|
|
87
|
+
suggestion: "Pass tenantId so publish snapshots stay tenant-scoped.",
|
|
88
|
+
statusCode: 400,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bodyForScan(request: PublishIdeaRequest): string {
|
|
95
|
+
return [request.title, request.oneLine, request.body ?? "", request.tags.join(" ")].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class IdeaPlaza {
|
|
99
|
+
readonly #tenantId: string;
|
|
100
|
+
readonly #debugRoot: string;
|
|
101
|
+
readonly #contentStore: ContentStore;
|
|
102
|
+
readonly #eventLog: EventLog;
|
|
103
|
+
readonly #ideas = new Map<string, PublishedIdea>();
|
|
104
|
+
readonly #forks = new Map<string, IdeaFork>();
|
|
105
|
+
|
|
106
|
+
private constructor(
|
|
107
|
+
options: Required<Pick<IdeaPlazaOptions, "tenantId" | "contentStore" | "eventLog">> &
|
|
108
|
+
Pick<IdeaPlazaOptions, "debugRoot">,
|
|
109
|
+
) {
|
|
110
|
+
this.#tenantId = requireTenant(options.tenantId);
|
|
111
|
+
this.#debugRoot = options.debugRoot ?? process.cwd();
|
|
112
|
+
this.#contentStore = options.contentStore;
|
|
113
|
+
this.#eventLog = options.eventLog;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static async open(
|
|
117
|
+
root = ".nebutra/idea-plaza",
|
|
118
|
+
options: Omit<IdeaPlazaOptions, "contentStore" | "eventLog"> = {},
|
|
119
|
+
): Promise<IdeaPlaza> {
|
|
120
|
+
const tenantId = options.tenantId ?? "local";
|
|
121
|
+
await mkdir(root, { recursive: true });
|
|
122
|
+
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
123
|
+
const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
|
|
124
|
+
return new IdeaPlaza({ ...options, tenantId, contentStore, eventLog });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async publish(request: PublishIdeaRequest): Promise<PublishedIdea> {
|
|
128
|
+
assertPublicDisclosureSafe({
|
|
129
|
+
capability: "idea-plaza",
|
|
130
|
+
content: bodyForScan(request),
|
|
131
|
+
...(request.redactions !== undefined ? { redactions: request.redactions } : {}),
|
|
132
|
+
suggestion: "Review detected private values and pass redactions before publishing.",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const ideaId = assetId("idea", `${request.title}_${request.oneLine}`);
|
|
136
|
+
const publicationPath = `plaza/ideas/${ideaId}.json`;
|
|
137
|
+
const publishedAt = new Date().toISOString();
|
|
138
|
+
const idea: Omit<PublishedIdea, "eventId"> = {
|
|
139
|
+
ideaId,
|
|
140
|
+
title: request.title,
|
|
141
|
+
oneLine: request.oneLine,
|
|
142
|
+
level: request.level,
|
|
143
|
+
tags: request.tags,
|
|
144
|
+
redactions: request.redactions ?? [],
|
|
145
|
+
cemeteryWarnings: request.cemeteryWarnings ?? [],
|
|
146
|
+
publicationPath,
|
|
147
|
+
publishedAt,
|
|
148
|
+
};
|
|
149
|
+
const snapshot = `${JSON.stringify({ ...idea, body: request.body ?? "" }, null, 2)}\n`;
|
|
150
|
+
await this.#contentStore.write(publicationPath, snapshot);
|
|
151
|
+
const eventId = await this.#eventLog.commit({
|
|
152
|
+
traceId: assetId("idea_plaza_publish", ideaId),
|
|
153
|
+
kind: "content_write",
|
|
154
|
+
affected: [publicationPath],
|
|
155
|
+
parent: null,
|
|
156
|
+
snapshot: { [publicationPath]: snapshot },
|
|
157
|
+
});
|
|
158
|
+
const published = { ...idea, eventId };
|
|
159
|
+
this.#ideas.set(ideaId, published);
|
|
160
|
+
await this.#debug({ type: "publish", tenantId: this.#tenantId, ideaId, level: request.level });
|
|
161
|
+
return published;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async fork(ideaId: string, request: ForkRequest): Promise<IdeaFork> {
|
|
165
|
+
if (!this.#ideas.has(ideaId)) {
|
|
166
|
+
throw new CapabilityError("idea-plaza", "Idea fork source not found", {
|
|
167
|
+
suggestion: "Fork only from an explicit public or cloneable snapshot in the current feed.",
|
|
168
|
+
metadata: { ideaId },
|
|
169
|
+
statusCode: 404,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const fork: IdeaFork = {
|
|
173
|
+
forkId: assetId("idea_fork", `${ideaId}_${request.newFounderId}`),
|
|
174
|
+
sourceIdeaId: ideaId,
|
|
175
|
+
newFounderId: request.newFounderId,
|
|
176
|
+
inheritPlayPreferences: request.inheritPlayPreferences ?? false,
|
|
177
|
+
attribution: {
|
|
178
|
+
displayInNewCompany: true,
|
|
179
|
+
publicLineage: true,
|
|
180
|
+
},
|
|
181
|
+
createdAt: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
const path = `plaza/forks/${fork.forkId}.json`;
|
|
184
|
+
const snapshot = `${JSON.stringify(fork, null, 2)}\n`;
|
|
185
|
+
await this.#contentStore.write(path, snapshot);
|
|
186
|
+
await this.#eventLog.commit({
|
|
187
|
+
traceId: assetId("idea_plaza_fork", fork.forkId),
|
|
188
|
+
kind: "content_write",
|
|
189
|
+
affected: [path],
|
|
190
|
+
parent: null,
|
|
191
|
+
snapshot: { [path]: snapshot },
|
|
192
|
+
});
|
|
193
|
+
this.#forks.set(fork.forkId, fork);
|
|
194
|
+
await this.#debug({ type: "fork", tenantId: this.#tenantId, ideaId, forkId: fork.forkId });
|
|
195
|
+
return fork;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async feed(request: FeedRequest): Promise<IdeaFeed> {
|
|
199
|
+
const items = [...this.#ideas.values()]
|
|
200
|
+
.sort((left, right) =>
|
|
201
|
+
request.sort === "new"
|
|
202
|
+
? right.publishedAt.localeCompare(left.publishedAt)
|
|
203
|
+
: right.cemeteryWarnings.length - left.cemeteryWarnings.length ||
|
|
204
|
+
right.publishedAt.localeCompare(left.publishedAt),
|
|
205
|
+
)
|
|
206
|
+
.slice(0, request.limit);
|
|
207
|
+
await this.#debug({ type: "feed", tenantId: this.#tenantId, sort: request.sort });
|
|
208
|
+
return { items };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async doctor(): Promise<IdeaPlazaDoctorReport> {
|
|
212
|
+
const report: IdeaPlazaDoctorReport = {
|
|
213
|
+
capability: "idea-plaza",
|
|
214
|
+
ok: true,
|
|
215
|
+
checkedAt: new Date().toISOString(),
|
|
216
|
+
features: ["publish", "redact", "fork-lineage", "feed", "cemetery-warning"],
|
|
217
|
+
suggestion:
|
|
218
|
+
"Local snapshots are active; public registry transport and moderation queues remain adapter handoffs.",
|
|
219
|
+
};
|
|
220
|
+
await this.#debug({ type: "doctor", tenantId: this.#tenantId, ideas: this.#ideas.size });
|
|
221
|
+
return report;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async close(): Promise<void> {
|
|
225
|
+
await this.#contentStore.close();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async #debug(entry: Record<string, unknown>): Promise<void> {
|
|
229
|
+
await appendCapabilityDebug("idea-plaza", entry, { root: this.#debugRoot });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function readIdeaPlazaDebug(root = process.cwd(), limit = 20): Promise<unknown[]> {
|
|
234
|
+
return readCapabilityDebug("idea-plaza", { root, limit });
|
|
235
|
+
}
|
package/tsconfig.json
ADDED