@rse/ase 0.9.61 → 0.9.63
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/dst/ase-artifact.js +19 -8
- package/dst/ase-config.js +12 -8
- package/dst/ase-hook.js +8 -0
- package/dst/ase-service.js +2 -0
- package/dst/ase-spec.js +241 -0
- package/dst/ase.js +2 -0
- package/package.json +10 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-code-lint.md +47 -3
- package/plugin/etc/stx.conf +5 -3
- package/plugin/meta/ase-common-grill.md +89 -0
- package/plugin/meta/ase-dialog.md +6 -0
- package/plugin/meta/ase-format-meta.md +23 -105
- package/plugin/meta/ase-format-spec.md +22 -1326
- package/plugin/meta/ase-tenets.md +63 -4
- package/plugin/package.json +6 -2
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-code-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-code-analyze/help.md +2 -1
- package/plugin/skills/ase-code-edit/SKILL.md +142 -140
- package/plugin/skills/ase-code-lint/SKILL.md +5 -5
- package/plugin/skills/ase-code-lint/help.md +23 -8
- package/plugin/skills/ase-help-skill/catalog.md +3 -0
- package/plugin/skills/ase-meta-review/help.md +1 -1
- package/plugin/skills/ase-spec-edit/SKILL.md +520 -0
- package/plugin/skills/ase-spec-edit/help.md +130 -0
- package/plugin/skills/ase-sync-export/SKILL.md +57 -116
- package/plugin/skills/ase-sync-export/help.md +30 -42
- package/plugin/skills/ase-sync-import/SKILL.md +37 -15
- package/plugin/skills/ase-sync-import/help.md +14 -10
- package/plugin/skills/ase-sync-reconcile/SKILL.md +37 -16
- package/plugin/skills/ase-sync-reconcile/help.md +19 -16
- package/plugin/skills/ase-task-grill/SKILL.md +120 -59
- package/plugin/skills/ase-task-grill/help.md +42 -12
- package/plugin/meta/ase-format-arch.md +0 -1164
package/dst/ase-artifact.js
CHANGED
|
@@ -13,9 +13,9 @@ import { Task } from "./ase-task.js";
|
|
|
13
13
|
import { writeStdout } from "./ase-stdio.js";
|
|
14
14
|
/* the recognized artifact kinds, in descending precedence order;
|
|
15
15
|
"othr" is the implicit catch-all and is always resolved last */
|
|
16
|
-
export const artifactKinds = ["spec", "
|
|
17
|
-
/* the
|
|
18
|
-
const configuredKinds = ["spec", "
|
|
16
|
+
export const artifactKinds = ["spec", "code", "docs", "infr", "othr"];
|
|
17
|
+
/* the four configured kinds (i.e. all kinds except the implicit "othr") */
|
|
18
|
+
const configuredKinds = ["spec", "code", "docs", "infr"];
|
|
19
19
|
/* reusable functionality: resolve artifact kinds to project-relative
|
|
20
20
|
file lists, driven by the "project.artifact.<kind>" configuration */
|
|
21
21
|
export class Artifact {
|
|
@@ -167,7 +167,7 @@ export class Artifact {
|
|
|
167
167
|
const all = Artifact.universe();
|
|
168
168
|
const cfg = new Config("config", configSchema, log);
|
|
169
169
|
cfg.read();
|
|
170
|
-
/* raw-resolve all
|
|
170
|
+
/* raw-resolve all four configured kinds */
|
|
171
171
|
const raw = new Map();
|
|
172
172
|
for (const kind of configuredKinds) {
|
|
173
173
|
const { basedir, files } = Artifact.spec(cfg, kind);
|
|
@@ -210,6 +210,17 @@ export class Artifact {
|
|
|
210
210
|
const { basedir } = Artifact.spec(cfg, kind);
|
|
211
211
|
return basedir === "" ? file : `${basedir}/${file}`;
|
|
212
212
|
}
|
|
213
|
+
/* resolve the configured "basedir" of a kind to an absolute path;
|
|
214
|
+
the implicit "othr" catch-all has no configured "basedir" and is
|
|
215
|
+
therefore rejected */
|
|
216
|
+
static basedir(log, kind) {
|
|
217
|
+
if (kind === "othr")
|
|
218
|
+
throw new Error("artifact: kind \"othr\" has no configured basedir");
|
|
219
|
+
const cfg = new Config("config", configSchema, log);
|
|
220
|
+
cfg.read();
|
|
221
|
+
const { basedir } = Artifact.spec(cfg, kind);
|
|
222
|
+
return path.join(Task.projectRoot(), basedir);
|
|
223
|
+
}
|
|
213
224
|
}
|
|
214
225
|
/* CLI command "ase artifact" */
|
|
215
226
|
export default class ArtifactCommand {
|
|
@@ -273,12 +284,12 @@ export class ArtifactMCP {
|
|
|
273
284
|
mcp.registerTool("ase_artifact_list", {
|
|
274
285
|
title: "ASE artifact list",
|
|
275
286
|
description: "Resolve one or more artifact `kind`s to project-relative file lists. " +
|
|
276
|
-
"Recognized kinds are `spec`, `
|
|
287
|
+
"Recognized kinds are `spec`, `code`, `docs`, `infr`, and `othr`. " +
|
|
277
288
|
"Returns an `artifacts` array of `{ kind, files }` objects. " +
|
|
278
289
|
"If `kind` is omitted or empty, all kinds are resolved.",
|
|
279
290
|
inputSchema: {
|
|
280
291
|
kind: z.array(z.string()).optional()
|
|
281
|
-
.describe("list of artifact kinds (`spec`, `
|
|
292
|
+
.describe("list of artifact kinds (`spec`, `code`, `docs`, `infr`, " +
|
|
282
293
|
"`othr`); if omitted or empty, all kinds are resolved")
|
|
283
294
|
},
|
|
284
295
|
outputSchema: {
|
|
@@ -306,13 +317,13 @@ export class ArtifactMCP {
|
|
|
306
317
|
title: "ASE artifact name",
|
|
307
318
|
description: "Resolve a base-relative `filename` within an artifact `kind` to a project-relative path " +
|
|
308
319
|
"by prefixing it with the kind's configured `basedir`. " +
|
|
309
|
-
"Recognized kinds are `spec`, `
|
|
320
|
+
"Recognized kinds are `spec`, `code`, `docs`, and `infr` " +
|
|
310
321
|
"(the implicit `othr` catch-all has no basedir and is rejected). " +
|
|
311
322
|
"If `kind` is omitted, it defaults to `code`. " +
|
|
312
323
|
"Returns the resolved path as `name`.",
|
|
313
324
|
inputSchema: {
|
|
314
325
|
kind: z.string().optional()
|
|
315
|
-
.describe("artifact kind (`spec`, `
|
|
326
|
+
.describe("artifact kind (`spec`, `code`, `docs`, `infr`); defaults to `code`"),
|
|
316
327
|
filename: z.string()
|
|
317
328
|
.describe("base-relative filename within the kind's basedir")
|
|
318
329
|
},
|
package/dst/ase-config.js
CHANGED
|
@@ -48,10 +48,9 @@ export const projectClassificationPresets = {
|
|
|
48
48
|
"project.boxing": "white",
|
|
49
49
|
"project.artifact.task.basedir": ".ase/task",
|
|
50
50
|
"project.artifact.task.files": "*.md",
|
|
51
|
-
"project.artifact.spec.basedir": "
|
|
52
|
-
"project.artifact.spec.files": "*.{md,txt}",
|
|
53
|
-
"project.artifact.
|
|
54
|
-
"project.artifact.arch.files": "*.{md,txt}",
|
|
51
|
+
"project.artifact.spec.basedir": "docs/specbook",
|
|
52
|
+
"project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
|
|
53
|
+
"project.artifact.spec.schema": "",
|
|
55
54
|
"project.artifact.code.basedir": "src",
|
|
56
55
|
"project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
|
|
57
56
|
"project.artifact.docs.basedir": "doc",
|
|
@@ -76,8 +75,7 @@ export const configWritableScopes = {
|
|
|
76
75
|
"project.artifact.task.files": ["user", "project"],
|
|
77
76
|
"project.artifact.spec.basedir": ["user", "project"],
|
|
78
77
|
"project.artifact.spec.files": ["user", "project"],
|
|
79
|
-
"project.artifact.
|
|
80
|
-
"project.artifact.arch.files": ["user", "project"],
|
|
78
|
+
"project.artifact.spec.schema": ["user", "project"],
|
|
81
79
|
"project.artifact.code.basedir": ["user", "project"],
|
|
82
80
|
"project.artifact.code.files": ["user", "project"],
|
|
83
81
|
"project.artifact.docs.basedir": ["user", "project"],
|
|
@@ -185,6 +183,13 @@ const artifactSchema = v.optional(v.strictObject({
|
|
|
185
183
|
basedir: v.optional(v.string()),
|
|
186
184
|
files: v.optional(v.string())
|
|
187
185
|
}));
|
|
186
|
+
/* schema for the "spec" artifact kind, additionally carrying the
|
|
187
|
+
SpecBook YAML "schema" configuration file (empty: bundled standard) */
|
|
188
|
+
const artifactSpecSchema = v.optional(v.strictObject({
|
|
189
|
+
basedir: v.optional(v.string()),
|
|
190
|
+
files: v.optional(v.string()),
|
|
191
|
+
schema: v.optional(v.string())
|
|
192
|
+
}));
|
|
188
193
|
/* schema for ".ase/config.yaml" */
|
|
189
194
|
export const configSchema = v.nullish(v.strictObject({
|
|
190
195
|
project: v.optional(v.strictObject({
|
|
@@ -192,8 +197,7 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
192
197
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
193
198
|
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
194
199
|
artifact: v.optional(v.strictObject({
|
|
195
|
-
spec:
|
|
196
|
-
arch: artifactSchema,
|
|
200
|
+
spec: artifactSpecSchema,
|
|
197
201
|
code: artifactSchema,
|
|
198
202
|
docs: artifactSchema,
|
|
199
203
|
infr: artifactSchema,
|
package/dst/ase-hook.js
CHANGED
|
@@ -292,6 +292,10 @@ export default class HookCommand {
|
|
|
292
292
|
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
293
293
|
const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
|
|
294
294
|
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
295
|
+
/* determine the specification base directory and the SpecBook
|
|
296
|
+
schema configuration (empty: the bundled standard schema) */
|
|
297
|
+
const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
|
|
298
|
+
const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
|
|
295
299
|
/* determine headless mode */
|
|
296
300
|
const headless = process.env.ASE_HEADLESS === "true" ? "true" : "false";
|
|
297
301
|
/* provide ASE information to Anthropic Claude Code CLI shell commands
|
|
@@ -303,6 +307,8 @@ export default class HookCommand {
|
|
|
303
307
|
`export ASE_USER_ID=${quote([userId])}\n` +
|
|
304
308
|
`export ASE_PROJECT_ID=${quote([projectId])}\n` +
|
|
305
309
|
`export ASE_PROJECT_BOXING=${quote([boxing])}\n` +
|
|
310
|
+
`export ASE_SPEC_BASEDIR=${quote([specBasedir])}\n` +
|
|
311
|
+
`export ASE_SPEC_SCHEMA=${quote([specSchema])}\n` +
|
|
306
312
|
`export ASE_TASK_ID=${quote([taskId])}\n` +
|
|
307
313
|
`export ASE_SESSION_ID=${quote([sessionId])}\n` +
|
|
308
314
|
`export ASE_HEADLESS=${quote([headless])}\n` +
|
|
@@ -325,6 +331,8 @@ export default class HookCommand {
|
|
|
325
331
|
`<ase-user-id>${userId}</ase-user-id>\n` +
|
|
326
332
|
`<ase-project-id>${projectId}</ase-project-id>\n` +
|
|
327
333
|
`<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
|
|
334
|
+
`<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
|
|
335
|
+
`<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
|
|
328
336
|
`<ase-task-id>${taskId}</ase-task-id>\n` +
|
|
329
337
|
`<ase-session-id>${sessionId}</ase-session-id>\n` +
|
|
330
338
|
`<ase-headless>${headless}</ase-headless>\n` +
|
package/dst/ase-service.js
CHANGED
|
@@ -22,6 +22,7 @@ import { DiagramMCP } from "./ase-diagram.js";
|
|
|
22
22
|
import { TaskMCP } from "./ase-task.js";
|
|
23
23
|
import { MarkdownMCP } from "./ase-markdown.js";
|
|
24
24
|
import { ArtifactMCP } from "./ase-artifact.js";
|
|
25
|
+
import { SpecMCP } from "./ase-spec.js";
|
|
25
26
|
import { KVMCP } from "./ase-kv.js";
|
|
26
27
|
import { TimestampMCP } from "./ase-timestamp.js";
|
|
27
28
|
import { SleepMCP } from "./ase-sleep.js";
|
|
@@ -268,6 +269,7 @@ export default class ServiceCommand {
|
|
|
268
269
|
new TaskMCP(this.log).register(mcp);
|
|
269
270
|
new MarkdownMCP().register(mcp);
|
|
270
271
|
new ArtifactMCP(this.log).register(mcp);
|
|
272
|
+
new SpecMCP(this.log).register(mcp);
|
|
271
273
|
new KVMCP().register(mcp);
|
|
272
274
|
new TimestampMCP().register(mcp);
|
|
273
275
|
new SleepMCP().register(mcp);
|
package/dst/ase-spec.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { isScalar } from "yaml";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import sourceCodeError from "source-code-error";
|
|
11
|
+
import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec } from "@rse/specbook";
|
|
12
|
+
import { Config, configSchema } from "./ase-config.js";
|
|
13
|
+
import { Task } from "./ase-task.js";
|
|
14
|
+
import { Artifact } from "./ase-artifact.js";
|
|
15
|
+
import { Meta } from "./ase-meta.js";
|
|
16
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
17
|
+
/* reusable functionality: lint and export the SpecBook-based project
|
|
18
|
+
specification, located via the "project.artifact.spec.basedir" and
|
|
19
|
+
"project.artifact.spec.schema" configuration */
|
|
20
|
+
export class Spec {
|
|
21
|
+
/* resolve the YAML schema configuration file: the configured
|
|
22
|
+
"project.artifact.spec.schema" relative to the project root, or
|
|
23
|
+
the bundled standard "ase-format-specbook.yaml" plugin meta file
|
|
24
|
+
if unset or empty */
|
|
25
|
+
static configFile(log) {
|
|
26
|
+
const cfg = new Config("config", configSchema, log);
|
|
27
|
+
cfg.read();
|
|
28
|
+
const val = cfg.get("project.artifact.spec.schema");
|
|
29
|
+
const file = val === undefined ? "" : String(isScalar(val) ? val.value : val);
|
|
30
|
+
if (file === "")
|
|
31
|
+
return Meta.resolve("ase-format-specbook.yaml");
|
|
32
|
+
return path.resolve(Task.projectRoot(), file);
|
|
33
|
+
}
|
|
34
|
+
/* create the SpecBook API instance, routing its verbose processing
|
|
35
|
+
messages into the info log if requested, else into the debug log */
|
|
36
|
+
static api(log, verbose) {
|
|
37
|
+
return new SpecBook({
|
|
38
|
+
verbose: (cmd, msg) => log.write(verbose ? "info" : "debug", `specbook: ${cmd}: ${renderVerbose(msg)}`)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/* render a diagnostic file path relative to the project root,
|
|
42
|
+
keeping paths outside the project (like the bundled schema) as-is */
|
|
43
|
+
static relativize(file) {
|
|
44
|
+
const rel = path.relative(Task.projectRoot(), file);
|
|
45
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
46
|
+
return file;
|
|
47
|
+
return rel.replace(/\\/g, "/");
|
|
48
|
+
}
|
|
49
|
+
/* lint the specification Markdown files below the "spec" artifact
|
|
50
|
+
base directory against the schema configuration */
|
|
51
|
+
static async lint(log, verbose = false) {
|
|
52
|
+
const result = await Spec.api(log, verbose).lint({
|
|
53
|
+
config: Spec.configFile(log),
|
|
54
|
+
basedir: Artifact.basedir(log, "spec")
|
|
55
|
+
});
|
|
56
|
+
return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
|
|
57
|
+
}
|
|
58
|
+
/* render a diagnostic as a multi-line message with the affected
|
|
59
|
+
source snippet (like the verbose SpecBook CLI), falling back to
|
|
60
|
+
the single-line message when the source file is unreadable or
|
|
61
|
+
empty (as there is no snippet to show) */
|
|
62
|
+
static render(diagnostic, colors) {
|
|
63
|
+
let code;
|
|
64
|
+
try {
|
|
65
|
+
code = fs.readFileSync(path.resolve(Task.projectRoot(), diagnostic.file), "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
69
|
+
}
|
|
70
|
+
if (code === "")
|
|
71
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
72
|
+
return sourceCodeError({
|
|
73
|
+
message: diagnostic.message,
|
|
74
|
+
filename: diagnostic.file,
|
|
75
|
+
code,
|
|
76
|
+
line: diagnostic.line,
|
|
77
|
+
column: diagnostic.column,
|
|
78
|
+
colors
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/* export the specification Markdown files below the "spec" artifact
|
|
82
|
+
base directory into the requested formats, one buffer per format */
|
|
83
|
+
static export(log, formats, verbose = false) {
|
|
84
|
+
return Spec.api(log, verbose).export({
|
|
85
|
+
config: Spec.configFile(log),
|
|
86
|
+
basedir: Artifact.basedir(log, "spec"),
|
|
87
|
+
formats
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/* CLI command "ase spec" */
|
|
92
|
+
export default class SpecCommand {
|
|
93
|
+
log;
|
|
94
|
+
constructor(log) {
|
|
95
|
+
this.log = log;
|
|
96
|
+
}
|
|
97
|
+
/* register commands */
|
|
98
|
+
register(program) {
|
|
99
|
+
/* register CLI top-level command "ase spec" */
|
|
100
|
+
const spec = program
|
|
101
|
+
.command("spec")
|
|
102
|
+
.description("Lint and export the SpecBook-based project specification")
|
|
103
|
+
.action(() => {
|
|
104
|
+
spec.outputHelp();
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
107
|
+
/* register CLI sub-command "ase spec lint" */
|
|
108
|
+
spec
|
|
109
|
+
.command("lint")
|
|
110
|
+
.description("Lint the specification Markdown files against the SpecBook schema configuration")
|
|
111
|
+
.option("-v, --verbose", "print verbose processing information and each diagnostic with its affected source snippet")
|
|
112
|
+
.action(async (opts) => {
|
|
113
|
+
const diagnostics = await Spec.lint(this.log, opts.verbose === true);
|
|
114
|
+
for (const diagnostic of diagnostics)
|
|
115
|
+
await writeStdout(opts.verbose === true ?
|
|
116
|
+
Spec.render(diagnostic, process.stdout.isTTY === true) :
|
|
117
|
+
`${renderDiagnostic(diagnostic)}\n`);
|
|
118
|
+
if (diagnostics.length > 0)
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
});
|
|
121
|
+
/* register CLI sub-command "ase spec export" */
|
|
122
|
+
spec
|
|
123
|
+
.command("export")
|
|
124
|
+
.description("Export the specification Markdown files as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
125
|
+
.option("-o, --output <[format:]file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
126
|
+
"from the filename extension unless explicitly prefixed " +
|
|
127
|
+
"(default: \"index.html\" inside the specification base directory)", (value, previous) => previous.concat(value), new Array())
|
|
128
|
+
.option("-v, --verbose", "print verbose processing information")
|
|
129
|
+
.action(async (opts) => {
|
|
130
|
+
const outputs = (opts.output.length > 0 ? opts.output :
|
|
131
|
+
[path.join(Artifact.basedir(this.log, "spec"), "index.html")]).map(parseOutputSpec);
|
|
132
|
+
const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
|
|
133
|
+
const buffers = await Spec.export(this.log, distinct, opts.verbose === true);
|
|
134
|
+
for (const { format, output } of outputs) {
|
|
135
|
+
const data = buffers[distinct.indexOf(format)];
|
|
136
|
+
if (output === "-")
|
|
137
|
+
await writeStdout(data);
|
|
138
|
+
else {
|
|
139
|
+
await fs.promises.writeFile(output, data);
|
|
140
|
+
this.log.write("info", `spec: exported specification into "${output}" (${data.length} bytes)`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/* render a caught error as an MCP tool error result */
|
|
147
|
+
const mcpToolError = (err) => ({
|
|
148
|
+
isError: true,
|
|
149
|
+
content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
|
|
150
|
+
});
|
|
151
|
+
/* MCP registration entry point for SpecBook tools */
|
|
152
|
+
export class SpecMCP {
|
|
153
|
+
log;
|
|
154
|
+
constructor(log) {
|
|
155
|
+
this.log = log;
|
|
156
|
+
}
|
|
157
|
+
/* register MCP tools */
|
|
158
|
+
register(mcp) {
|
|
159
|
+
mcp.registerTool("ase_specbook_lint", {
|
|
160
|
+
title: "ASE SpecBook lint",
|
|
161
|
+
description: "Lint the SpecBook specification Markdown files of the project (located via the " +
|
|
162
|
+
"`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
|
|
163
|
+
"configuration (`project.artifact.spec.schema`, defaulting to the bundled `ase-format-specbook.yaml`). " +
|
|
164
|
+
"Returns a `diagnostics` array of `{ file, line, column, message }` objects (with " +
|
|
165
|
+
"project-relative `file`), rendered as bullet points in `text`. With `verbose`, " +
|
|
166
|
+
"each diagnostic additionally carries a multi-line `snippet` rendering with the " +
|
|
167
|
+
"affected source lines, which is also used for `text`. An empty array " +
|
|
168
|
+
"(`text` of `specification valid`) means the specification is valid.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
verbose: z.boolean().optional()
|
|
171
|
+
.describe("if true, render each diagnostic with its affected source snippet (default: false)")
|
|
172
|
+
},
|
|
173
|
+
outputSchema: {
|
|
174
|
+
diagnostics: z.array(z.object({
|
|
175
|
+
file: z.string().describe("project-relative file path"),
|
|
176
|
+
line: z.number().describe("line number (1-based)"),
|
|
177
|
+
column: z.number().describe("column number (1-based)"),
|
|
178
|
+
message: z.string().describe("diagnostic message"),
|
|
179
|
+
snippet: z.string().optional()
|
|
180
|
+
.describe("multi-line rendering with the affected source snippet (with `verbose` only)")
|
|
181
|
+
})).describe("lint diagnostics, empty if the specification is valid")
|
|
182
|
+
}
|
|
183
|
+
}, async (args) => {
|
|
184
|
+
try {
|
|
185
|
+
const verbose = args.verbose ?? false;
|
|
186
|
+
const diagnostics = (await Spec.lint(this.log))
|
|
187
|
+
.map((d) => verbose ? { ...d, snippet: Spec.render(d, false) } : d);
|
|
188
|
+
const text = diagnostics.length === 0 ? "specification valid" :
|
|
189
|
+
verbose ?
|
|
190
|
+
diagnostics.map((d) => d.snippet ?? "").join("").replace(/\n$/, "") :
|
|
191
|
+
diagnostics.map((d) => `- ${renderDiagnostic(d)}`).join("\n");
|
|
192
|
+
return {
|
|
193
|
+
structuredContent: { diagnostics },
|
|
194
|
+
content: [{ type: "text", text }]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return mcpToolError(err);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
mcp.registerTool("ase_specbook_export", {
|
|
202
|
+
title: "ASE SpecBook export",
|
|
203
|
+
description: "Export the SpecBook specification Markdown files of the project (located via the " +
|
|
204
|
+
"`project.artifact.spec.basedir` configuration) as JSON, JSON5, YAML, TOON, HTML, PDF, " +
|
|
205
|
+
"or normalized Markdown. The result is written to the `output` file (a relative path " +
|
|
206
|
+
"resolves against the project root) if given, else it is returned directly " +
|
|
207
|
+
"(PDF as a base64-encoded resource). The export fails on any lint diagnostic.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
format: z.enum(formats).optional()
|
|
210
|
+
.describe("output format (default: inferred from the `output` file extension, else `json`)"),
|
|
211
|
+
output: z.string().optional()
|
|
212
|
+
.describe("output file path (\"-\" or omitted returns the result directly)")
|
|
213
|
+
}
|
|
214
|
+
}, async (args) => {
|
|
215
|
+
try {
|
|
216
|
+
/* an explicit format takes the output as a plain file path, while
|
|
217
|
+
otherwise the output is an "[<format>:]<file>" specification */
|
|
218
|
+
const spec = args.format !== undefined || args.output === undefined ?
|
|
219
|
+
{ format: args.format ?? "json", output: args.output } :
|
|
220
|
+
parseOutputSpec(args.output);
|
|
221
|
+
const [data] = await Spec.export(this.log, [spec.format]);
|
|
222
|
+
if (spec.output !== undefined && spec.output !== "-") {
|
|
223
|
+
await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), data);
|
|
224
|
+
return { content: [{ type: "text", text: `exported specification into "${spec.output}" (${data.length} bytes)` }] };
|
|
225
|
+
}
|
|
226
|
+
else if (spec.format === "pdf")
|
|
227
|
+
return {
|
|
228
|
+
content: [{
|
|
229
|
+
type: "resource",
|
|
230
|
+
resource: { uri: "ase:specbook-export.pdf", mimeType: "application/pdf", blob: data.toString("base64") }
|
|
231
|
+
}]
|
|
232
|
+
};
|
|
233
|
+
else
|
|
234
|
+
return { content: [{ type: "text", text: data.toString("utf8") }] };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return mcpToolError(err);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
package/dst/ase.js
CHANGED
|
@@ -14,6 +14,7 @@ import HookCommand from "./ase-hook.js";
|
|
|
14
14
|
import StatuslineCommand from "./ase-statusline.js";
|
|
15
15
|
import TaskCommand from "./ase-task.js";
|
|
16
16
|
import ArtifactCommand from "./ase-artifact.js";
|
|
17
|
+
import SpecCommand from "./ase-spec.js";
|
|
17
18
|
import MetaCommand from "./ase-meta.js";
|
|
18
19
|
import CompatCommand from "./ase-compat.js";
|
|
19
20
|
import DiagramCommand from "./ase-diagram.js";
|
|
@@ -54,6 +55,7 @@ const main = async () => {
|
|
|
54
55
|
new StatuslineCommand(log).register(program);
|
|
55
56
|
new TaskCommand(log).register(program);
|
|
56
57
|
new ArtifactCommand(log).register(program);
|
|
58
|
+
new SpecCommand(log).register(program);
|
|
57
59
|
new MetaCommand(log).register(program);
|
|
58
60
|
new CompatCommand().register(program);
|
|
59
61
|
new DiagramCommand(log).register(program);
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.9.
|
|
9
|
+
"version": "0.9.63",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": { "ase": "bin/ase" },
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"eslint": "10.9.
|
|
19
|
+
"eslint": "10.9.1",
|
|
20
20
|
"@eslint/js": "10.0.1",
|
|
21
|
-
"@typescript-eslint/parser": "8.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
23
|
-
"typescript-eslint": "8.
|
|
21
|
+
"@typescript-eslint/parser": "8.68.0",
|
|
22
|
+
"@typescript-eslint/eslint-plugin": "8.68.0",
|
|
23
|
+
"typescript-eslint": "8.68.0",
|
|
24
24
|
"eslint-plugin-promise": "7.3.0",
|
|
25
25
|
"neostandard": "0.14.0-next.1",
|
|
26
26
|
"globals": "17.11.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"nodemon": "3.1.14",
|
|
31
31
|
"shx": "0.4.0",
|
|
32
32
|
|
|
33
|
-
"@types/node": "26.
|
|
33
|
+
"@types/node": "26.3.0",
|
|
34
34
|
"@types/luxon": "3.7.5",
|
|
35
35
|
"@types/which": "3.0.4",
|
|
36
36
|
"@types/update-notifier": "6.0.8",
|
|
@@ -64,8 +64,10 @@
|
|
|
64
64
|
"write-file-atomic": "8.0.0",
|
|
65
65
|
"pacote": "22.0.0",
|
|
66
66
|
"ofetch": "1.5.1",
|
|
67
|
-
"picomatch": "4.0.
|
|
68
|
-
"lru-cache": "11.5.2"
|
|
67
|
+
"picomatch": "4.0.7",
|
|
68
|
+
"lru-cache": "11.5.2",
|
|
69
|
+
"@rse/specbook": "1.0.2",
|
|
70
|
+
"source-code-error": "1.2.0"
|
|
69
71
|
},
|
|
70
72
|
"allowScripts": {
|
|
71
73
|
"fsevents": true
|
|
@@ -15,7 +15,7 @@ Workflow
|
|
|
15
15
|
1. Set the requested context: <context>$ARGUMENTS</context>.
|
|
16
16
|
The *first* whitespace-separated token of <context/> is the
|
|
17
17
|
comma-separated *aspect set* <aspects/> (a non-empty subset of the
|
|
18
|
-
aspect ids `A01`...`
|
|
18
|
+
aspect ids `A01`...`A21`). The *remaining* tokens are the source
|
|
19
19
|
code files to check.
|
|
20
20
|
|
|
21
21
|
2. Use the `Read` tool to read all source code files referenced by
|
|
@@ -28,7 +28,7 @@ Workflow
|
|
|
28
28
|
|
|
29
29
|
4. Set <problems/> to empty.
|
|
30
30
|
Then check the read source code for the following aspects (each
|
|
31
|
-
aspect is uniquely identified by its `aspect` id `A01 - XXX`...`
|
|
31
|
+
aspect is uniquely identified by its `aspect` id `A01 - XXX`...`A21
|
|
32
32
|
- XXX`), but *strictly limited* to those aspects whose id is
|
|
33
33
|
contained in the aspect set <aspects/> -- all other aspects are
|
|
34
34
|
*not* checked and their problems are *never* reported:
|
|
@@ -266,6 +266,50 @@ Workflow
|
|
|
266
266
|
*masks* another bug (e.g., unreachable code after a misplaced
|
|
267
267
|
`return` that skips cleanup logic).
|
|
268
268
|
|
|
269
|
+
- **A21 - DOCUMENTATION**:
|
|
270
|
+
Check for *incomplete* and for *excessive* code documentation
|
|
271
|
+
across the following sub-aspects. The yardstick is a *minimal*
|
|
272
|
+
description: one or two lines stating WHAT the construct does,
|
|
273
|
+
optimally written in the *idiomatic documentation convention* of
|
|
274
|
+
the project or the target programming language.
|
|
275
|
+
|
|
276
|
+
- **C1 MISSING-DOCUMENTATION**: functions, methods, classes,
|
|
277
|
+
interfaces, or modules without any documentation comment
|
|
278
|
+
describing their purpose -- *private* and *internal*
|
|
279
|
+
constructs included, not just the public API surface.
|
|
280
|
+
Exclude trivial constructs whose name already fully conveys
|
|
281
|
+
the purpose (plain getters/setters, one-line lambdas,
|
|
282
|
+
delegating overloads) and constructs inheriting the
|
|
283
|
+
documentation of an overridden or implemented declaration.
|
|
284
|
+
Propose *adding* those missing comments.
|
|
285
|
+
|
|
286
|
+
- **C2 EXCESSIVE-DOCUMENTATION**: comments going far beyond
|
|
287
|
+
the minimal description: narrated decision logs ("chose X
|
|
288
|
+
over Y because ..."), change history ("now uses X instead of
|
|
289
|
+
Y"), line-by-line explanations of the obvious, or comment
|
|
290
|
+
blocks substantially longer than the code they describe.
|
|
291
|
+
Propose *condensing* to a brief 1-2 line description (up to
|
|
292
|
+
4 lines only for genuinely non-obvious constraints).
|
|
293
|
+
|
|
294
|
+
- **C3 RESTATING-DOCUMENTATION**: comments merely repeating
|
|
295
|
+
the code or the identifier verbatim without adding any
|
|
296
|
+
information (e.g., "increment i" above `i++`). Propose
|
|
297
|
+
*removing* them.
|
|
298
|
+
|
|
299
|
+
- **C4 DRIFTED-DOCUMENTATION**: comments contradicting the
|
|
300
|
+
code they describe (stale parameter lists, outdated behavior
|
|
301
|
+
claims). Propose *correcting* the *comment* only -- *never*
|
|
302
|
+
change the code under this aspect.
|
|
303
|
+
|
|
304
|
+
Keep intact comments stating a *constraint the code cannot
|
|
305
|
+
show* (brief WHY-comments on non-obvious decisions) -- they are
|
|
306
|
+
neither excessive nor restating.
|
|
307
|
+
|
|
308
|
+
Severity guidance: C1 defaults to MEDIUM for non-trivial
|
|
309
|
+
constructs, else LOW; C2 and C3 default to LOW, escalating to
|
|
310
|
+
MEDIUM when the noise dominates the file; and C4 defaults to
|
|
311
|
+
MEDIUM (it actively misleads).
|
|
312
|
+
|
|
269
313
|
Be conservative - only report clear, well-grounded issues
|
|
270
314
|
that require an actual *code change*. Think twice to avoid
|
|
271
315
|
*false positives*.
|
|
@@ -277,7 +321,7 @@ Workflow
|
|
|
277
321
|
|
|
278
322
|
For *each* found problem which requires a code change:
|
|
279
323
|
|
|
280
|
-
1. Set <aspect/> to the identifier `A01 - XXX`...`
|
|
324
|
+
1. Set <aspect/> to the identifier `A01 - XXX`...`A21 - XXX`,
|
|
281
325
|
indicating the aspect under which the problem was detected.
|
|
282
326
|
|
|
283
327
|
2. Set <severity/> to the string `LOW`, `MEDIUM`, or `HIGH`,
|
package/plugin/etc/stx.conf
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
# [plugin] lint project
|
|
8
8
|
lint
|
|
9
|
-
markdownlint-cli2 --config etc/markdownlint.yaml meta/*.md skills/**/*.md && \
|
|
10
|
-
eslint --config etc/eslint.mjs meta skills
|
|
9
|
+
markdownlint-cli2 --config etc/markdownlint.yaml meta/*.md skills/**/*.md '!meta/ase-format-specbook.md' && \
|
|
10
|
+
eslint --config etc/eslint.mjs --ignore-pattern meta/ase-format-specbook.md meta skills
|
|
11
11
|
|
|
12
12
|
# [plugin] build project
|
|
13
13
|
build : lint
|
|
@@ -17,10 +17,12 @@ build : lint
|
|
|
17
17
|
echo "</purpose>"
|
|
18
18
|
done
|
|
19
19
|
) >skills/ase-help-intent/data.md
|
|
20
|
+
specbook describe -p meta -f raw >meta/ase-format-specbook.md
|
|
21
|
+
specbook describe -p schema -f raw >meta/ase-format-specbook.yaml
|
|
20
22
|
|
|
21
23
|
# [plugin] remove all generated files
|
|
22
24
|
clean
|
|
23
|
-
|
|
25
|
+
shx rm -f meta/ase-format-specbook.md meta/ase-format-specbook.yaml
|
|
24
26
|
|
|
25
27
|
# [plugin] remove all built files
|
|
26
28
|
distclean: clean
|