deepclause-pi 0.2.0 → 0.3.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.
package/src/planner.ts CHANGED
@@ -16,6 +16,8 @@ export interface PlanStepSpec {
16
16
  requiredTools: string[];
17
17
  relevantSkills: string[];
18
18
  expectedResult: string;
19
+ satisfies: string[];
20
+ checks: string[];
19
21
  }
20
22
 
21
23
  export interface PlanSpec {
@@ -26,6 +28,12 @@ export interface PlanSpec {
26
28
  steps: PlanStepSpec[];
27
29
  finalSynthesis?: string;
28
30
  failureMessage: string;
31
+ change?: string;
32
+ }
33
+
34
+ export interface ValidatePlanOptions {
35
+ requireChecks?: boolean;
36
+ change?: string;
29
37
  }
30
38
 
31
39
  export interface PlanningSnapshot {
@@ -70,7 +78,12 @@ export function normalizePlanSlug(value: string): string {
70
78
  return slug;
71
79
  }
72
80
 
73
- export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nameOverride?: string): ValidatedPlan {
81
+ export function validatePlanSpec(
82
+ value: unknown,
83
+ snapshot: PlanningSnapshot,
84
+ nameOverride?: string,
85
+ options: ValidatePlanOptions = {},
86
+ ): ValidatedPlan {
74
87
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Plan specification must be an object");
75
88
  const raw = value as Record<string, unknown>;
76
89
  if (!Array.isArray(raw.steps) || raw.steps.length < 1 || raw.steps.length > 12) {
@@ -86,13 +99,19 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
86
99
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`steps[${index}] must be an object`);
87
100
  const step = entry as Record<string, unknown>;
88
101
  const id = requireText(step.id ?? `step_${index + 1}`, `steps[${index}].id`, 80);
89
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(id)) throw new Error(`steps[${index}].id must be a simple identifier`);
102
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(id)) throw new Error(`steps[${index}].id must be a simple identifier (letters, digits, dot, dash, underscore)`);
90
103
  if (ids.has(id)) throw new Error(`Duplicate plan step id: ${id}`);
91
104
  ids.add(id);
92
105
  const executor = step.executor;
93
106
  if (executor !== "pi" && executor !== "dml") throw new Error(`steps[${index}].executor must be pi or dml`);
94
107
  const requiredTools = stringArray(step.requiredTools, `steps[${index}].requiredTools`, 16);
95
108
  const relevantSkills = stringArray(step.relevantSkills, `steps[${index}].relevantSkills`, 16);
109
+ const satisfies = stringArray(step.satisfies, `steps[${index}].satisfies`, 32);
110
+ const checks = stringArray(step.checks, `steps[${index}].checks`, 16);
111
+ for (const check of checks) parseCheck(check);
112
+ if (options.requireChecks && checks.length === 0) {
113
+ throw new Error(`Plan step ${id} must declare at least one verification check (cmd:..., exists:... or model:...)`);
114
+ }
96
115
 
97
116
  if (executor === "dml" && requiredTools.length > 0) {
98
117
  throw new Error(`DML step ${id} cannot request pi tools; use executor=pi`);
@@ -114,6 +133,8 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
114
133
  requiredTools: [...new Set(requiredTools)],
115
134
  relevantSkills: [...new Set(relevantSkills)],
116
135
  expectedResult: requireText(step.expectedResult, `steps[${index}].expectedResult`, 1_000),
136
+ satisfies: [...new Set(satisfies)],
137
+ checks: [...new Set(checks)],
117
138
  };
118
139
  });
119
140
 
@@ -127,6 +148,7 @@ export function validatePlanSpec(value: unknown, snapshot: PlanningSnapshot, nam
127
148
  ? requireText(raw.finalSynthesis, "finalSynthesis", 2_000)
128
149
  : undefined,
129
150
  failureMessage: requireText(raw.failureMessage, "failureMessage", 1_000),
151
+ change: options.change ? normalizePlanSlug(options.change) : undefined,
130
152
  };
131
153
 
132
154
  return {
@@ -148,6 +170,75 @@ function commentText(value: string): string {
148
170
  return value.replace(/[\r\n]+/g, " ").replace(/%/g, "percent").trim();
149
171
  }
150
172
 
173
+ export interface ParsedCheck {
174
+ kind: "cmd" | "exists" | "model";
175
+ value: string;
176
+ }
177
+
178
+ /** Parse an encoded verification check: cmd:<command>, exists:<path>, model:<question>. */
179
+ export function parseCheck(encoded: string): ParsedCheck {
180
+ const separator = encoded.indexOf(":");
181
+ if (separator <= 0) throw new Error(`Check must be cmd:..., exists:... or model:...; got '${encoded}'`);
182
+ const kind = encoded.slice(0, separator).trim();
183
+ const value = encoded.slice(separator + 1).trim();
184
+ if (kind !== "cmd" && kind !== "exists" && kind !== "model") throw new Error(`Unknown check kind '${kind}' in '${encoded}'`);
185
+ if (!value) throw new Error(`Check '${encoded}' has an empty value`);
186
+ return { kind, value };
187
+ }
188
+
189
+ /**
190
+ * Assemble the `tasks.dml` data artifact for a change: plan_task/2 definitions plus
191
+ * the managed plan_task_status/2 block. Verified by /dc-check and executed by /dc-apply.
192
+ */
193
+ export function assembleTasksDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string {
194
+ const { spec } = plan;
195
+ const taskBlocks = spec.steps.map((step) => {
196
+ const checks = step.checks.map((encoded) => {
197
+ const { kind, value } = parseCheck(encoded);
198
+ return `${kind}(${dmlString(value)})`;
199
+ });
200
+ return [
201
+ `plan_task(${dmlString(step.id)}, task{`,
202
+ ` executor: ${step.executor},`,
203
+ ` do: ${dmlString(step.instruction)},`,
204
+ ` tools: ${dmlStringList(step.requiredTools)},`,
205
+ ` expected: ${dmlString(step.expectedResult)},`,
206
+ ` satisfies: ${dmlStringList(step.satisfies)},`,
207
+ ` checks: [${checks.join(", ")}]`,
208
+ `}).`,
209
+ ].join("\n");
210
+ });
211
+ const statusLines = spec.steps.map((step) => `plan_task_status(${dmlString(step.id)}, pending).`);
212
+ const metadata = [
213
+ "% tasks.dml — generated by /dc-plan. Edit conservatively.",
214
+ `% Change: ${commentText(spec.change ?? spec.slug)}`,
215
+ `% Title: ${commentText(spec.title)}`,
216
+ `% Planning model: ${commentText(snapshot.model)}`,
217
+ `% Required pi tools: ${plan.requiredTools.join(", ") || "none"}`,
218
+ ].join("\n");
219
+ return `${metadata}\n\n${taskBlocks.join("\n\n")}\n\n% --- execution state (managed by apply.dml; do not edit by hand) ---\n${statusLines.join("\n")}\n`;
220
+ }
221
+
222
+ /** Write changes/<slug>/tasks.dml, refusing to clobber an existing plan unless overwrite=true. */
223
+ export async function writeChangeTasks(paths: DeepClausePaths, slug: string, text: string, overwrite = false): Promise<string> {
224
+ const dir = path.join(paths.changes, slug);
225
+ await mkdir(dir, { recursive: true });
226
+ const filePath = path.join(dir, "tasks.dml");
227
+ if (overwrite) {
228
+ await writeFile(filePath, text, { encoding: "utf8" });
229
+ return filePath;
230
+ }
231
+ try {
232
+ await writeFile(filePath, text, { encoding: "utf8", flag: "wx" });
233
+ } catch (error) {
234
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
235
+ throw new Error(`changes/${slug}/tasks.dml already exists; re-run with --update to regenerate it`);
236
+ }
237
+ throw error;
238
+ }
239
+ return filePath;
240
+ }
241
+
151
242
  export function assemblePlanDml(plan: ValidatedPlan, snapshot: PlanningSnapshot): string {
152
243
  const { spec } = plan;
153
244
  const resultVariables: string[] = [];
@@ -209,6 +300,16 @@ export async function validateGeneratedPlan(dml: string): Promise<void> {
209
300
  if (!validation.valid) throw new Error(`Generated DML failed validation: ${validation.errors.join("; ")}`);
210
301
  }
211
302
 
303
+ /**
304
+ * Data-only artifact (tasks.dml): no agent_main of its own, so validation appends a
305
+ * trivial entry point to parse the facts without changing what is written.
306
+ */
307
+ export async function validateGeneratedTasks(dml: string): Promise<void> {
308
+ if (dml.includes(".deepclause/")) throw new Error("Generated tasks may not reference .deepclause/");
309
+ const validation = await validateWithProlog(`${dml}\nagent_main :- true.\n`);
310
+ if (!validation.valid) throw new Error(`Generated tasks.dml failed validation: ${validation.errors.join("; ")}`);
311
+ }
312
+
212
313
  export async function writePlanNonDestructively(paths: DeepClausePaths, slug: string, dml: string): Promise<string> {
213
314
  await mkdir(paths.plans, { recursive: true });
214
315
  for (let suffix = 1; suffix <= 100; suffix++) {
@@ -235,7 +336,7 @@ export async function readPlanRequiredTools(filePath: string): Promise<string[]>
235
336
  return [...new Set(match[1]!.split(",").map((name) => name.trim()).filter(Boolean))];
236
337
  }
237
338
 
238
- export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string): string {
339
+ export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot, nameOverride?: string, change?: string, update = false): string {
239
340
  const tools = snapshot.allTools.map((tool) => ({
240
341
  name: tool.name,
241
342
  active: snapshot.activeTools.includes(tool.name),
@@ -244,6 +345,24 @@ export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot,
244
345
  guidelines: tool.promptGuidelines ?? [],
245
346
  source: tool.sourceInfo,
246
347
  }));
348
+ const changeInstructions = change
349
+ ? [
350
+ update
351
+ ? `This regenerates the change '${change}'. Read the existing .pi/deepclause/changes/${change}/proposal.md, specs/**, design.md and tasks.dml first, then revise them as needed.`
352
+ : `This plan is for the change '${change}'. Before committing, create .pi/deepclause/changes/${change}/ with normal file tools:`,
353
+ ...(update
354
+ ? []
355
+ : [
356
+ "- proposal.md — why / what / capabilities / impact.",
357
+ "- specs/<capability>.spec.md — delta(s) using ## ADDED|MODIFIED|REMOVED Requirements.",
358
+ "- design.md — optional approach and trade-offs.",
359
+ ]),
360
+ "Specs describe behaviour only: no commands, file paths, library choices or implementation steps.",
361
+ "Use exactly three hashes for ### Requirement and four for #### Scenario; every requirement needs at least one scenario.",
362
+ "Each committed step must list the scenario ids it satisfies (capability#scenario-slug) and at least one check encoded as cmd:<command>, exists:<path> or model:<question>.",
363
+ "The committed tasks.dml replaces any previous one; every plan_task_status entry resets to pending.",
364
+ ]
365
+ : [];
247
366
  return [
248
367
  "Create an executable DeepClause plan for the request below.",
249
368
  "You are in a normal pi turn: inspect the workspace and use currently active tools when that materially improves the plan.",
@@ -253,6 +372,7 @@ export function buildPlanningPrompt(request: string, snapshot: PlanningSnapshot,
253
372
  "Choose executor='dml' for contained reasoning that needs no pi tool; requiredTools must then be empty.",
254
373
  "Use only exact active tool names. Never request dc_run, dc_plan_commit, or pi_agent_step.",
255
374
  "Keep steps bounded, concrete, ordered, and independently observable. Prefer 3-8 steps.",
375
+ ...changeInstructions,
256
376
  nameOverride ? `The user requested the plan filename slug: ${nameOverride}` : "Choose a concise lowercase slug.",
257
377
  `User request:\n${request}`,
258
378
  `Current model: ${snapshot.model}; thinking level: ${snapshot.thinkingLevel}`,
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { realpath, readFile } from "node:fs/promises";
1
+ import { realpath, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createDeepClause } from "deepclause-sdk";
4
4
  import type {
@@ -16,6 +16,66 @@ import { PI_AGENT_STEP_TOOL } from "./planner.js";
16
16
 
17
17
  export const PI_WORKSPACE_LIST_TOOL = "pi_workspace_list";
18
18
  export const PI_BASH_TOOL = "pi_bash";
19
+ export const DC_VERIFY_RUN_TOOL = "dc_verify_run";
20
+ export const DC_APPLY_SNAPSHOT_TOOL = "dc_apply_snapshot";
21
+ export const DC_APPLY_ACCEPT_TOOL = "dc_apply_accept";
22
+ export const DC_APPLY_RESTORE_TOOL = "dc_apply_restore";
23
+
24
+ async function readChangeJson(changeJsonPath: string): Promise<Record<string, unknown>> {
25
+ try {
26
+ const parsed: unknown = JSON.parse(await readFile(changeJsonPath, "utf8"));
27
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+
33
+ async function patchChangeJson(changeJsonPath: string, patch: Record<string, unknown>): Promise<void> {
34
+ let existing: Record<string, unknown> = {};
35
+ try {
36
+ const parsed: unknown = JSON.parse(await readFile(changeJsonPath, "utf8"));
37
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
38
+ } catch {
39
+ // start a fresh manifest
40
+ }
41
+ await writeFile(changeJsonPath, `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, "utf8");
42
+ }
43
+
44
+ /**
45
+ * Record a git snapshot ref, refusing a dirty working tree. If an apply is already
46
+ * in progress for this change, the existing ref is returned so the run resumes
47
+ * instead of starting over or rejecting the (intentionally) dirty tree.
48
+ */
49
+ export async function gitSnapshot(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string> {
50
+ const existing = await readChangeJson(changeJsonPath);
51
+ const resumable = typeof existing.snapshot === "string" && existing.snapshot && existing.applyState === "in_progress";
52
+ if (resumable) return existing.snapshot as string;
53
+
54
+ const status = await pi.exec("git", ["status", "--porcelain"], { cwd });
55
+ if (status.code !== 0) throw new Error("git is unavailable or this is not a repository");
56
+ if (status.stdout.trim()) throw new Error("working tree is dirty; commit or stash before applying, or resume with /dc-apply (which preserves an interrupted apply)");
57
+ const head = await pi.exec("git", ["rev-parse", "HEAD"], { cwd });
58
+ if (head.code !== 0) throw new Error("could not read git HEAD");
59
+ const ref = head.stdout.trim();
60
+ await patchChangeJson(changeJsonPath, { snapshot: ref, applyState: "in_progress" });
61
+ return ref;
62
+ }
63
+
64
+ /** Restore the recorded snapshot (hard reset plus clean of untracked files). */
65
+ export async function gitRestore(pi: Pick<ExtensionAPI, "exec">, cwd: string, changeJsonPath: string): Promise<string | null> {
66
+ const existing = await readChangeJson(changeJsonPath);
67
+ const ref = typeof existing.snapshot === "string" && existing.snapshot ? existing.snapshot : null;
68
+ if (!ref) return null;
69
+ await pi.exec("git", ["reset", "--hard", ref], { cwd });
70
+ await pi.exec("git", ["clean", "-fd"], { cwd });
71
+ await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "aborted" });
72
+ return ref;
73
+ }
74
+
75
+ /** Mark the apply accepted and clear the snapshot. */
76
+ export async function gitAccept(changeJsonPath: string): Promise<void> {
77
+ await patchChangeJson(changeJsonPath, { snapshot: null, applyState: "done" });
78
+ }
19
79
 
20
80
  export type BashApproval = (command: string, signal: AbortSignal) => Promise<boolean>;
21
81
 
@@ -273,6 +333,8 @@ export async function executeDml(
273
333
  controller: AbortController,
274
334
  callbacks: ExecutionCallbacks,
275
335
  runPiAgentStep?: (request: PiAgentStepRequest, signal: AbortSignal) => Promise<PiAgentStepResult>,
336
+ verifyCommands: string[] = [],
337
+ applyChangeJsonPath?: string,
276
338
  ): Promise<ExecutionResult> {
277
339
  const model = ctx.model;
278
340
  if (!model) throw new Error("Select a pi model before running DeepClause");
@@ -325,9 +387,62 @@ export async function executeDml(
325
387
  },
326
388
  });
327
389
  }
390
+ if (verifyCommands.length > 0) {
391
+ sdk.registerTool(DC_VERIFY_RUN_TOOL, {
392
+ description: "Run one of the change's pre-approved verification commands in the workspace and return its exit code, stdout and stderr.",
393
+ parameters: {
394
+ type: "object",
395
+ properties: {
396
+ command: { type: "string", description: "Exact approved command string" },
397
+ },
398
+ required: ["command"],
399
+ },
400
+ execute: async (args) => {
401
+ const command = typeof args.command === "string" ? args.command.trim() : "";
402
+ if (!verifyCommands.includes(command)) {
403
+ throw new Error(`verification command was not approved for this run: ${command}`);
404
+ }
405
+ const result = await pi.exec("bash", ["-lc", command], {
406
+ cwd: ctx.cwd,
407
+ signal: controller.signal,
408
+ timeout: 120_000,
409
+ });
410
+ return { command, stdout: result.stdout, stderr: result.stderr, exitCode: result.code, killed: result.killed };
411
+ },
412
+ });
413
+ }
414
+
415
+ if (applyChangeJsonPath) {
416
+ const changeJsonPath = applyChangeJsonPath;
417
+ sdk.registerTool(DC_APPLY_SNAPSHOT_TOOL, {
418
+ description: "Record a git snapshot of the workspace before applying tasks. Refuses a dirty working tree.",
419
+ parameters: { type: "object", properties: {}, required: [] },
420
+ execute: async () => gitSnapshot(pi, ctx.cwd, changeJsonPath),
421
+ });
422
+ sdk.registerTool(DC_APPLY_ACCEPT_TOOL, {
423
+ description: "Accept the apply so the working tree is not restored.",
424
+ parameters: { type: "object", properties: {}, required: [] },
425
+ execute: async () => {
426
+ await gitAccept(changeJsonPath);
427
+ return "accepted";
428
+ },
429
+ });
430
+ sdk.registerTool(DC_APPLY_RESTORE_TOOL, {
431
+ description: "Restore the recorded snapshot, discarding task changes.",
432
+ parameters: { type: "object", properties: {}, required: [] },
433
+ execute: async () => (await gitRestore(pi, ctx.cwd, changeJsonPath)) ?? "none",
434
+ });
435
+ }
436
+
328
437
  sdk.setToolPolicy({
329
438
  mode: "whitelist",
330
- tools: [PI_WORKSPACE_LIST_TOOL, PI_BASH_TOOL, ...(runPiAgentStep ? [PI_AGENT_STEP_TOOL] : [])],
439
+ tools: [
440
+ PI_WORKSPACE_LIST_TOOL,
441
+ PI_BASH_TOOL,
442
+ ...(runPiAgentStep ? [PI_AGENT_STEP_TOOL] : []),
443
+ ...(verifyCommands.length > 0 ? [DC_VERIFY_RUN_TOOL] : []),
444
+ ...(applyChangeJsonPath ? [DC_APPLY_SNAPSHOT_TOOL, DC_APPLY_ACCEPT_TOOL, DC_APPLY_RESTORE_TOOL] : []),
445
+ ],
331
446
  });
332
447
 
333
448
  const result: ExecutionResult = {
package/src/workspace.ts CHANGED
@@ -7,6 +7,9 @@ export interface DeepClausePaths {
7
7
  root: string;
8
8
  skills: string;
9
9
  plans: string;
10
+ specs: string;
11
+ changes: string;
12
+ lib: string;
10
13
  config: string;
11
14
  agents: string;
12
15
  reference: string;
@@ -57,6 +60,9 @@ export function getPaths(cwd: string): DeepClausePaths {
57
60
  root,
58
61
  skills: path.join(root, "skills"),
59
62
  plans: path.join(root, "plans"),
63
+ specs: path.join(root, "specs"),
64
+ changes: path.join(root, "changes"),
65
+ lib: path.join(root, "lib"),
60
66
  config: path.join(root, "config.json"),
61
67
  agents: path.join(root, "AGENTS.md"),
62
68
  reference: path.join(root, "DML_REFERENCE.md"),
@@ -81,6 +87,10 @@ async function bundledAuthoringGuide(): Promise<string> {
81
87
  return readFile(fileURLToPath(new URL("./assets/AGENTS.md", import.meta.url)), "utf8");
82
88
  }
83
89
 
90
+ async function bundledAsset(name: string): Promise<string> {
91
+ return readFile(fileURLToPath(new URL(`./assets/${name}`, import.meta.url)), "utf8");
92
+ }
93
+
84
94
  async function bundledDeepResearch(): Promise<string> {
85
95
  return readFile(fileURLToPath(new URL("./assets/deep_research.dml", import.meta.url)), "utf8");
86
96
  }
@@ -90,6 +100,9 @@ export async function initializeWorkspace(cwd: string): Promise<DeepClausePaths>
90
100
  await Promise.all([
91
101
  mkdir(paths.skills, { recursive: true }),
92
102
  mkdir(paths.plans, { recursive: true }),
103
+ mkdir(paths.specs, { recursive: true }),
104
+ mkdir(paths.changes, { recursive: true }),
105
+ mkdir(paths.lib, { recursive: true }),
93
106
  ]);
94
107
  await Promise.all([
95
108
  writeIfMissing(paths.config, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`),
@@ -97,6 +110,17 @@ export async function initializeWorkspace(cwd: string): Promise<DeepClausePaths>
97
110
  writeIfMissing(paths.reference, await bundledReference()),
98
111
  writeIfMissing(path.join(paths.skills, "example.dml"), EXAMPLE_DML),
99
112
  writeIfMissing(path.join(paths.skills, "deep_research.dml"), await bundledDeepResearch()),
113
+ writeIfMissing(path.join(paths.lib, "specs.dml"), await bundledAsset("specs.dml")),
114
+ writeIfMissing(path.join(paths.lib, "apply.dml"), await bundledAsset("apply.dml")),
115
+ writeIfMissing(path.join(paths.skills, "spec_validate.dml"), await bundledAsset("spec_validate.dml")),
116
+ writeIfMissing(path.join(paths.skills, "spec_status.dml"), await bundledAsset("spec_status.dml")),
117
+ writeIfMissing(path.join(paths.skills, "spec_query.dml"), await bundledAsset("spec_query.dml")),
118
+ writeIfMissing(path.join(paths.skills, "spec_graph.dml"), await bundledAsset("spec_graph.dml")),
119
+ writeIfMissing(path.join(paths.skills, "spec_merge.dml"), await bundledAsset("spec_merge.dml")),
120
+ writeIfMissing(path.join(paths.skills, "spec_archive.dml"), await bundledAsset("spec_archive.dml")),
121
+ writeIfMissing(path.join(paths.skills, "spec_coverage.dml"), await bundledAsset("spec_coverage.dml")),
122
+ writeIfMissing(path.join(paths.skills, "spec_scaffold.dml"), await bundledAsset("spec_scaffold.dml")),
123
+ writeIfMissing(path.join(paths.skills, "spec_apply.dml"), await bundledAsset("spec_apply.dml")),
100
124
  ]);
101
125
  return paths;
102
126
  }