@workflow-cannon/workspace-kit 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/module-contract.d.ts +62 -0
- package/dist/contracts/module-contract.js +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -0
- package/dist/core/module-command-router.d.ts +27 -0
- package/dist/core/module-command-router.js +84 -0
- package/dist/core/module-registry.d.ts +24 -0
- package/dist/core/module-registry.js +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/modules/approvals/index.d.ts +2 -0
- package/dist/modules/approvals/index.js +30 -0
- package/dist/modules/documentation/index.d.ts +3 -0
- package/dist/modules/documentation/index.js +73 -0
- package/dist/modules/documentation/runtime.d.ts +8 -0
- package/dist/modules/documentation/runtime.js +285 -0
- package/dist/modules/documentation/types.d.ts +32 -0
- package/dist/modules/documentation/types.js +1 -0
- package/dist/modules/improvement/index.d.ts +2 -0
- package/dist/modules/improvement/index.js +30 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/planning/index.d.ts +2 -0
- package/dist/modules/planning/index.js +30 -0
- package/dist/modules/task-engine/index.d.ts +2 -0
- package/dist/modules/task-engine/index.js +30 -0
- package/dist/ops/index.d.ts +1 -0
- package/dist/ops/index.js +1 -0
- 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 {};
|
|
@@ -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,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
package/dist/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const approvalsModule = {
|
|
2
|
+
registration: {
|
|
3
|
+
id: "approvals",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
contractVersion: "1",
|
|
6
|
+
capabilities: ["approvals"],
|
|
7
|
+
dependsOn: ["task-engine"],
|
|
8
|
+
enabledByDefault: true,
|
|
9
|
+
config: {
|
|
10
|
+
path: "src/modules/approvals/config.md",
|
|
11
|
+
format: "md",
|
|
12
|
+
description: "Approvals module policy and queue configuration contract."
|
|
13
|
+
},
|
|
14
|
+
state: {
|
|
15
|
+
path: "src/modules/approvals/state.md",
|
|
16
|
+
format: "md",
|
|
17
|
+
description: "Approvals module decision and queue state contract."
|
|
18
|
+
},
|
|
19
|
+
instructions: {
|
|
20
|
+
directory: "src/modules/approvals/instructions",
|
|
21
|
+
entries: [
|
|
22
|
+
{
|
|
23
|
+
name: "review-item",
|
|
24
|
+
file: "review-item.md",
|
|
25
|
+
description: "Review and record an approval decision."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { WorkflowModule } from "../../contracts/module-contract.js";
|
|
2
|
+
export type { DocumentationConflict, DocumentationGenerateOptions, DocumentationGenerateResult, DocumentationGenerationEvidence, DocumentationValidationIssue } from "./types.js";
|
|
3
|
+
export declare const documentationModule: WorkflowModule;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { generateDocument } from "./runtime.js";
|
|
2
|
+
export const documentationModule = {
|
|
3
|
+
registration: {
|
|
4
|
+
id: "documentation",
|
|
5
|
+
version: "0.1.0",
|
|
6
|
+
contractVersion: "1",
|
|
7
|
+
capabilities: ["documentation"],
|
|
8
|
+
dependsOn: [],
|
|
9
|
+
enabledByDefault: true,
|
|
10
|
+
config: {
|
|
11
|
+
path: "src/modules/documentation/config.md",
|
|
12
|
+
format: "md",
|
|
13
|
+
description: "Documentation module configuration contract."
|
|
14
|
+
},
|
|
15
|
+
state: {
|
|
16
|
+
path: "src/modules/documentation/state.md",
|
|
17
|
+
format: "md",
|
|
18
|
+
description: "Documentation module generation/runtime state contract."
|
|
19
|
+
},
|
|
20
|
+
instructions: {
|
|
21
|
+
directory: "src/modules/documentation/instructions",
|
|
22
|
+
entries: [
|
|
23
|
+
{
|
|
24
|
+
name: "document-project",
|
|
25
|
+
file: "document-project.md",
|
|
26
|
+
description: "Generate aligned project docs for .ai and docs surfaces."
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async onCommand(command, ctx) {
|
|
32
|
+
if (command.name !== "document-project" && command.name !== "generate-document") {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
code: "unsupported-command",
|
|
36
|
+
message: `Documentation module does not support command '${command.name}'`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const args = command.args ?? {};
|
|
40
|
+
const rawOptions = typeof args.options === "object" && args.options !== null
|
|
41
|
+
? args.options
|
|
42
|
+
: undefined;
|
|
43
|
+
const options = rawOptions
|
|
44
|
+
? {
|
|
45
|
+
dryRun: typeof rawOptions.dryRun === "boolean" ? rawOptions.dryRun : undefined,
|
|
46
|
+
overwrite: typeof rawOptions.overwrite === "boolean" ? rawOptions.overwrite : undefined,
|
|
47
|
+
strict: typeof rawOptions.strict === "boolean" ? rawOptions.strict : undefined,
|
|
48
|
+
maxValidationAttempts: typeof rawOptions.maxValidationAttempts === "number"
|
|
49
|
+
? rawOptions.maxValidationAttempts
|
|
50
|
+
: undefined,
|
|
51
|
+
allowWithoutTemplate: typeof rawOptions.allowWithoutTemplate === "boolean"
|
|
52
|
+
? rawOptions.allowWithoutTemplate
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
55
|
+
: undefined;
|
|
56
|
+
const result = await generateDocument({
|
|
57
|
+
documentType: typeof args.documentType === "string" ? args.documentType : undefined,
|
|
58
|
+
options
|
|
59
|
+
}, ctx);
|
|
60
|
+
return {
|
|
61
|
+
ok: result.ok,
|
|
62
|
+
code: result.ok ? "generated-document" : "generation-failed",
|
|
63
|
+
message: result.ok
|
|
64
|
+
? `Generated document '${args.documentType ?? "unknown"}'`
|
|
65
|
+
: `Failed to generate document '${args.documentType ?? "unknown"}'`,
|
|
66
|
+
data: {
|
|
67
|
+
aiOutputPath: result.aiOutputPath,
|
|
68
|
+
humanOutputPath: result.humanOutputPath,
|
|
69
|
+
evidence: result.evidence
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DocumentationGenerateOptions, DocumentationGenerateResult } from "./types.js";
|
|
2
|
+
import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
|
|
3
|
+
type GenerateDocumentArgs = {
|
|
4
|
+
documentType?: string;
|
|
5
|
+
options?: DocumentationGenerateOptions;
|
|
6
|
+
};
|
|
7
|
+
export declare function generateDocument(args: GenerateDocumentArgs, ctx: ModuleLifecycleContext): Promise<DocumentationGenerateResult>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve, sep } from "node:path";
|
|
4
|
+
function isPathWithinRoot(path, root) {
|
|
5
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
6
|
+
}
|
|
7
|
+
function parseDefaultValue(fileContent, key, fallback) {
|
|
8
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
|
+
const regex = new RegExp(`\\\`${escaped}\\\`[^\\n]*default:\\s*\\\`([^\\\`]+)\\\``);
|
|
10
|
+
const match = fileContent.match(regex);
|
|
11
|
+
return match?.[1] ?? fallback;
|
|
12
|
+
}
|
|
13
|
+
async function loadRuntimeConfig(workspacePath) {
|
|
14
|
+
const configPath = resolve(workspacePath, "src/modules/documentation/config.md");
|
|
15
|
+
const configContent = await readFile(configPath, "utf8");
|
|
16
|
+
const aiRoot = parseDefaultValue(configContent, "sources.aiRoot", "/.ai");
|
|
17
|
+
const humanRoot = parseDefaultValue(configContent, "sources.humanRoot", "docs/maintainers");
|
|
18
|
+
const templatesRoot = parseDefaultValue(configContent, "sources.templatesRoot", "src/modules/documentation/templates");
|
|
19
|
+
const instructionsRoot = parseDefaultValue(configContent, "sources.instructionsRoot", "src/modules/documentation/instructions");
|
|
20
|
+
const schemasRoot = parseDefaultValue(configContent, "sources.schemasRoot", "src/modules/documentation/schemas");
|
|
21
|
+
const maxValidationAttemptsRaw = parseDefaultValue(configContent, "generation.maxValidationAttempts", "3");
|
|
22
|
+
const maxValidationAttempts = Number.parseInt(maxValidationAttemptsRaw, 10);
|
|
23
|
+
return {
|
|
24
|
+
aiRoot,
|
|
25
|
+
humanRoot,
|
|
26
|
+
templatesRoot,
|
|
27
|
+
instructionsRoot,
|
|
28
|
+
schemasRoot,
|
|
29
|
+
maxValidationAttempts: Number.isFinite(maxValidationAttempts) ? maxValidationAttempts : 3
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function validateAiSchema(aiOutput) {
|
|
33
|
+
const issues = [];
|
|
34
|
+
const lines = aiOutput.split("\n").filter((line) => line.trim().length > 0);
|
|
35
|
+
if (!lines[0]?.startsWith("meta|v=")) {
|
|
36
|
+
issues.push({
|
|
37
|
+
check: "schema",
|
|
38
|
+
message: "AI output must start with a meta record",
|
|
39
|
+
resolved: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
function autoResolveAiSchema(aiOutput) {
|
|
45
|
+
if (aiOutput.startsWith("meta|v=")) {
|
|
46
|
+
return aiOutput;
|
|
47
|
+
}
|
|
48
|
+
return `meta|v=1|doc=rules|truth=canonical|st=draft\n\n${aiOutput}`;
|
|
49
|
+
}
|
|
50
|
+
function renderTemplate(templateContent) {
|
|
51
|
+
const output = templateContent.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (_match, instructionText) => {
|
|
52
|
+
const normalized = instructionText.trim().split("\n")[0] ?? "template instructions";
|
|
53
|
+
return `Generated content based on instruction: ${normalized}`;
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
output,
|
|
57
|
+
unresolvedBlocks: output.includes("{{{")
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function validateSectionCoverage(templateContent, output) {
|
|
61
|
+
const issues = [];
|
|
62
|
+
const sectionRegex = /^##\s+(.+)$/gm;
|
|
63
|
+
const expectedSections = [...templateContent.matchAll(sectionRegex)].map((match) => match[1]);
|
|
64
|
+
for (const section of expectedSections) {
|
|
65
|
+
if (!output.includes(`## ${section}`)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
check: "section-coverage",
|
|
68
|
+
message: `Missing required section: ${section}`,
|
|
69
|
+
resolved: false
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return issues;
|
|
74
|
+
}
|
|
75
|
+
function detectConflicts(aiOutput, humanOutput) {
|
|
76
|
+
const conflicts = [];
|
|
77
|
+
const combined = `${aiOutput}\n${humanOutput}`;
|
|
78
|
+
if (combined.includes("CONFLICT:")) {
|
|
79
|
+
conflicts.push({
|
|
80
|
+
source: "generated-output",
|
|
81
|
+
reason: "Generated output flagged a conflict marker",
|
|
82
|
+
severity: "stop"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return conflicts;
|
|
86
|
+
}
|
|
87
|
+
export async function generateDocument(args, ctx) {
|
|
88
|
+
const documentType = args.documentType;
|
|
89
|
+
if (!documentType) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
evidence: {
|
|
93
|
+
documentType: "unknown",
|
|
94
|
+
filesRead: [],
|
|
95
|
+
filesWritten: [],
|
|
96
|
+
validationIssues: [
|
|
97
|
+
{
|
|
98
|
+
check: "template-resolution",
|
|
99
|
+
message: "Missing required argument 'documentType'",
|
|
100
|
+
resolved: false
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
conflicts: [],
|
|
104
|
+
attemptsUsed: 0,
|
|
105
|
+
timestamp: new Date().toISOString()
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const options = args.options ?? {};
|
|
110
|
+
const config = await loadRuntimeConfig(ctx.workspacePath);
|
|
111
|
+
const filesRead = [];
|
|
112
|
+
const filesWritten = [];
|
|
113
|
+
const validationIssues = [];
|
|
114
|
+
const conflicts = [];
|
|
115
|
+
const aiRoot = resolve(ctx.workspacePath, config.aiRoot.replace(/^\//, ""));
|
|
116
|
+
const humanRoot = resolve(ctx.workspacePath, config.humanRoot.replace(/^\//, ""));
|
|
117
|
+
const templatePath = resolve(ctx.workspacePath, config.templatesRoot, documentType);
|
|
118
|
+
const aiOutputPath = resolve(aiRoot, documentType);
|
|
119
|
+
const humanOutputPath = resolve(humanRoot, documentType);
|
|
120
|
+
if (!isPathWithinRoot(aiOutputPath, aiRoot) || !isPathWithinRoot(humanOutputPath, humanRoot)) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
evidence: {
|
|
124
|
+
documentType,
|
|
125
|
+
filesRead,
|
|
126
|
+
filesWritten,
|
|
127
|
+
validationIssues: [
|
|
128
|
+
{
|
|
129
|
+
check: "write-boundary",
|
|
130
|
+
message: "Resolved output path escapes configured output roots",
|
|
131
|
+
resolved: false
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
conflicts,
|
|
135
|
+
attemptsUsed: 0,
|
|
136
|
+
timestamp: new Date().toISOString()
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
let templateContent = "";
|
|
141
|
+
let templateFound = existsSync(templatePath);
|
|
142
|
+
if (templateFound) {
|
|
143
|
+
templateContent = await readFile(templatePath, "utf8");
|
|
144
|
+
filesRead.push(templatePath);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
validationIssues.push({
|
|
148
|
+
check: "template-resolution",
|
|
149
|
+
message: `Template not found for '${documentType}'`,
|
|
150
|
+
resolved: Boolean(options.allowWithoutTemplate)
|
|
151
|
+
});
|
|
152
|
+
if (!options.allowWithoutTemplate) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
evidence: {
|
|
156
|
+
documentType,
|
|
157
|
+
filesRead,
|
|
158
|
+
filesWritten,
|
|
159
|
+
validationIssues,
|
|
160
|
+
conflicts,
|
|
161
|
+
attemptsUsed: 0,
|
|
162
|
+
timestamp: new Date().toISOString()
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const schemaPath = resolve(ctx.workspacePath, config.schemasRoot, "documentation-schema.md");
|
|
168
|
+
if (existsSync(schemaPath)) {
|
|
169
|
+
filesRead.push(schemaPath);
|
|
170
|
+
await readFile(schemaPath, "utf8");
|
|
171
|
+
}
|
|
172
|
+
let aiOutput = `meta|v=1|doc=rules|truth=canonical|st=draft\nproject|name=workflow-cannon|type=generated_doc|scope=${documentType}`;
|
|
173
|
+
let attemptsUsed = 0;
|
|
174
|
+
const maxAttempts = options.maxValidationAttempts ?? config.maxValidationAttempts;
|
|
175
|
+
while (attemptsUsed < maxAttempts) {
|
|
176
|
+
attemptsUsed += 1;
|
|
177
|
+
const schemaIssues = validateAiSchema(aiOutput);
|
|
178
|
+
if (schemaIssues.length === 0) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
validationIssues.push(...schemaIssues);
|
|
182
|
+
aiOutput = autoResolveAiSchema(aiOutput);
|
|
183
|
+
}
|
|
184
|
+
const aiFinalIssues = validateAiSchema(aiOutput);
|
|
185
|
+
if (aiFinalIssues.length > 0) {
|
|
186
|
+
validationIssues.push(...aiFinalIssues);
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
evidence: {
|
|
190
|
+
documentType,
|
|
191
|
+
filesRead,
|
|
192
|
+
filesWritten,
|
|
193
|
+
validationIssues,
|
|
194
|
+
conflicts,
|
|
195
|
+
attemptsUsed,
|
|
196
|
+
timestamp: new Date().toISOString()
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
let humanOutput = `# ${documentType}\n\nGenerated without template.`;
|
|
201
|
+
if (templateFound) {
|
|
202
|
+
const rendered = renderTemplate(templateContent);
|
|
203
|
+
humanOutput = rendered.output;
|
|
204
|
+
if (rendered.unresolvedBlocks) {
|
|
205
|
+
validationIssues.push({
|
|
206
|
+
check: "section-coverage",
|
|
207
|
+
message: "Template output still contains unresolved {{{ }}} blocks",
|
|
208
|
+
resolved: false
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
validationIssues.push(...validateSectionCoverage(templateContent, humanOutput));
|
|
212
|
+
}
|
|
213
|
+
conflicts.push(...detectConflicts(aiOutput, humanOutput));
|
|
214
|
+
if (conflicts.some((conflict) => conflict.severity === "stop")) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
evidence: {
|
|
218
|
+
documentType,
|
|
219
|
+
filesRead,
|
|
220
|
+
filesWritten,
|
|
221
|
+
validationIssues,
|
|
222
|
+
conflicts,
|
|
223
|
+
attemptsUsed,
|
|
224
|
+
timestamp: new Date().toISOString()
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const hasUnresolvedValidation = validationIssues.some((issue) => !issue.resolved);
|
|
229
|
+
if (options.strict !== false && hasUnresolvedValidation) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
evidence: {
|
|
233
|
+
documentType,
|
|
234
|
+
filesRead,
|
|
235
|
+
filesWritten,
|
|
236
|
+
validationIssues,
|
|
237
|
+
conflicts,
|
|
238
|
+
attemptsUsed,
|
|
239
|
+
timestamp: new Date().toISOString()
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!options.dryRun) {
|
|
244
|
+
if (!options.overwrite && (existsSync(aiOutputPath) || existsSync(humanOutputPath))) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
evidence: {
|
|
248
|
+
documentType,
|
|
249
|
+
filesRead,
|
|
250
|
+
filesWritten,
|
|
251
|
+
validationIssues: [
|
|
252
|
+
...validationIssues,
|
|
253
|
+
{
|
|
254
|
+
check: "write-boundary",
|
|
255
|
+
message: "Output exists and overwrite=false",
|
|
256
|
+
resolved: false
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
conflicts,
|
|
260
|
+
attemptsUsed,
|
|
261
|
+
timestamp: new Date().toISOString()
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
await mkdir(aiRoot, { recursive: true });
|
|
266
|
+
await mkdir(humanRoot, { recursive: true });
|
|
267
|
+
await writeFile(aiOutputPath, `${aiOutput}\n`, "utf8");
|
|
268
|
+
await writeFile(humanOutputPath, `${humanOutput}\n`, "utf8");
|
|
269
|
+
filesWritten.push(aiOutputPath, humanOutputPath);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
aiOutputPath,
|
|
274
|
+
humanOutputPath,
|
|
275
|
+
evidence: {
|
|
276
|
+
documentType,
|
|
277
|
+
filesRead,
|
|
278
|
+
filesWritten,
|
|
279
|
+
validationIssues,
|
|
280
|
+
conflicts,
|
|
281
|
+
attemptsUsed,
|
|
282
|
+
timestamp: new Date().toISOString()
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DocumentationGenerateOptions = {
|
|
2
|
+
dryRun?: boolean;
|
|
3
|
+
overwrite?: boolean;
|
|
4
|
+
strict?: boolean;
|
|
5
|
+
maxValidationAttempts?: number;
|
|
6
|
+
allowWithoutTemplate?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type DocumentationConflict = {
|
|
9
|
+
source: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
severity: "warn" | "stop";
|
|
12
|
+
};
|
|
13
|
+
export type DocumentationValidationIssue = {
|
|
14
|
+
check: "schema" | "section-coverage" | "template-resolution" | "write-boundary" | "conflict";
|
|
15
|
+
message: string;
|
|
16
|
+
resolved: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type DocumentationGenerationEvidence = {
|
|
19
|
+
documentType: string;
|
|
20
|
+
filesRead: string[];
|
|
21
|
+
filesWritten: string[];
|
|
22
|
+
validationIssues: DocumentationValidationIssue[];
|
|
23
|
+
conflicts: DocumentationConflict[];
|
|
24
|
+
attemptsUsed: number;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
};
|
|
27
|
+
export type DocumentationGenerateResult = {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
aiOutputPath?: string;
|
|
30
|
+
humanOutputPath?: string;
|
|
31
|
+
evidence: DocumentationGenerationEvidence;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const improvementModule = {
|
|
2
|
+
registration: {
|
|
3
|
+
id: "improvement",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
contractVersion: "1",
|
|
6
|
+
capabilities: ["improvement"],
|
|
7
|
+
dependsOn: ["task-engine", "planning"],
|
|
8
|
+
enabledByDefault: true,
|
|
9
|
+
config: {
|
|
10
|
+
path: "src/modules/improvement/config.md",
|
|
11
|
+
format: "md",
|
|
12
|
+
description: "Improvement module configuration contract."
|
|
13
|
+
},
|
|
14
|
+
state: {
|
|
15
|
+
path: "src/modules/improvement/state.md",
|
|
16
|
+
format: "md",
|
|
17
|
+
description: "Improvement module recommendation state contract."
|
|
18
|
+
},
|
|
19
|
+
instructions: {
|
|
20
|
+
directory: "src/modules/improvement/instructions",
|
|
21
|
+
entries: [
|
|
22
|
+
{
|
|
23
|
+
name: "generate-recommendations",
|
|
24
|
+
file: "generate-recommendations.md",
|
|
25
|
+
description: "Produce evidence-backed workflow recommendations."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { approvalsModule } from "./approvals/index.js";
|
|
2
|
+
export { documentationModule } from "./documentation/index.js";
|
|
3
|
+
export type { DocumentationConflict, DocumentationGenerateOptions, DocumentationGenerateResult, DocumentationGenerationEvidence, DocumentationValidationIssue } from "./documentation/types.js";
|
|
4
|
+
export { improvementModule } from "./improvement/index.js";
|
|
5
|
+
export { planningModule } from "./planning/index.js";
|
|
6
|
+
export { taskEngineModule } from "./task-engine/index.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { approvalsModule } from "./approvals/index.js";
|
|
2
|
+
export { documentationModule } from "./documentation/index.js";
|
|
3
|
+
export { improvementModule } from "./improvement/index.js";
|
|
4
|
+
export { planningModule } from "./planning/index.js";
|
|
5
|
+
export { taskEngineModule } from "./task-engine/index.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const planningModule = {
|
|
2
|
+
registration: {
|
|
3
|
+
id: "planning",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
contractVersion: "1",
|
|
6
|
+
capabilities: ["planning"],
|
|
7
|
+
dependsOn: ["task-engine"],
|
|
8
|
+
enabledByDefault: true,
|
|
9
|
+
config: {
|
|
10
|
+
path: "src/modules/planning/config.md",
|
|
11
|
+
format: "md",
|
|
12
|
+
description: "Planning module configuration contract."
|
|
13
|
+
},
|
|
14
|
+
state: {
|
|
15
|
+
path: "src/modules/planning/state.md",
|
|
16
|
+
format: "md",
|
|
17
|
+
description: "Planning module runtime state contract."
|
|
18
|
+
},
|
|
19
|
+
instructions: {
|
|
20
|
+
directory: "src/modules/planning/instructions",
|
|
21
|
+
entries: [
|
|
22
|
+
{
|
|
23
|
+
name: "build-plan",
|
|
24
|
+
file: "build-plan.md",
|
|
25
|
+
description: "Generate a dependency-aware execution plan."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const taskEngineModule = {
|
|
2
|
+
registration: {
|
|
3
|
+
id: "task-engine",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
contractVersion: "1",
|
|
6
|
+
capabilities: ["task-engine"],
|
|
7
|
+
dependsOn: [],
|
|
8
|
+
enabledByDefault: true,
|
|
9
|
+
config: {
|
|
10
|
+
path: "src/modules/task-engine/config.md",
|
|
11
|
+
format: "md",
|
|
12
|
+
description: "Task Engine configuration contract."
|
|
13
|
+
},
|
|
14
|
+
state: {
|
|
15
|
+
path: "src/modules/task-engine/state.md",
|
|
16
|
+
format: "md",
|
|
17
|
+
description: "Task Engine runtime state contract."
|
|
18
|
+
},
|
|
19
|
+
instructions: {
|
|
20
|
+
directory: "src/modules/task-engine/instructions",
|
|
21
|
+
entries: [
|
|
22
|
+
{
|
|
23
|
+
name: "run-transition",
|
|
24
|
+
file: "run-transition.md",
|
|
25
|
+
description: "Run a validated task status transition."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type OpsVersion = "0.1";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
23
23
|
"clean": "rm -rf dist",
|
|
24
24
|
"test": "pnpm run build && node --test test/**/*.test.mjs",
|
|
25
|
-
"pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack"
|
|
25
|
+
"pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack",
|
|
26
|
+
"check-release-metadata": "node scripts/check-release-metadata.mjs",
|
|
27
|
+
"parity": "node scripts/run-parity.mjs"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@types/node": "^25.5.0",
|