@oa-sdk/spec-bundler 0.1.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.
@@ -0,0 +1,14 @@
1
+ import type { TopicProfile, ResolvedFileEntry, DocumentCategory } from "./types.js";
2
+ export interface ContextGeneratorOptions {
3
+ boundaryTerminals?: string[];
4
+ upstreamNodes?: string[];
5
+ nodeCategories?: Map<string, DocumentCategory>;
6
+ }
7
+ /**
8
+ * Automatically synthesizes a standardized CONTEXT.md file at the bundle root
9
+ * summarizing the topic's objectives, boundaries, ambient background context,
10
+ * directional mode (downstream/upstream/bidirectional), and categorized file index.
11
+ * Supports custom templates via context.template with macro token substitution.
12
+ */
13
+ export declare function generateTopicContextMarkdown(topicName: string, profile: TopicProfile, files: ResolvedFileEntry[], boundaryTerminalsOrOptions?: string[] | ContextGeneratorOptions): string;
14
+ //# sourceMappingURL=context-generator.d.ts.map
@@ -0,0 +1,297 @@
1
+ import { parseDocumentCategory } from "./category-parser.js";
2
+ import fs from "node:fs";
3
+ const CODE_FENCE = "```";
4
+ /**
5
+ * Automatically synthesizes a standardized CONTEXT.md file at the bundle root
6
+ * summarizing the topic's objectives, boundaries, ambient background context,
7
+ * directional mode (downstream/upstream/bidirectional), and categorized file index.
8
+ * Supports custom templates via context.template with macro token substitution.
9
+ */
10
+ export function generateTopicContextMarkdown(topicName, profile, files, boundaryTerminalsOrOptions = []) {
11
+ const options = Array.isArray(boundaryTerminalsOrOptions)
12
+ ? { boundaryTerminals: boundaryTerminalsOrOptions }
13
+ : boundaryTerminalsOrOptions;
14
+ const boundaryTerminals = options.boundaryTerminals ?? [];
15
+ const upstreamNodes = new Set(options.upstreamNodes ?? []);
16
+ const nodeCategories = options.nodeCategories ?? new Map();
17
+ const context = profile.context ?? {};
18
+ const scope = profile.scope ?? profile.treeShake ?? { entrypoints: [] };
19
+ const direction = scope.direction ?? "downstream";
20
+ // 1. Header & Title
21
+ const title = context.description || profile.description || `Topic Context: ${topicName}`;
22
+ // 2. Phase Badge
23
+ const phaseLabels = {
24
+ develop: "`๐Ÿ› ๏ธ Development` (Feature Implementation)",
25
+ fix: "`๐Ÿ› Bugfix & Refactoring` (Targeted Fix & Upstream Coordination)",
26
+ test: "`๐Ÿงช Testing & QA` (Contract & Error Invariant Verification)",
27
+ review: "`๐Ÿ” Architecture & Peer Review` (Compact System Briefing)",
28
+ };
29
+ const phaseBadge = profile.phase ? phaseLabels[profile.phase] ?? `\`${profile.phase}\`` : "";
30
+ // 3. Metadata lines
31
+ const metadataLines = [];
32
+ metadataLines.push(`- **Topic Identifier**: \`${topicName}\``);
33
+ if (profile.phase) {
34
+ metadataLines.push(`- **Work Phase**: ${phaseLabels[profile.phase] ?? `\`${profile.phase}\``}`);
35
+ }
36
+ if (profile.author) {
37
+ metadataLines.push(`- **Initial Author / Worker**: \`${profile.author}\``);
38
+ }
39
+ if (context.domain) {
40
+ metadataLines.push(`- **Domain**: \`${context.domain}\``);
41
+ }
42
+ if (context.issue) {
43
+ metadataLines.push(`- **Target Issue / Ticket**: \`${context.issue}\``);
44
+ }
45
+ if (context.persona) {
46
+ metadataLines.push(`- **Target Role / Persona**: \`${context.persona}\``);
47
+ }
48
+ if (context.tags && context.tags.length > 0) {
49
+ metadataLines.push(`- **Tags**: ${context.tags.map((t) => `\`${t}\``).join(", ")}`);
50
+ }
51
+ if (context.prerequisites && context.prerequisites.length > 0) {
52
+ metadataLines.push(`- **Prerequisites**: ${context.prerequisites.map((p) => `\`${p}\``).join(", ")}`);
53
+ }
54
+ if (context.metadata && Object.keys(context.metadata).length > 0) {
55
+ for (const [k, v] of Object.entries(context.metadata)) {
56
+ metadataLines.push(`- **${k}**: \`${JSON.stringify(v)}\``);
57
+ }
58
+ }
59
+ const metadataSection = metadataLines.join("\n");
60
+ // 4. Instructions
61
+ const instructionLines = [];
62
+ if (profile.phase === "fix") {
63
+ instructionLines.push("> [!IMPORTANT]");
64
+ instructionLines.push("> **Bugfix & Modification Slice (Preserve Upstream Caller Contracts)**: Crafted for targeted bug resolution with upstream consumer coordination. Review upstream contracts before altering behavior.");
65
+ }
66
+ else if (profile.phase === "test") {
67
+ instructionLines.push("> [!TIP]");
68
+ instructionLines.push("> **Testing & QA Slice (Verification & Acceptance Invariants)**: Focused on acceptance criteria, contract guarantees, and error catalogs for deterministic verification.");
69
+ }
70
+ else if (profile.phase === "review") {
71
+ instructionLines.push("> [!NOTE]");
72
+ instructionLines.push("> **Review Slice (High-Level Review Contract)**: Condensed contract and architectural summary tailored for efficient PR/RFC evaluation without implementation bloat.");
73
+ }
74
+ if (context.prompt) {
75
+ if (instructionLines.length > 0)
76
+ instructionLines.push("");
77
+ instructionLines.push("> [!NOTE]");
78
+ instructionLines.push(`> ${context.prompt.replace(/\r?\n/g, "\n> ")}`);
79
+ }
80
+ const instructionsSection = instructionLines.join("\n");
81
+ // 5. Scope & Direction
82
+ const scopeLines = [];
83
+ const entrypoints = scope.entrypoints ?? [];
84
+ if (entrypoints.length > 0) {
85
+ scopeLines.push(`- **Entrypoint(s)**: ${entrypoints.map((e) => `\`${e}\``).join(", ")}`);
86
+ }
87
+ scopeLines.push(`- **Traversal Direction**: \`${direction}\``);
88
+ if (direction === "upstream" || direction === "bidirectional") {
89
+ scopeLines.push(` - *Upstream Hop Depth*: ${scope.upstreamDepth ?? 1} (parent consumer / governing contracts)`);
90
+ }
91
+ if (scope.include && scope.include.length > 0) {
92
+ scopeLines.push(`- **Domain Scope (include)**: ${scope.include.map((i) => `\`${i}\``).join(", ")}`);
93
+ }
94
+ if (scope.stopAt && scope.stopAt.length > 0) {
95
+ scopeLines.push(`- **Boundary Stops (stopAt)**: ${scope.stopAt.map((s) => `\`${s}\``).join(", ")}`);
96
+ }
97
+ if (scope.maxDepth !== undefined) {
98
+ scopeLines.push(`- **Maximum Traversal Depth**: ${scope.maxDepth} hop(s)`);
99
+ }
100
+ if (scope.sections && scope.sections.length > 0) {
101
+ scopeLines.push(`- **Section Slices**: ${scope.sections.map((s) => `\`${s}\``).join(", ")}`);
102
+ }
103
+ if (scope.includeCategories && scope.includeCategories.length > 0) {
104
+ scopeLines.push(`- **Allowed Categories**: ${scope.includeCategories.map((c) => `\`${c}\``).join(", ")}`);
105
+ }
106
+ if (scope.excludeCategories && scope.excludeCategories.length > 0) {
107
+ scopeLines.push(`- **Excluded Categories**: ${scope.excludeCategories.map((c) => `\`${c}\``).join(", ")}`);
108
+ }
109
+ const scopeSection = scopeLines.join("\n");
110
+ // 6. Partition files into categories
111
+ const contextFiles = [];
112
+ const upstreamFiles = [];
113
+ const categorized = {
114
+ feature: [],
115
+ mechanism: [],
116
+ error: [],
117
+ issue: [],
118
+ convention: [],
119
+ };
120
+ for (const f of files) {
121
+ if (f.bundleDestPath === "CONTEXT.md")
122
+ continue;
123
+ if (f.bundleDestPath.startsWith("context/") || f.bundleDestPath.startsWith("_context/")) {
124
+ contextFiles.push(f);
125
+ continue;
126
+ }
127
+ if (upstreamNodes.has(f.bundleDestPath)) {
128
+ upstreamFiles.push(f);
129
+ continue;
130
+ }
131
+ let cat = nodeCategories.get(f.bundleDestPath);
132
+ if (!cat) {
133
+ try {
134
+ const raw = fs.readFileSync(f.sourceAbsolutePath, "utf8");
135
+ cat = parseDocumentCategory(raw, f.sourceAbsolutePath);
136
+ }
137
+ catch {
138
+ cat = "feature";
139
+ }
140
+ }
141
+ categorized[cat].push(f);
142
+ }
143
+ // 7. Spec Index sections
144
+ const indexLines = [];
145
+ if (upstreamFiles.length > 0) {
146
+ indexLines.push("## Upstream Consumers & Coordinating Contracts");
147
+ indexLines.push("");
148
+ indexLines.push("> [!IMPORTANT]\n> The following parent/caller contracts reference this slice. Review these for compatibility and contract alignment.");
149
+ indexLines.push("");
150
+ for (const f of upstreamFiles) {
151
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
152
+ }
153
+ indexLines.push("");
154
+ }
155
+ if (categorized.feature.length > 0) {
156
+ indexLines.push("## Bundled Specifications Index");
157
+ indexLines.push("");
158
+ for (const f of categorized.feature) {
159
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
160
+ }
161
+ indexLines.push("");
162
+ }
163
+ if (categorized.mechanism.length > 0) {
164
+ indexLines.push("## Implementation Mechanisms & Behavioral Contracts (HOW)");
165
+ indexLines.push("");
166
+ for (const f of categorized.mechanism) {
167
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
168
+ }
169
+ indexLines.push("");
170
+ }
171
+ if (categorized.error.length > 0) {
172
+ indexLines.push("## Error Scenarios & Failure Catalogs (FAILURE)");
173
+ indexLines.push("");
174
+ for (const f of categorized.error) {
175
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
176
+ }
177
+ indexLines.push("");
178
+ }
179
+ if (categorized.convention.length > 0) {
180
+ indexLines.push("## Architectural & Specification Conventions (RULE)");
181
+ indexLines.push("");
182
+ for (const f of categorized.convention) {
183
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
184
+ }
185
+ indexLines.push("");
186
+ }
187
+ if (categorized.issue.length > 0) {
188
+ indexLines.push("## Issues & Historical Context (WHY)");
189
+ indexLines.push("");
190
+ for (const f of categorized.issue) {
191
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
192
+ }
193
+ indexLines.push("");
194
+ }
195
+ if (contextFiles.length > 0) {
196
+ indexLines.push("## Ambient Background Context Documents");
197
+ indexLines.push("");
198
+ for (const f of contextFiles) {
199
+ indexLines.push(`- [${f.bundleDestPath}](./${f.bundleDestPath}) (${(f.size / 1024).toFixed(1)} KB)`);
200
+ }
201
+ indexLines.push("");
202
+ }
203
+ if (boundaryTerminals.length > 0) {
204
+ indexLines.push("## Terminal Boundary References (Unbundled Leaves)");
205
+ indexLines.push("");
206
+ indexLines.push("The following cross-cutting references were deliberately halted at the topic boundary to prevent transitive repository graph explosion:");
207
+ indexLines.push("");
208
+ for (const b of boundaryTerminals) {
209
+ indexLines.push(`- \`${b}\` *(boundary terminal, not traversed)*`);
210
+ }
211
+ indexLines.push("");
212
+ }
213
+ const specIndexSection = indexLines.join("\n");
214
+ // 8. Synthesis Guide
215
+ const hasTypeSpec = files.some((f) => f.bundleDestPath.endsWith(".tsp") ||
216
+ f.sourceAbsolutePath.endsWith(".tsp") ||
217
+ f.sourceRelativePath.endsWith(".tsp"));
218
+ let synthesisGuideSection = "";
219
+ if (hasTypeSpec) {
220
+ const guideLines = [
221
+ "## Downstream Code Synthesis & DTO Generation (TypeSpec SSOT)",
222
+ "",
223
+ "This bundle contains pure TypeSpec (`.tsp`) contract definitions with `@evidence` bindings. Downstream consumer teams can synthesize type-safe DTOs and client SDKs directly without manually writing boilerplate:",
224
+ "",
225
+ `${CODE_FENCE}bash`,
226
+ "# 1. Emit OpenAPI 3.1 enriched with x-samchon-evidence metadata",
227
+ "tsp-evidence emit --openapi openapi.json --specs specs/**/*.tsp",
228
+ "",
229
+ "# 2. Synthesize TypeScript DTOs with verified @evidence annotations",
230
+ "tsp-evidence synthesize --openapi openapi.json --out-dir src/generated",
231
+ "",
232
+ "# 3. Compile or generate polyglot models (Python, Java, Go, OpenAPI generator)",
233
+ "tsp compile . --emit @typespec/openapi3",
234
+ CODE_FENCE,
235
+ "",
236
+ ];
237
+ synthesisGuideSection = guideLines.join("\n");
238
+ }
239
+ // 9. Custom Template Support
240
+ if (context.template) {
241
+ let templateText;
242
+ if (fs.existsSync(context.template)) {
243
+ templateText = fs.readFileSync(context.template, "utf8");
244
+ }
245
+ else {
246
+ templateText = context.template;
247
+ }
248
+ const replacements = {
249
+ "{{TITLE}}": title,
250
+ "{{TOPIC_NAME}}": topicName,
251
+ "{{DESCRIPTION}}": profile.description ?? "",
252
+ "{{PHASE_BADGE}}": phaseBadge,
253
+ "{{METADATA}}": metadataSection,
254
+ "{{INSTRUCTIONS}}": instructionsSection,
255
+ "{{SCOPE}}": scopeSection,
256
+ "{{SPEC_INDEX}}": specIndexSection,
257
+ "{{SYNTHESIS_GUIDE}}": synthesisGuideSection,
258
+ "{{DATE}}": new Date().toISOString().split("T")[0],
259
+ "{{FILE_COUNT}}": String(files.length),
260
+ };
261
+ let rendered = templateText;
262
+ for (const [placeholder, val] of Object.entries(replacements)) {
263
+ rendered = rendered.split(placeholder).join(val);
264
+ }
265
+ return rendered;
266
+ }
267
+ // 10. Default Standard Markdown Synthesis
268
+ const defaultLines = [];
269
+ defaultLines.push(`# ${title}`);
270
+ defaultLines.push("");
271
+ if (profile.description) {
272
+ defaultLines.push(profile.description);
273
+ defaultLines.push("");
274
+ }
275
+ defaultLines.push("## Topic Metadata");
276
+ defaultLines.push("");
277
+ defaultLines.push(metadataSection);
278
+ defaultLines.push("");
279
+ if (instructionsSection.length > 0) {
280
+ defaultLines.push("## Agent Instructions & Guidance");
281
+ defaultLines.push("");
282
+ defaultLines.push(instructionsSection);
283
+ defaultLines.push("");
284
+ }
285
+ defaultLines.push("## Extraction Scope & Directionality");
286
+ defaultLines.push("");
287
+ defaultLines.push(scopeSection);
288
+ defaultLines.push("");
289
+ if (specIndexSection.length > 0) {
290
+ defaultLines.push(specIndexSection);
291
+ }
292
+ if (synthesisGuideSection.length > 0) {
293
+ defaultLines.push(synthesisGuideSection);
294
+ }
295
+ return defaultLines.join("\n");
296
+ }
297
+ //# sourceMappingURL=context-generator.js.map
@@ -0,0 +1,41 @@
1
+ import type { BundleLockfile, BundleManifest, FreshnessResult, LockfileMode, ResolvedFileEntry, SemanticDriftReport } from "./types.js";
2
+ /**
3
+ * Calculates a deterministic master aggregate hash across all resolved file entries.
4
+ */
5
+ export declare function calculateAggregateHash(entries: ResolvedFileEntry[]): string;
6
+ /**
7
+ * Returns the lockfile path corresponding to a manifest file and optional topic profile.
8
+ * Supports consolidated lockfile mode (bundle.manifest.lock.json) or split topic lockfiles.
9
+ */
10
+ export declare function getLockfilePath(manifestPath: string, topic?: string, lockfileMode?: LockfileMode): string;
11
+ /**
12
+ * Reads and parses an existing lockfile. Returns null if missing or invalid.
13
+ */
14
+ export declare function readLockfile(lockfilePath: string): BundleLockfile | null;
15
+ /**
16
+ * Generates an in-memory lockfile object without disk mutation.
17
+ */
18
+ export declare function generateLockfileData(manifest: BundleManifest, entries: ResolvedFileEntry[], generatedAt?: string): BundleLockfile;
19
+ /**
20
+ * Writes a formatted lockfile to disk.
21
+ * If writing in consolidated mode with a topic, updates the topic entry inside bundle.manifest.lock.json.
22
+ */
23
+ export declare function writeLockfile(lockfilePath: string, manifest: BundleManifest, entries: ResolvedFileEntry[], options?: {
24
+ topic?: string;
25
+ lockfileMode?: LockfileMode;
26
+ }): BundleLockfile;
27
+ /**
28
+ * Evaluates whether the bundle is fresh or has drifted compared to the lockfile and output file.
29
+ * Supports consolidated lockfile reading and semantic structural diff computing.
30
+ */
31
+ export declare function checkFreshness(manifest: BundleManifest, baseDir: string, lockfilePath: string, options?: {
32
+ topic?: string;
33
+ diff?: boolean;
34
+ lockfileMode?: LockfileMode;
35
+ }): FreshnessResult;
36
+ /**
37
+ * Computes semantic structural diff between source files on disk and the built bundle/lockfile.
38
+ * Detects modified anchors in Markdown and symbols in TypeSpec.
39
+ */
40
+ export declare function computeSemanticDiff(manifest: BundleManifest, baseDir: string, currentEntries: ResolvedFileEntry[], lockfile: BundleLockfile | null, topic?: string): SemanticDriftReport;
41
+ //# sourceMappingURL=freshness.d.ts.map