@workflow-cannon/workspace-kit 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/adapters/index.d.ts +1 -0
  4. package/dist/adapters/index.js +1 -0
  5. package/dist/cli.js +60 -2
  6. package/dist/contracts/index.d.ts +1 -0
  7. package/dist/contracts/index.js +1 -0
  8. package/dist/contracts/module-contract.d.ts +62 -0
  9. package/dist/contracts/module-contract.js +1 -0
  10. package/dist/core/index.d.ts +3 -0
  11. package/dist/core/index.js +2 -0
  12. package/dist/core/module-command-router.d.ts +27 -0
  13. package/dist/core/module-command-router.js +84 -0
  14. package/dist/core/module-registry.d.ts +24 -0
  15. package/dist/core/module-registry.js +183 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +3 -0
  18. package/dist/modules/approvals/index.d.ts +2 -0
  19. package/dist/modules/approvals/index.js +30 -0
  20. package/dist/modules/documentation/index.d.ts +3 -0
  21. package/dist/modules/documentation/index.js +98 -0
  22. package/dist/modules/documentation/runtime.d.ts +12 -0
  23. package/dist/modules/documentation/runtime.js +368 -0
  24. package/dist/modules/documentation/types.d.ts +46 -0
  25. package/dist/modules/documentation/types.js +1 -0
  26. package/dist/modules/improvement/index.d.ts +2 -0
  27. package/dist/modules/improvement/index.js +30 -0
  28. package/dist/modules/index.d.ts +7 -0
  29. package/dist/modules/index.js +5 -0
  30. package/dist/modules/planning/index.d.ts +2 -0
  31. package/dist/modules/planning/index.js +30 -0
  32. package/dist/modules/task-engine/generator.d.ts +2 -0
  33. package/dist/modules/task-engine/generator.js +101 -0
  34. package/dist/modules/task-engine/importer.d.ts +8 -0
  35. package/dist/modules/task-engine/importer.js +157 -0
  36. package/dist/modules/task-engine/index.d.ts +9 -0
  37. package/dist/modules/task-engine/index.js +253 -0
  38. package/dist/modules/task-engine/service.d.ts +21 -0
  39. package/dist/modules/task-engine/service.js +105 -0
  40. package/dist/modules/task-engine/store.d.ts +16 -0
  41. package/dist/modules/task-engine/store.js +88 -0
  42. package/dist/modules/task-engine/suggestions.d.ts +2 -0
  43. package/dist/modules/task-engine/suggestions.js +51 -0
  44. package/dist/modules/task-engine/transitions.d.ts +23 -0
  45. package/dist/modules/task-engine/transitions.js +109 -0
  46. package/dist/modules/task-engine/types.d.ts +82 -0
  47. package/dist/modules/task-engine/types.js +1 -0
  48. package/dist/ops/index.d.ts +1 -0
  49. package/dist/ops/index.js +1 -0
  50. package/package.json +4 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Workflow Cannon contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ AI agents: `./.ai/` is the authoritative source of truth; read all files there before any analysis, planning, or code changes, and follow `./.ai/` over `README.md`, `./docs/`, and conflicting existing code patterns.
2
+
3
+ # Workflow Cannon
4
+
5
+ Workflow Cannon is the canonical home of `@workflow-cannon/workspace-kit` and the operational docs that drive its evolution.
6
+
7
+ It is built for developers using VS Code who want safe, reproducible, package-first workflow automation with clear release evidence.
8
+
9
+ ## Table of Contents
10
+
11
+ - [What This Repository Is](#what-this-repository-is)
12
+ - [Big-Picture Vision](#big-picture-vision)
13
+ - [Current Status](#current-status)
14
+ - [Goals](#goals)
15
+ - [Package](#package)
16
+ - [Repository Map](#repository-map)
17
+ - [Documentation Index](#documentation-index)
18
+ - [License](#license)
19
+
20
+ ## What This Repository Is
21
+
22
+ Workflow Cannon is the source of truth for:
23
+
24
+ - The `@workflow-cannon/workspace-kit` package
25
+ - Maintainer planning and execution artifacts
26
+ - Consumer validation and release-readiness evidence
27
+
28
+ Guiding characteristics:
29
+
30
+ - Package-first delivery and verification
31
+ - Deterministic, auditable workflows
32
+ - Safe-by-default operations (validation, traceability, rollback-friendly changes)
33
+
34
+ ## Big-Picture Vision
35
+
36
+ Workflow Cannon is evolving from a package and docs repository into a developer workflow platform that can:
37
+
38
+ - model planning, tasks, policy, and execution as first-class, versioned contracts
39
+ - run repeatable workflows with deterministic outcomes and evidence capture
40
+ - continuously improve itself based on observed friction and outcome data
41
+
42
+ The long-term direction in this repository is to close the loop between:
43
+
44
+ 1. **What happened** (transcripts, diffs, run artifacts, diagnostics)
45
+ 2. **What should change** (recommendations to templates, rules, process, and config)
46
+ 3. **What gets adopted** (human-reviewed approvals, policy checks, safe rollout)
47
+
48
+ ### Enhancement Engine (automatic learning and correction)
49
+
50
+ The Improvement/Enhancement Engine is intended to detect weak spots in workflows and rules, then generate high-signal fixes with supporting evidence. Instead of hard-coding static process forever, the system should learn from real usage patterns and propose better defaults.
51
+
52
+ In practice, this means:
53
+
54
+ - detect recurring failure patterns, manual rework, and template drift
55
+ - emit recommendation items with confidence, deduping, and provenance
56
+ - route recommendations through an approval queue (`accept`, `decline`, `accept edited`)
57
+ - apply approved changes through guarded automation (dry-run, diff, rollback-ready)
58
+ - measure post-change outcomes so future recommendations improve over time
59
+
60
+ This keeps automation adaptive without sacrificing safety, governance, or developer trust.
61
+
62
+ ## Current Status
63
+
64
+ - Project tracking has been reset for split-repo execution.
65
+ - Current execution phase is **Phase 0 (foundation)**.
66
+ - Phase 0 opens with `T178` to `T180` for release hardening and consumer cadence definition.
67
+
68
+ ## Goals
69
+
70
+ - Keep package implementation and release operations centralized here.
71
+ - Preserve independent consumer validation and update cadence.
72
+ - Grow modular capabilities for planning, tasking, configuration, policy, and improvement.
73
+ - Build a human-governed enhancement loop that learns from usage and recommends better workflows/rules.
74
+ - Maintain deterministic and auditable behavior as system complexity increases.
75
+
76
+ ## Package
77
+
78
+ Install:
79
+
80
+ ```bash
81
+ npm install @workflow-cannon/workspace-kit
82
+ ```
83
+
84
+ ## Repository Map
85
+
86
+ - `README.md` - project entry point
87
+ - `.ai/PRINCIPLES.md` - project goals and decision principles (canonical AI)
88
+ - `docs/maintainers/ROADMAP.md` - roadmap and decision log
89
+ - `docs/maintainers/TASKS.md` - execution tracking
90
+ - `docs/maintainers/ARCHITECTURE.md` - architecture direction
91
+ - `docs/maintainers/DECISIONS.md` - focused design/decision notes
92
+ - `docs/maintainers/RELEASING.md` - release checklist and validation expectations
93
+ - `.ai/module-build.md` - canonical AI module build guidance
94
+ - `docs/maintainers/` - maintainer process and boundary docs
95
+ - `docs/maintainers/module-build-guide.md` - human-readable module build guidance
96
+ - `docs/adr/` - ADR templates and records
97
+
98
+ ## Documentation Index
99
+
100
+ - Project goals and decision principles: `.ai/PRINCIPLES.md`
101
+ - Strategy and long-range direction: `docs/maintainers/ROADMAP.md`
102
+ - Active execution tasks: `docs/maintainers/TASKS.md`
103
+ - Glossary and agent-guidance terms: `docs/maintainers/TERMS.md`
104
+ - Architecture direction: `docs/maintainers/ARCHITECTURE.md`
105
+ - Project decisions: `docs/maintainers/DECISIONS.md`
106
+ - Contribution guidelines: `docs/maintainers/CONTRIBUTING.md`
107
+ - Release process and gates: `docs/maintainers/RELEASING.md`
108
+ - Canonical AI module build guidance: `.ai/module-build.md`
109
+ - Human module build guide: `docs/maintainers/module-build-guide.md`
110
+ - Security, support, and governance: `docs/maintainers/SECURITY.md`, `docs/maintainers/SUPPORT.md`, `docs/maintainers/GOVERNANCE.md`
111
+ - AI behavior rules and command wrappers: `.cursor/rules/`, `.cursor/commands/`
112
+
113
+ ## License
114
+
115
+ Licensed under MIT. See `LICENSE`.
@@ -0,0 +1 @@
1
+ export type AdapterVersion = "0.1";
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js CHANGED
@@ -2,6 +2,13 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { ModuleRegistry } from "./core/module-registry.js";
6
+ import { ModuleCommandRouter } from "./core/module-command-router.js";
7
+ import { documentationModule } from "./modules/documentation/index.js";
8
+ import { taskEngineModule } from "./modules/task-engine/index.js";
9
+ import { approvalsModule } from "./modules/approvals/index.js";
10
+ import { planningModule } from "./modules/planning/index.js";
11
+ import { improvementModule } from "./modules/improvement/index.js";
5
12
  const EXIT_SUCCESS = 0;
6
13
  const EXIT_VALIDATION_FAILURE = 1;
7
14
  const EXIT_USAGE_ERROR = 2;
@@ -305,7 +312,7 @@ export async function runCli(args, options = {}) {
305
312
  const writeError = options.writeError ?? console.error;
306
313
  const [command] = args;
307
314
  if (!command) {
308
- writeError("Usage: workspace-kit <init|doctor|check|upgrade>");
315
+ writeError("Usage: workspace-kit <init|doctor|check|upgrade|drift-check|run>");
309
316
  return EXIT_USAGE_ERROR;
310
317
  }
311
318
  if (command === "init") {
@@ -525,8 +532,59 @@ export async function runCli(args, options = {}) {
525
532
  }
526
533
  return EXIT_SUCCESS;
527
534
  }
535
+ if (command === "run") {
536
+ const allModules = [
537
+ documentationModule,
538
+ taskEngineModule,
539
+ approvalsModule,
540
+ planningModule,
541
+ improvementModule
542
+ ];
543
+ const registry = new ModuleRegistry(allModules);
544
+ const router = new ModuleCommandRouter(registry);
545
+ const subcommand = args[1];
546
+ if (!subcommand) {
547
+ const commands = router.listCommands();
548
+ writeLine("Available module commands:");
549
+ for (const cmd of commands) {
550
+ const desc = cmd.description ? ` — ${cmd.description}` : "";
551
+ writeLine(` ${cmd.name} (${cmd.moduleId})${desc}`);
552
+ }
553
+ writeLine("");
554
+ writeLine("Usage: workspace-kit run <command> [json-args]");
555
+ return EXIT_SUCCESS;
556
+ }
557
+ let commandArgs = {};
558
+ if (args[2]) {
559
+ try {
560
+ const parsed = JSON.parse(args[2]);
561
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
562
+ commandArgs = parsed;
563
+ }
564
+ else {
565
+ writeError("Command args must be a JSON object.");
566
+ return EXIT_USAGE_ERROR;
567
+ }
568
+ }
569
+ catch {
570
+ writeError(`Invalid JSON args: ${args[2]}`);
571
+ return EXIT_USAGE_ERROR;
572
+ }
573
+ }
574
+ const ctx = { runtimeVersion: "0.1", workspacePath: cwd };
575
+ try {
576
+ const result = await router.execute(subcommand, commandArgs, ctx);
577
+ writeLine(JSON.stringify(result, null, 2));
578
+ return result.ok ? EXIT_SUCCESS : EXIT_VALIDATION_FAILURE;
579
+ }
580
+ catch (error) {
581
+ const message = error instanceof Error ? error.message : String(error);
582
+ writeError(`Module command failed: ${message}`);
583
+ return EXIT_INTERNAL_ERROR;
584
+ }
585
+ }
528
586
  if (command !== "doctor") {
529
- writeError(`Unknown command '${command}'. Supported commands: init, doctor, check, upgrade, drift-check.`);
587
+ writeError(`Unknown command '${command}'. Supported commands: init, doctor, check, upgrade, drift-check, run.`);
530
588
  return EXIT_USAGE_ERROR;
531
589
  }
532
590
  const issues = [];
@@ -0,0 +1 @@
1
+ export type { ModuleCommand, ModuleCommandResult, ModuleCapability, ModuleDocumentContract, ModuleEvent, ModuleInstructionContract, ModuleInstructionEntry, ModuleLifecycleContext, ModuleRegistration, WorkflowModule } from "./module-contract.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ export type ModuleCapability = "documentation" | "task-engine" | "planning" | "improvement" | "approvals" | "diagnostics" | "migration";
2
+ export type ModuleDocumentContract = {
3
+ path: string;
4
+ format: "md";
5
+ description?: string;
6
+ };
7
+ export type ModuleInstructionEntry = {
8
+ /**
9
+ * Function-like instruction name, e.g. "document-project".
10
+ */
11
+ name: string;
12
+ /**
13
+ * Markdown instruction file path under the module instruction directory.
14
+ */
15
+ file: string;
16
+ description?: string;
17
+ };
18
+ export type ModuleInstructionContract = {
19
+ /**
20
+ * Directory containing markdown instruction files for this module.
21
+ */
22
+ directory: string;
23
+ entries: ModuleInstructionEntry[];
24
+ };
25
+ export type ModuleEvent = {
26
+ type: string;
27
+ payload?: Record<string, unknown>;
28
+ };
29
+ export type ModuleCommand = {
30
+ name: string;
31
+ args?: Record<string, unknown>;
32
+ };
33
+ export type ModuleCommandResult = {
34
+ ok: boolean;
35
+ code: string;
36
+ message?: string;
37
+ data?: Record<string, unknown>;
38
+ };
39
+ export type ModuleLifecycleContext = {
40
+ runtimeVersion: string;
41
+ workspacePath: string;
42
+ };
43
+ export type ModuleRegistration = {
44
+ id: string;
45
+ version: string;
46
+ contractVersion: "1";
47
+ capabilities: ModuleCapability[];
48
+ dependsOn: string[];
49
+ enabledByDefault: boolean;
50
+ config: ModuleDocumentContract;
51
+ state: ModuleDocumentContract;
52
+ instructions: ModuleInstructionContract;
53
+ };
54
+ export interface WorkflowModule {
55
+ registration: ModuleRegistration;
56
+ onInstall?(ctx: ModuleLifecycleContext): Promise<void>;
57
+ onConfigChange?(ctx: ModuleLifecycleContext): Promise<void>;
58
+ onStart?(ctx: ModuleLifecycleContext): Promise<void>;
59
+ onStop?(ctx: ModuleLifecycleContext): Promise<void>;
60
+ onEvent?(event: ModuleEvent, ctx: ModuleLifecycleContext): Promise<void>;
61
+ onCommand?(command: ModuleCommand, ctx: ModuleLifecycleContext): Promise<ModuleCommandResult>;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { ModuleRegistry, ModuleRegistryError, validateModuleSet, type ModuleRegistryOptions } from "./module-registry.js";
2
+ export { ModuleCommandRouter, ModuleCommandRouterError, type ModuleCommandDescriptor, type ModuleCommandRouterOptions } from "./module-command-router.js";
3
+ export type CoreRuntimeVersion = "0.1";
@@ -0,0 +1,2 @@
1
+ export { ModuleRegistry, ModuleRegistryError, validateModuleSet } from "./module-registry.js";
2
+ export { ModuleCommandRouter, ModuleCommandRouterError } from "./module-command-router.js";
@@ -0,0 +1,27 @@
1
+ import type { ModuleCommandResult, ModuleLifecycleContext } from "../contracts/module-contract.js";
2
+ import { ModuleRegistry } from "./module-registry.js";
3
+ export type ModuleCommandDescriptor = {
4
+ name: string;
5
+ moduleId: string;
6
+ instructionFile: string;
7
+ description?: string;
8
+ };
9
+ export type ModuleCommandRouterOptions = {
10
+ aliases?: Record<string, string>;
11
+ };
12
+ export declare class ModuleCommandRouterError extends Error {
13
+ readonly code: string;
14
+ constructor(code: string, message: string);
15
+ }
16
+ export declare class ModuleCommandRouter {
17
+ private readonly commands;
18
+ private readonly aliases;
19
+ private readonly registry;
20
+ constructor(registry: ModuleRegistry, options?: ModuleCommandRouterOptions);
21
+ listCommands(): ModuleCommandDescriptor[];
22
+ describeCommand(name: string): ModuleCommandDescriptor | undefined;
23
+ execute(name: string, args: Record<string, unknown> | undefined, ctx: ModuleLifecycleContext): Promise<ModuleCommandResult>;
24
+ private indexEnabledModuleCommands;
25
+ private validateAliases;
26
+ private resolveCommandName;
27
+ }
@@ -0,0 +1,84 @@
1
+ export class ModuleCommandRouterError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = "ModuleCommandRouterError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export class ModuleCommandRouter {
10
+ commands = new Map();
11
+ aliases;
12
+ registry;
13
+ constructor(registry, options) {
14
+ this.registry = registry;
15
+ this.aliases = options?.aliases ?? {};
16
+ this.indexEnabledModuleCommands();
17
+ this.validateAliases();
18
+ }
19
+ listCommands() {
20
+ return [...this.commands.values()]
21
+ .map((entry) => entry.descriptor)
22
+ .sort((a, b) => a.name.localeCompare(b.name));
23
+ }
24
+ describeCommand(name) {
25
+ const commandName = this.resolveCommandName(name);
26
+ return this.commands.get(commandName)?.descriptor;
27
+ }
28
+ async execute(name, args, ctx) {
29
+ const commandName = this.resolveCommandName(name);
30
+ const indexed = this.commands.get(commandName);
31
+ if (!indexed) {
32
+ const known = this.listCommands()
33
+ .map((command) => command.name)
34
+ .join(", ");
35
+ throw new ModuleCommandRouterError("unknown-command", `Unknown command '${name}'. Known commands: ${known || "none"}`);
36
+ }
37
+ if (!this.registry.isModuleEnabled(indexed.descriptor.moduleId)) {
38
+ throw new ModuleCommandRouterError("disabled-module", `Module '${indexed.descriptor.moduleId}' is disabled for command '${commandName}'`);
39
+ }
40
+ if (!indexed.module.onCommand) {
41
+ throw new ModuleCommandRouterError("command-not-implemented", `Module '${indexed.descriptor.moduleId}' does not implement onCommand for '${commandName}'`);
42
+ }
43
+ const command = {
44
+ name: commandName,
45
+ args
46
+ };
47
+ return indexed.module.onCommand(command, ctx);
48
+ }
49
+ indexEnabledModuleCommands() {
50
+ for (const module of this.registry.getEnabledModules()) {
51
+ for (const entry of module.registration.instructions.entries) {
52
+ if (this.commands.has(entry.name)) {
53
+ const existing = this.commands.get(entry.name);
54
+ throw new ModuleCommandRouterError("duplicate-command", `Command '${entry.name}' is declared by both '${existing?.descriptor.moduleId}' and '${module.registration.id}'`);
55
+ }
56
+ this.commands.set(entry.name, {
57
+ descriptor: {
58
+ name: entry.name,
59
+ moduleId: module.registration.id,
60
+ instructionFile: `${module.registration.instructions.directory}/${entry.file}`,
61
+ description: entry.description
62
+ },
63
+ module
64
+ });
65
+ }
66
+ }
67
+ }
68
+ validateAliases() {
69
+ for (const [alias, commandName] of Object.entries(this.aliases)) {
70
+ if (alias === commandName) {
71
+ throw new ModuleCommandRouterError("invalid-alias", `Alias '${alias}' cannot map to itself`);
72
+ }
73
+ if (!this.commands.has(commandName)) {
74
+ throw new ModuleCommandRouterError("unknown-alias-target", `Alias '${alias}' maps to unknown command '${commandName}'`);
75
+ }
76
+ if (this.commands.has(alias)) {
77
+ throw new ModuleCommandRouterError("alias-conflict", `Alias '${alias}' conflicts with a declared command`);
78
+ }
79
+ }
80
+ }
81
+ resolveCommandName(name) {
82
+ return this.aliases[name] ?? name;
83
+ }
84
+ }
@@ -0,0 +1,24 @@
1
+ import type { WorkflowModule } from "../contracts/module-contract.js";
2
+ export declare class ModuleRegistryError extends Error {
3
+ readonly code: string;
4
+ constructor(code: string, message: string);
5
+ }
6
+ export declare function validateModuleSet(modules: WorkflowModule[]): void;
7
+ export type ModuleRegistryOptions = {
8
+ enabledModules?: string[];
9
+ disabledModules?: string[];
10
+ workspacePath?: string;
11
+ };
12
+ export declare class ModuleRegistry {
13
+ private readonly modules;
14
+ private readonly enabledModules;
15
+ private readonly sortedModules;
16
+ private readonly moduleMap;
17
+ private readonly enabledModuleMap;
18
+ constructor(modules: WorkflowModule[], options?: ModuleRegistryOptions);
19
+ getAllModules(): WorkflowModule[];
20
+ getModuleById(id: string): WorkflowModule | undefined;
21
+ isModuleEnabled(id: string): boolean;
22
+ getEnabledModules(): WorkflowModule[];
23
+ getStartupOrder(): WorkflowModule[];
24
+ }
@@ -0,0 +1,183 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ export class ModuleRegistryError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.name = "ModuleRegistryError";
8
+ this.code = code;
9
+ }
10
+ }
11
+ function buildModuleMap(modules) {
12
+ const moduleMap = new Map();
13
+ for (const module of modules) {
14
+ const id = module.registration.id;
15
+ if (moduleMap.has(id)) {
16
+ throw new ModuleRegistryError("duplicate-module-id", `Duplicate module id: '${id}'`);
17
+ }
18
+ moduleMap.set(id, module);
19
+ }
20
+ return moduleMap;
21
+ }
22
+ function validateDependencies(moduleMap) {
23
+ for (const module of moduleMap.values()) {
24
+ const moduleId = module.registration.id;
25
+ for (const dependency of module.registration.dependsOn) {
26
+ if (dependency === moduleId) {
27
+ throw new ModuleRegistryError("self-dependency", `Module '${moduleId}' cannot depend on itself`);
28
+ }
29
+ if (!moduleMap.has(dependency)) {
30
+ throw new ModuleRegistryError("missing-dependency", `Module '${moduleId}' depends on missing module '${dependency}'`);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ function topologicalSort(moduleMap) {
36
+ const visited = new Set();
37
+ const inStack = new Set();
38
+ const sorted = [];
39
+ const visit = (id) => {
40
+ if (visited.has(id)) {
41
+ return;
42
+ }
43
+ if (inStack.has(id)) {
44
+ throw new ModuleRegistryError("dependency-cycle", `Dependency cycle detected at module '${id}'`);
45
+ }
46
+ inStack.add(id);
47
+ const module = moduleMap.get(id);
48
+ if (!module) {
49
+ throw new ModuleRegistryError("missing-module", `Module '${id}' not found`);
50
+ }
51
+ for (const dependencyId of module.registration.dependsOn) {
52
+ visit(dependencyId);
53
+ }
54
+ inStack.delete(id);
55
+ visited.add(id);
56
+ sorted.push(module);
57
+ };
58
+ for (const moduleId of moduleMap.keys()) {
59
+ visit(moduleId);
60
+ }
61
+ return sorted;
62
+ }
63
+ function resolveEnabledModuleIds(modules, options) {
64
+ const explicitEnabled = options?.enabledModules;
65
+ const explicitDisabled = new Set(options?.disabledModules ?? []);
66
+ const enabledIds = new Set();
67
+ for (const module of modules) {
68
+ if (module.registration.enabledByDefault) {
69
+ enabledIds.add(module.registration.id);
70
+ }
71
+ }
72
+ if (explicitEnabled && explicitEnabled.length > 0) {
73
+ enabledIds.clear();
74
+ for (const moduleId of explicitEnabled) {
75
+ enabledIds.add(moduleId);
76
+ }
77
+ }
78
+ for (const moduleId of explicitDisabled) {
79
+ enabledIds.delete(moduleId);
80
+ }
81
+ return enabledIds;
82
+ }
83
+ function buildEnabledModuleMap(moduleMap, enabledModuleIds) {
84
+ const enabledModuleMap = new Map();
85
+ for (const moduleId of enabledModuleIds) {
86
+ const module = moduleMap.get(moduleId);
87
+ if (!module) {
88
+ throw new ModuleRegistryError("unknown-enabled-module", `Enabled module '${moduleId}' not found`);
89
+ }
90
+ enabledModuleMap.set(moduleId, module);
91
+ }
92
+ return enabledModuleMap;
93
+ }
94
+ function validateEnabledDependencies(enabledModuleMap) {
95
+ for (const module of enabledModuleMap.values()) {
96
+ for (const dependencyId of module.registration.dependsOn) {
97
+ if (!enabledModuleMap.has(dependencyId)) {
98
+ throw new ModuleRegistryError("disabled-required-dependency", `Enabled module '${module.registration.id}' requires disabled module '${dependencyId}'`);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ function isInstructionNameValid(name) {
104
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
105
+ }
106
+ function validateInstructionContracts(moduleMap, workspacePath) {
107
+ for (const module of moduleMap.values()) {
108
+ const { id, instructions } = module.registration;
109
+ const instructionDirectory = resolve(workspacePath, instructions.directory);
110
+ const seenNames = new Set();
111
+ const seenFiles = new Set();
112
+ for (const entry of instructions.entries) {
113
+ if (!isInstructionNameValid(entry.name)) {
114
+ throw new ModuleRegistryError("invalid-instruction-name", `Module '${id}' has invalid instruction name '${entry.name}'`);
115
+ }
116
+ if (seenNames.has(entry.name)) {
117
+ throw new ModuleRegistryError("duplicate-instruction-name", `Module '${id}' has duplicate instruction name '${entry.name}'`);
118
+ }
119
+ seenNames.add(entry.name);
120
+ if (!entry.file.endsWith(".md")) {
121
+ throw new ModuleRegistryError("invalid-instruction-file", `Module '${id}' instruction file '${entry.file}' must end with .md`);
122
+ }
123
+ const expectedFileName = `${entry.name}.md`;
124
+ if (entry.file !== expectedFileName) {
125
+ throw new ModuleRegistryError("instruction-name-file-mismatch", `Module '${id}' instruction '${entry.name}' must map to '${expectedFileName}'`);
126
+ }
127
+ if (seenFiles.has(entry.file)) {
128
+ throw new ModuleRegistryError("duplicate-instruction-file", `Module '${id}' has duplicate instruction file '${entry.file}'`);
129
+ }
130
+ seenFiles.add(entry.file);
131
+ const instructionFilePath = resolve(instructionDirectory, entry.file);
132
+ if (instructionFilePath !== instructionDirectory &&
133
+ !instructionFilePath.startsWith(`${instructionDirectory}${sep}`)) {
134
+ throw new ModuleRegistryError("instruction-path-escape", `Module '${id}' instruction '${entry.name}' resolves outside instruction directory`);
135
+ }
136
+ if (!existsSync(instructionFilePath)) {
137
+ throw new ModuleRegistryError("missing-instruction-file", `Module '${id}' instruction file '${instructionFilePath}' does not exist`);
138
+ }
139
+ if (!statSync(instructionFilePath).isFile()) {
140
+ throw new ModuleRegistryError("invalid-instruction-file", `Module '${id}' instruction path '${instructionFilePath}' is not a file`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ export function validateModuleSet(modules) {
146
+ const moduleMap = buildModuleMap(modules);
147
+ validateDependencies(moduleMap);
148
+ validateInstructionContracts(moduleMap, process.cwd());
149
+ topologicalSort(moduleMap);
150
+ }
151
+ export class ModuleRegistry {
152
+ modules;
153
+ enabledModules;
154
+ sortedModules;
155
+ moduleMap;
156
+ enabledModuleMap;
157
+ constructor(modules, options) {
158
+ this.moduleMap = buildModuleMap(modules);
159
+ validateDependencies(this.moduleMap);
160
+ validateInstructionContracts(this.moduleMap, options?.workspacePath ?? process.cwd());
161
+ this.modules = [...modules];
162
+ const enabledModuleIds = resolveEnabledModuleIds(this.modules, options);
163
+ this.enabledModuleMap = buildEnabledModuleMap(this.moduleMap, enabledModuleIds);
164
+ validateEnabledDependencies(this.enabledModuleMap);
165
+ this.sortedModules = topologicalSort(this.enabledModuleMap);
166
+ this.enabledModules = [...this.sortedModules];
167
+ }
168
+ getAllModules() {
169
+ return [...this.modules];
170
+ }
171
+ getModuleById(id) {
172
+ return this.moduleMap.get(id);
173
+ }
174
+ isModuleEnabled(id) {
175
+ return this.enabledModuleMap.has(id);
176
+ }
177
+ getEnabledModules() {
178
+ return [...this.enabledModules];
179
+ }
180
+ getStartupOrder() {
181
+ return [...this.sortedModules];
182
+ }
183
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
1
  export { defaultWorkspaceKitPaths, parseJsonFile, runCli, type WorkspaceKitCliOptions } from "./cli.js";
2
+ export * from "./core/index.js";
3
+ export * from "./contracts/index.js";
4
+ export * from "./modules/index.js";
package/dist/index.js CHANGED
@@ -1 +1,4 @@
1
1
  export { defaultWorkspaceKitPaths, parseJsonFile, runCli } from "./cli.js";
2
+ export * from "./core/index.js";
3
+ export * from "./contracts/index.js";
4
+ export * from "./modules/index.js";
@@ -0,0 +1,2 @@
1
+ import type { WorkflowModule } from "../../contracts/module-contract.js";
2
+ export declare const approvalsModule: WorkflowModule;