@akanjs/devkit 2.3.9-rc.7 → 2.3.9-rc.9

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.
@@ -51,6 +51,7 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
51
51
  "",
52
52
  `- Mode: ${plan.mode}`,
53
53
  `- Requires approval: ${plan.requiresApproval}`,
54
+ `- Approval: ${plan.approval?.meaning ?? "Review this read-only plan before applying workflow mutations."}`,
54
55
  "",
55
56
  "## Inputs",
56
57
  ...Object.entries(plan.inputs).map(
@@ -93,6 +94,9 @@ const renderRecommendation = (recommendation: WorkflowApplyReport["recommendatio
93
94
  const renderDiagnostic = (diagnostic: WorkflowApplyReport["diagnostics"][number]) =>
94
95
  `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`;
95
96
 
97
+ const renderNextAction = (action: WorkflowApplyReport["nextActions"][number]) =>
98
+ `- \`${action.command}\`: ${action.reason}`;
99
+
96
100
  const applySourceStatus = (report: WorkflowApplyReport) =>
97
101
  report.diagnostics.some(
98
102
  (diagnostic) =>
@@ -105,6 +109,10 @@ const applySourceStatus = (report: WorkflowApplyReport) =>
105
109
  : "passed";
106
110
 
107
111
  export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
112
+ const applySummary = {
113
+ sourceFilesChanged: report.summary?.sourceFilesChanged ?? report.changedFiles,
114
+ generatedFilesSynced: report.summary?.generatedFilesSynced ?? report.generatedFiles,
115
+ };
108
116
  const manualReviewItems = [
109
117
  ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
110
118
  ...report.recommendations
@@ -134,6 +142,8 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
134
142
  `- Status: ${report.status}`,
135
143
  `- Source-change status: ${applySourceStatus(report)}`,
136
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}`,
137
147
  ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
138
148
  "",
139
149
  "## Apply Checks",
@@ -145,13 +155,13 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
145
155
  ...(sourceBlockers.length ? sourceBlockers : ["- none"]),
146
156
  "",
147
157
  "## Automatically Modified",
148
- ...(report.changedFiles.length
149
- ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
158
+ ...(applySummary.sourceFilesChanged.length
159
+ ? applySummary.sourceFilesChanged.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
150
160
  : ["- none"]),
151
161
  "",
152
162
  "## Generated Sync",
153
- ...(report.generatedFiles.length
154
- ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
163
+ ...(applySummary.generatedFilesSynced.length
164
+ ? applySummary.generatedFilesSynced.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
155
165
  : ["- none"]),
156
166
  "",
157
167
  "## Applied Commands",
@@ -181,9 +191,7 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
181
191
  ...(report.recommendations.length ? report.recommendations.slice(0, 3).map(renderRecommendation) : ["- none"]),
182
192
  "",
183
193
  "## Next Actions",
184
- ...(report.nextActions.length
185
- ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
186
- : ["- none"]),
194
+ ...(report.nextActions.length ? report.nextActions.slice(0, 3).map(renderNextAction) : ["- none"]),
187
195
  "",
188
196
  ].join("\n");
189
197
  };
@@ -191,19 +199,32 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
191
199
  export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
192
200
  format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
193
201
 
194
- export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
195
- [
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 [
196
213
  `# Workflow Validation: ${report.workflow}`,
197
214
  "",
198
215
  `- Run: ${report.runId}`,
199
216
  `- Status: ${report.status}`,
200
217
  `- Source status: ${report.sourceStatus}`,
201
218
  `- Workspace status: ${report.workspaceStatus}`,
219
+ `- Validation commands status: ${report.validationCommandsStatus ?? "unknown"}`,
220
+ `- Baseline status: ${report.baselineStatus ?? baselineSummary.status}`,
202
221
  `- Overall status: ${report.overallStatus}`,
203
222
  "",
204
223
  "## Status Summary",
205
224
  `- Source-change: ${report.summary.sourceChange}`,
206
225
  `- Generated sync: ${report.summary.generatedSync}`,
226
+ `- Validation commands: ${report.summary.validationCommands ?? "unknown"}`,
227
+ `- Baseline: ${report.summary.baseline ?? baselineSummary.status}`,
207
228
  `- Workspace config: ${report.summary.workspaceConfig}`,
208
229
  `- Environment: ${report.summary.environment}`,
209
230
  "",
@@ -215,11 +236,28 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
215
236
  : ["- none"]),
216
237
  "",
217
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",
218
254
  ...(report.baselineDiagnostics?.length
219
255
  ? report.baselineDiagnostics.map(
220
256
  (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
221
257
  )
222
- : ["- none"]),
258
+ : baselineSummary.detailsIncluded
259
+ ? ["- none"]
260
+ : ["- omitted"]),
223
261
  "",
224
262
  "## Known Blockers",
225
263
  ...(report.knownBlockers.length
@@ -255,6 +293,7 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
255
293
  : ["- none"]),
256
294
  "",
257
295
  ].join("\n");
296
+ };
258
297
 
259
298
  export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
260
299
  format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
@@ -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
+ });