@akanjs/devkit 2.3.9-rc.0 → 2.3.9-rc.10

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,335 @@
1
+ import type {
2
+ RepairReport,
3
+ WorkflowApplyReport,
4
+ WorkflowFormat,
5
+ WorkflowPlan,
6
+ WorkflowRunArtifact,
7
+ WorkflowSpec,
8
+ WorkflowValidationRunReport,
9
+ } from "./types";
10
+ import { jsonText } from "./utils";
11
+
12
+ export const renderWorkflowList = (specs: readonly WorkflowSpec[]) =>
13
+ [
14
+ "# Akan Workflows",
15
+ "",
16
+ ...specs.flatMap((spec) => [`- \`${spec.name}\`: ${spec.description}`, ` - When: ${spec.whenToUse}`]),
17
+ "",
18
+ ].join("\n");
19
+
20
+ export const renderWorkflowExplain = (spec: WorkflowSpec) =>
21
+ [
22
+ `# Workflow: ${spec.name}`,
23
+ "",
24
+ spec.description,
25
+ "",
26
+ "## When To Use",
27
+ spec.whenToUse,
28
+ "",
29
+ "## Inputs",
30
+ ...Object.entries(spec.inputs).map(
31
+ ([name, input]) =>
32
+ `- \`${name}\`${input.required ? " (required)" : ""}: ${input.description}${
33
+ input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""
34
+ }`,
35
+ ),
36
+ "",
37
+ "## Optional Surfaces",
38
+ ...Object.entries(spec.optionalSurfaces ?? {}).map(([name, mode]) => `- \`${name}\`: ${mode}`),
39
+ "",
40
+ "## Steps",
41
+ ...spec.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
42
+ "",
43
+ "## Validation",
44
+ ...spec.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
45
+ "",
46
+ ].join("\n");
47
+
48
+ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
49
+ [
50
+ `# Workflow Plan: ${plan.workflow}`,
51
+ "",
52
+ `- Mode: ${plan.mode}`,
53
+ `- Requires approval: ${plan.requiresApproval}`,
54
+ `- Approval: ${plan.approval?.meaning ?? "Review this read-only plan before applying workflow mutations."}`,
55
+ "",
56
+ "## Inputs",
57
+ ...Object.entries(plan.inputs).map(
58
+ ([name, value]) => `- \`${name}\`: ${Array.isArray(value) ? value.join(", ") : value}`,
59
+ ),
60
+ "",
61
+ "## Optional Surfaces",
62
+ ...Object.entries(plan.optionalSurfaces).map(([name, mode]) => `- \`${name}\`: ${mode}`),
63
+ "",
64
+ "## Steps",
65
+ ...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
66
+ "",
67
+ "## Predicted Changes",
68
+ ...plan.predictedChanges.map((change) => {
69
+ const scope = change.applyScope ? ` (${change.applyScope})` : "";
70
+ return `- \`${change.action}\`${scope} ${change.target}: ${change.reason}`;
71
+ }),
72
+ "",
73
+ "## Validation",
74
+ ...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
75
+ "",
76
+ "## Diagnostics",
77
+ ...(plan.diagnostics.length
78
+ ? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
79
+ : ["- none"]),
80
+ "",
81
+ "## Recommendations",
82
+ ...(plan.recommendations.length
83
+ ? plan.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
84
+ : ["- none"]),
85
+ "",
86
+ ].join("\n");
87
+
88
+ const renderRecommendation = (recommendation: WorkflowApplyReport["recommendations"][number]) => {
89
+ const target = recommendation.target ? ` ${recommendation.target}` : "";
90
+ const action = recommendation.action ? ` Action: ${recommendation.action}` : "";
91
+ return `- [${recommendation.kind}]${target} ${recommendation.message}${action}`;
92
+ };
93
+
94
+ const renderDiagnostic = (diagnostic: WorkflowApplyReport["diagnostics"][number]) =>
95
+ `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`;
96
+
97
+ const renderNextAction = (action: WorkflowApplyReport["nextActions"][number]) =>
98
+ `- \`${action.command}\`: ${action.reason}`;
99
+
100
+ const applySourceStatus = (report: WorkflowApplyReport) =>
101
+ report.diagnostics.some(
102
+ (diagnostic) =>
103
+ diagnostic.severity === "error" &&
104
+ (diagnostic.failureScope === "source-change" ||
105
+ !diagnostic.failureScope ||
106
+ diagnostic.failureScope === "unknown"),
107
+ )
108
+ ? "failed"
109
+ : "passed";
110
+
111
+ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
112
+ const applySummary = {
113
+ sourceFilesChanged: report.summary?.sourceFilesChanged ?? report.changedFiles,
114
+ generatedFilesSynced: report.summary?.generatedFilesSynced ?? report.generatedFiles,
115
+ };
116
+ const manualReviewItems = [
117
+ ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
118
+ ...report.recommendations
119
+ .filter((recommendation) => recommendation.kind === "manual-action")
120
+ .map(renderRecommendation),
121
+ ];
122
+ const validationBlockers = report.diagnostics
123
+ .filter(
124
+ (diagnostic) =>
125
+ diagnostic.severity === "error" &&
126
+ (diagnostic.failureScope === "workspace-config" || diagnostic.failureScope === "environment"),
127
+ )
128
+ .map(renderDiagnostic);
129
+ const sourceBlockers = report.diagnostics
130
+ .filter(
131
+ (diagnostic) =>
132
+ diagnostic.severity === "error" &&
133
+ (diagnostic.failureScope === "source-change" ||
134
+ !diagnostic.failureScope ||
135
+ diagnostic.failureScope === "unknown"),
136
+ )
137
+ .map(renderDiagnostic);
138
+ return [
139
+ `# Workflow Apply: ${report.workflow}`,
140
+ "",
141
+ `- Mode: ${report.mode}`,
142
+ `- Status: ${report.status}`,
143
+ `- Source-change status: ${applySourceStatus(report)}`,
144
+ `- Workspace status: ${validationBlockers.length ? "blocked" : "not blocked during apply"}`,
145
+ `- Source files changed: ${applySummary.sourceFilesChanged.length}`,
146
+ `- Generated files queued for sync: ${applySummary.generatedFilesSynced.length}`,
147
+ ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
148
+ "",
149
+ "## Apply Checks",
150
+ ...(report.postApplyChecks?.length
151
+ ? report.postApplyChecks.map((check) => `- [${check.status}] ${check.code} ${check.target}: ${check.message}`)
152
+ : ["- not run"]),
153
+ "",
154
+ "## Source Change Blockers",
155
+ ...(sourceBlockers.length ? sourceBlockers : ["- none"]),
156
+ "",
157
+ "## Automatically Modified",
158
+ ...(applySummary.sourceFilesChanged.length
159
+ ? applySummary.sourceFilesChanged.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
160
+ : ["- none"]),
161
+ "",
162
+ "## Generated Sync",
163
+ ...(applySummary.generatedFilesSynced.length
164
+ ? applySummary.generatedFilesSynced.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
165
+ : ["- none"]),
166
+ "",
167
+ "## Applied Commands",
168
+ ...(report.appliedCommands.length
169
+ ? report.appliedCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
170
+ : ["- none"]),
171
+ "",
172
+ "## Recommended Validation Commands",
173
+ ...(report.recommendedValidationCommands.length
174
+ ? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
175
+ : ["- none"]),
176
+ "",
177
+ "## User Review Required",
178
+ ...(manualReviewItems.length ? manualReviewItems : ["- none"]),
179
+ "",
180
+ "## Validation Blockers",
181
+ ...(validationBlockers.length
182
+ ? validationBlockers
183
+ : report.recommendedValidationCommands.length
184
+ ? ["- none detected during apply; run validation with the validation target for command-level blockers."]
185
+ : ["- none"]),
186
+ "",
187
+ "## Diagnostics",
188
+ ...(report.diagnostics.length ? report.diagnostics.map(renderDiagnostic) : ["- none"]),
189
+ "",
190
+ "## Recommendations",
191
+ ...(report.recommendations.length ? report.recommendations.slice(0, 3).map(renderRecommendation) : ["- none"]),
192
+ "",
193
+ "## Next Actions",
194
+ ...(report.nextActions.length ? report.nextActions.slice(0, 3).map(renderNextAction) : ["- none"]),
195
+ "",
196
+ ].join("\n");
197
+ };
198
+
199
+ export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
200
+ format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
201
+
202
+ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) => {
203
+ const baselineSummary = report.baselineSummary ?? {
204
+ status: report.baselineDiagnostics?.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "unknown",
205
+ total: report.baselineDiagnostics?.length ?? 0,
206
+ totalErrors: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "error").length ?? 0,
207
+ totalWarnings: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "warning").length ?? 0,
208
+ detailsIncluded: true,
209
+ knownBlockerCount: report.knownBlockers.filter((blocker) => blocker.known).length,
210
+ byCode: [],
211
+ };
212
+ return [
213
+ `# Workflow Validation: ${report.workflow}`,
214
+ "",
215
+ `- Run: ${report.runId}`,
216
+ `- Status: ${report.status}`,
217
+ `- Source status: ${report.sourceStatus}`,
218
+ `- Workspace status: ${report.workspaceStatus}`,
219
+ `- Validation commands status: ${report.validationCommandsStatus ?? "unknown"}`,
220
+ `- Baseline status: ${report.baselineStatus ?? baselineSummary.status}`,
221
+ `- Overall status: ${report.overallStatus}`,
222
+ "",
223
+ "## Status Summary",
224
+ `- Source-change: ${report.summary.sourceChange}`,
225
+ `- Generated sync: ${report.summary.generatedSync}`,
226
+ `- Validation commands: ${report.summary.validationCommands ?? "unknown"}`,
227
+ `- Baseline: ${report.summary.baseline ?? baselineSummary.status}`,
228
+ `- Workspace config: ${report.summary.workspaceConfig}`,
229
+ `- Environment: ${report.summary.environment}`,
230
+ "",
231
+ "## Source Change Diagnostics",
232
+ ...(report.workflowDiagnostics?.length
233
+ ? report.workflowDiagnostics.map(
234
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
235
+ )
236
+ : ["- none"]),
237
+ "",
238
+ "## Existing Workspace Blockers",
239
+ `- Status: ${baselineSummary.status}`,
240
+ `- Errors: ${baselineSummary.totalErrors}`,
241
+ `- Warnings: ${baselineSummary.totalWarnings}`,
242
+ ...(baselineSummary.byCode.length
243
+ ? baselineSummary.byCode.map(
244
+ (item) => `- [${item.severity}] ${item.code} (${item.count}x): ${item.sampleMessage}`,
245
+ )
246
+ : ["- none"]),
247
+ ...(baselineSummary.detailsIncluded
248
+ ? []
249
+ : [
250
+ "- Baseline details are summarized by default. Re-run validation with includeBaselineDetails=true for full baselineDiagnostics.",
251
+ ]),
252
+ "",
253
+ "## Existing Workspace Blocker Details",
254
+ ...(report.baselineDiagnostics?.length
255
+ ? report.baselineDiagnostics.map(
256
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
257
+ )
258
+ : baselineSummary.detailsIncluded
259
+ ? ["- none"]
260
+ : ["- omitted"]),
261
+ "",
262
+ "## Known Blockers",
263
+ ...(report.knownBlockers.length
264
+ ? report.knownBlockers.map((blocker) => {
265
+ const command = blocker.command ? ` \`${blocker.command}\`` : "";
266
+ const count = blocker.count > 1 ? ` (${blocker.count}x)` : "";
267
+ const known = blocker.known ? " known" : "";
268
+ return `- [${blocker.failureScope}${known}]${command}${count}: ${blocker.message}`;
269
+ })
270
+ : ["- none"]),
271
+ "",
272
+ "## Commands",
273
+ ...(report.commands.length
274
+ ? report.commands.map((command) => {
275
+ const scope = command.failureScope ? ` (${command.failureScope})` : "";
276
+ return `- [${command.status}] \`${command.command}\`${scope}: ${command.reason}`;
277
+ })
278
+ : ["- none"]),
279
+ "",
280
+ "## Diagnostics",
281
+ ...(report.diagnostics.length
282
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
283
+ : ["- none"]),
284
+ "",
285
+ "## Repair Actions",
286
+ ...(report.repairActions.length
287
+ ? report.repairActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
288
+ : ["- none"]),
289
+ "",
290
+ "## Next Actions",
291
+ ...(report.nextActions.length
292
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
293
+ : ["- none"]),
294
+ "",
295
+ ].join("\n");
296
+ };
297
+
298
+ export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
299
+ format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
300
+
301
+ export const renderRepairReportMarkdown = (report: RepairReport) =>
302
+ [
303
+ `# Akan Repair: ${report.kind}`,
304
+ "",
305
+ `- Status: ${report.status}`,
306
+ `- Target: ${report.target ?? "none"}`,
307
+ "",
308
+ "## Commands",
309
+ ...(report.commands.length
310
+ ? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
311
+ : ["- none"]),
312
+ "",
313
+ "## Diagnostics",
314
+ ...(report.diagnostics.length
315
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
316
+ : ["- none"]),
317
+ "",
318
+ "## Next Actions",
319
+ ...(report.nextActions.length
320
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
321
+ : ["- none"]),
322
+ "",
323
+ ].join("\n");
324
+
325
+ export const renderRepairReport = (report: RepairReport, format: WorkflowFormat = "markdown") =>
326
+ format === "json" ? jsonText(report) : renderRepairReportMarkdown(report);
327
+
328
+ export const renderWorkflowRunArtifact = (artifact: WorkflowRunArtifact, format: WorkflowFormat = "markdown") => {
329
+ if ("kind" in artifact) return renderRepairReport(artifact, format);
330
+ if ("mode" in artifact && artifact.mode === "validate") return renderWorkflowValidation(artifact, format);
331
+ if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
332
+ return renderWorkflowApply(artifact, format);
333
+ }
334
+ return jsonText(artifact);
335
+ };
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import type { PackageJson } from "../types";
4
+ import { findToolingRolloutViolations } from "./rolloutGate";
5
+
6
+ const manifest = (overrides: Partial<PackageJson>): PackageJson => ({
7
+ name: "fixture",
8
+ version: "0.0.0",
9
+ description: "fixture",
10
+ ...overrides,
11
+ });
12
+
13
+ const workspaceRoot = path.join(import.meta.dir, "../../../..");
14
+
15
+ const readWorkspaceManifest = (relativePath: string) =>
16
+ Bun.file(path.join(workspaceRoot, relativePath)).json() as Promise<PackageJson>;
17
+
18
+ const workspacePackageManifestPaths = async () => {
19
+ const rootManifest = await readWorkspaceManifest("package.json");
20
+ const workspaces = Array.isArray(rootManifest.workspaces)
21
+ ? rootManifest.workspaces.filter((workspace): workspace is string => typeof workspace === "string")
22
+ : [];
23
+ const paths = new Set(["package.json"]);
24
+ for (const workspace of workspaces) {
25
+ const glob = new Bun.Glob(`${workspace}/package.json`);
26
+ for await (const relativePath of glob.scan({ cwd: workspaceRoot, onlyFiles: true })) {
27
+ paths.add(relativePath);
28
+ }
29
+ }
30
+ return [...paths].sort();
31
+ };
32
+
33
+ describe("tooling rollout gate", () => {
34
+ test("allows the stable TypeScript Compiler API baseline", () => {
35
+ expect(
36
+ findToolingRolloutViolations(
37
+ manifest({
38
+ dependencies: { typescript: "^6.0.3" },
39
+ }),
40
+ ),
41
+ ).toEqual([]);
42
+ });
43
+
44
+ test("rejects reference-only and experiment-only AST dependencies in package manifests", () => {
45
+ const violations = findToolingRolloutViolations(
46
+ manifest({
47
+ dependencies: {
48
+ "@ttsc/graph": "0.1.0",
49
+ "ast-grep": "0.39.0",
50
+ recast: "0.23.11",
51
+ "ts-morph": "27.0.2",
52
+ },
53
+ optionalDependencies: {
54
+ "typescript-go": "0.0.0",
55
+ },
56
+ }),
57
+ );
58
+
59
+ expect(violations.map((violation) => `${violation.packageName}:${violation.status}`).sort()).toEqual([
60
+ "@ttsc/graph:reference-only",
61
+ "ast-grep:experiment-only",
62
+ "recast:experiment-only",
63
+ "ts-morph:experiment-only",
64
+ "typescript-go:blocked",
65
+ ]);
66
+ });
67
+
68
+ test("rejects TypeScript rc or native-preview toolchain transitions", () => {
69
+ const violations = findToolingRolloutViolations(
70
+ manifest({
71
+ devDependencies: {
72
+ "@typescript/native-preview": "0.0.1",
73
+ typescript: "npm:typescript@rc",
74
+ },
75
+ }),
76
+ );
77
+
78
+ expect(violations).toContainEqual(
79
+ expect.objectContaining({
80
+ packageName: "typescript",
81
+ section: "devDependencies",
82
+ status: "blocked",
83
+ }),
84
+ );
85
+ expect(violations).toContainEqual(
86
+ expect.objectContaining({
87
+ packageName: "@typescript/native-preview",
88
+ section: "devDependencies",
89
+ status: "blocked",
90
+ }),
91
+ );
92
+ });
93
+
94
+ test("keeps workspace package manifests free of blocked AST and graph dependencies", async () => {
95
+ const manifestPaths = await workspacePackageManifestPaths();
96
+ expect(manifestPaths).toEqual(
97
+ expect.arrayContaining([
98
+ "package.json",
99
+ "pkgs/@akanjs/devkit/package.json",
100
+ "pkgs/@akanjs/cli/package.json",
101
+ "pkgs/akanjs/package.json",
102
+ "pkgs/create-akan-workspace/package.json",
103
+ ]),
104
+ );
105
+ const manifests = await Promise.all(manifestPaths.map(readWorkspaceManifest));
106
+
107
+ expect(manifests.flatMap(findToolingRolloutViolations)).toEqual([]);
108
+ });
109
+ });
@@ -0,0 +1,141 @@
1
+ import type { PackageJson } from "../types";
2
+
3
+ export type ToolingRolloutDependencySection =
4
+ | "dependencies"
5
+ | "devDependencies"
6
+ | "peerDependencies"
7
+ | "optionalDependencies";
8
+
9
+ export type ToolingRolloutCandidateStatus = "allowed" | "reference-only" | "experiment-only" | "blocked";
10
+
11
+ export interface ToolingRolloutCandidate {
12
+ packageName: string;
13
+ status: ToolingRolloutCandidateStatus;
14
+ role: string;
15
+ adoptionGate: string;
16
+ }
17
+
18
+ export interface ToolingRolloutViolation {
19
+ packageName: string;
20
+ version: string;
21
+ section: ToolingRolloutDependencySection;
22
+ status: Exclude<ToolingRolloutCandidateStatus, "allowed">;
23
+ reason: string;
24
+ }
25
+
26
+ type RestrictedToolingRolloutCandidate = ToolingRolloutCandidate & {
27
+ status: Exclude<ToolingRolloutCandidateStatus, "allowed">;
28
+ };
29
+
30
+ const dependencySections = [
31
+ "dependencies",
32
+ "devDependencies",
33
+ "peerDependencies",
34
+ "optionalDependencies",
35
+ ] as const satisfies readonly ToolingRolloutDependencySection[];
36
+
37
+ export const toolingRolloutCandidates = [
38
+ {
39
+ packageName: "typescript",
40
+ status: "allowed",
41
+ role: "Primary TypeScript Compiler API baseline for AST locator, text splice, and reparse checks.",
42
+ adoptionGate: "Already present; keep using stable Bun-compatible package builds.",
43
+ },
44
+ {
45
+ packageName: "@ttsc/graph",
46
+ status: "reference-only",
47
+ role: "Source-free graph shape and lazy resident model reference.",
48
+ adoptionGate: "Do not add as an Akan runtime dependency without a separate proposal or milestone update.",
49
+ },
50
+ {
51
+ packageName: "ts-morph",
52
+ status: "experiment-only",
53
+ role: "Prototype candidate for complex object literal insertion, import update, and class traversal.",
54
+ adoptionGate: "Use only in isolated prototypes until package size and formatting churn are measured.",
55
+ },
56
+ {
57
+ packageName: "ast-grep",
58
+ status: "experiment-only",
59
+ role: "Prototype candidate for shape detection, CI rules, and migration rules.",
60
+ adoptionGate: "Use as a local rule/check experiment, not as the edit engine.",
61
+ },
62
+ {
63
+ packageName: "recast",
64
+ status: "experiment-only",
65
+ role: "Prototype candidate for formatting preservation.",
66
+ adoptionGate: "Low priority; require proof that formatting churn is lower than the baseline.",
67
+ },
68
+ {
69
+ packageName: "typescript-go",
70
+ status: "blocked",
71
+ role: "TypeScript-Go toolchain transition placeholder.",
72
+ adoptionGate: "Out of scope for Season 2 rollout.",
73
+ },
74
+ {
75
+ packageName: "@typescript/native-preview",
76
+ status: "blocked",
77
+ role: "TypeScript-Go native preview package placeholder.",
78
+ adoptionGate: "Out of scope for Season 2 rollout.",
79
+ },
80
+ ] as const satisfies readonly ToolingRolloutCandidate[];
81
+
82
+ export const toolingRolloutGate = {
83
+ schemaVersion: 1,
84
+ strategy: "reference-first-dependency-later",
85
+ dependencyPolicy:
86
+ "Season 2 keeps new AST and graph tooling as references or isolated experiments until a separate proposal or milestone update approves adoption.",
87
+ gateConditions: [
88
+ "Works in Bun runtime and published package artifacts.",
89
+ "Does not break dist/pkgs package verification.",
90
+ "Keeps generated source formatting churn limited.",
91
+ "Reduces add-field regression fixture failures compared with the current TypeScript API baseline.",
92
+ "Does not move MCP responses toward returning source bodies.",
93
+ ],
94
+ candidates: toolingRolloutCandidates,
95
+ };
96
+
97
+ const isRestrictedCandidate = (candidate: ToolingRolloutCandidate): candidate is RestrictedToolingRolloutCandidate =>
98
+ candidate.status !== "allowed";
99
+
100
+ const restrictedCandidates = new Map<string, RestrictedToolingRolloutCandidate>();
101
+ for (const candidate of toolingRolloutCandidates) {
102
+ if (isRestrictedCandidate(candidate)) restrictedCandidates.set(candidate.packageName, candidate);
103
+ }
104
+
105
+ const blockedTypeScriptVersion = (version: string) =>
106
+ /(?:^|[^\w])(?:rc|next|beta|canary|insiders)(?:[^\w]|$)/i.test(version) || /typescript@rc/i.test(version);
107
+
108
+ export const findToolingRolloutViolations = (packageJson: PackageJson): ToolingRolloutViolation[] => {
109
+ const violations: ToolingRolloutViolation[] = [];
110
+
111
+ for (const section of dependencySections) {
112
+ const dependencies = packageJson[section];
113
+ if (!dependencies) continue;
114
+
115
+ for (const [packageName, version] of Object.entries(dependencies)) {
116
+ const candidate = restrictedCandidates.get(packageName);
117
+ if (candidate) {
118
+ violations.push({
119
+ packageName,
120
+ version,
121
+ section,
122
+ status: candidate.status,
123
+ reason: candidate.adoptionGate,
124
+ });
125
+ continue;
126
+ }
127
+
128
+ if (packageName === "typescript" && blockedTypeScriptVersion(version)) {
129
+ violations.push({
130
+ packageName,
131
+ version,
132
+ section,
133
+ status: "blocked",
134
+ reason: "TypeScript-Go or typescript@rc toolchain transitions are out of scope for Season 2 rollout.",
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ return violations;
141
+ };
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { inspectDictionaryStructure } from "./source";
3
+
4
+ describe("inspectDictionaryStructure", () => {
5
+ test("preserves protected dictionary chain order around the model object", () => {
6
+ const structure = inspectDictionaryStructure(
7
+ `
8
+ import { modelDictionary } from "akanjs/dictionary";
9
+ import type { Article, ArticleSlice } from "./article.constant";
10
+
11
+ export const dictionary = modelDictionary(["en", "ko"])
12
+ .of((t) => t(["Article", "Article"]))
13
+ .model<Article>((t) => ({
14
+ title: t(["Title", "제목"]),
15
+ }))
16
+ .slice<ArticleSlice>((fn) => ({
17
+ inPublic: fn(["Article In Public", "Article 공개"]),
18
+ }))
19
+ .enum<ArticleStatus>("articleStatus", (t) => ({}))
20
+ .error({})
21
+ .translate({});
22
+ `,
23
+ "Article",
24
+ );
25
+
26
+ expect(structure).toMatchObject({
27
+ parseValid: true,
28
+ modelObjectFound: true,
29
+ chainOrderValid: true,
30
+ fields: ["title"],
31
+ });
32
+ expect(structure.chainMethods).toEqual(["modelDictionary", "of", "model", "slice", "enum", "error", "translate"]);
33
+ });
34
+
35
+ test("reports broken dictionary chain order even when the field remains inside model", () => {
36
+ const structure = inspectDictionaryStructure(
37
+ `
38
+ import { modelDictionary } from "akanjs/dictionary";
39
+ import type { Article, ArticleSlice } from "./article.constant";
40
+
41
+ export const dictionary = modelDictionary(["en", "ko"])
42
+ .slice<ArticleSlice>((fn) => ({
43
+ inPublic: fn(["Article In Public", "Article 공개"]),
44
+ }))
45
+ .model<Article>((t) => ({
46
+ title: t(["Title", "제목"]),
47
+ }))
48
+ .translate({})
49
+ .error({});
50
+ `,
51
+ "Article",
52
+ );
53
+
54
+ expect(structure).toMatchObject({
55
+ parseValid: true,
56
+ modelObjectFound: true,
57
+ chainOrderValid: false,
58
+ fields: ["title"],
59
+ });
60
+ expect(structure.chainMethods).toEqual(["modelDictionary", "slice", "model", "translate", "error"]);
61
+ });
62
+ });