@workflow-cannon/workspace-kit 0.18.0 → 0.24.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 (140) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/doctor-planning-issues.js +3 -22
  3. package/dist/cli/run-command.js +22 -38
  4. package/dist/cli.js +95 -4
  5. package/dist/contracts/command-manifest.d.ts +17 -0
  6. package/dist/contracts/command-manifest.js +1 -0
  7. package/dist/contracts/index.d.ts +1 -1
  8. package/dist/contracts/module-contract.d.ts +12 -11
  9. package/dist/core/agent-instruction-surface.d.ts +33 -0
  10. package/dist/core/agent-instruction-surface.js +46 -0
  11. package/dist/core/config-cli.js +13 -17
  12. package/dist/core/config-metadata.js +61 -2
  13. package/dist/core/index.d.ts +4 -1
  14. package/dist/core/index.js +3 -0
  15. package/dist/core/module-command-router.js +19 -1
  16. package/dist/core/module-registry-resolve.d.ts +27 -0
  17. package/dist/core/module-registry-resolve.js +91 -0
  18. package/dist/core/module-registry.d.ts +14 -0
  19. package/dist/core/module-registry.js +57 -0
  20. package/dist/core/planning/build-plan-session-file.d.ts +29 -0
  21. package/dist/core/planning/build-plan-session-file.js +58 -0
  22. package/dist/core/planning/index.d.ts +17 -0
  23. package/dist/core/planning/index.js +15 -0
  24. package/dist/core/policy.js +18 -8
  25. package/dist/core/state/unified-state-db.d.ts +21 -0
  26. package/dist/core/state/unified-state-db.js +80 -0
  27. package/dist/core/workspace-kit-config.js +8 -0
  28. package/dist/modules/agent-behavior/builtins.d.ts +3 -0
  29. package/dist/modules/agent-behavior/builtins.js +71 -0
  30. package/dist/modules/agent-behavior/explain.d.ts +6 -0
  31. package/dist/modules/agent-behavior/explain.js +46 -0
  32. package/dist/modules/agent-behavior/index.d.ts +4 -0
  33. package/dist/modules/agent-behavior/index.js +461 -0
  34. package/dist/modules/agent-behavior/interview-session-file.d.ts +9 -0
  35. package/dist/modules/agent-behavior/interview-session-file.js +43 -0
  36. package/dist/modules/agent-behavior/interview.d.ts +13 -0
  37. package/dist/modules/agent-behavior/interview.js +88 -0
  38. package/dist/modules/agent-behavior/persistence.d.ts +6 -0
  39. package/dist/modules/agent-behavior/persistence.js +89 -0
  40. package/dist/modules/agent-behavior/store.d.ts +34 -0
  41. package/dist/modules/agent-behavior/store.js +119 -0
  42. package/dist/modules/agent-behavior/types.d.ts +28 -0
  43. package/dist/modules/agent-behavior/types.js +1 -0
  44. package/dist/modules/agent-behavior/validate.d.ts +11 -0
  45. package/dist/modules/agent-behavior/validate.js +123 -0
  46. package/dist/modules/approvals/index.js +54 -51
  47. package/dist/modules/approvals/policy-sensitive-commands.d.ts +4 -0
  48. package/dist/modules/approvals/policy-sensitive-commands.js +4 -0
  49. package/dist/modules/approvals/review-runtime.js +1 -2
  50. package/dist/modules/documentation/index.js +47 -45
  51. package/dist/modules/documentation/normalizer.d.ts +3 -0
  52. package/dist/modules/documentation/normalizer.js +171 -0
  53. package/dist/modules/documentation/parser.d.ts +7 -0
  54. package/dist/modules/documentation/parser.js +39 -0
  55. package/dist/modules/documentation/policy-sensitive-commands.d.ts +5 -0
  56. package/dist/modules/documentation/policy-sensitive-commands.js +8 -0
  57. package/dist/modules/documentation/renderer.d.ts +23 -0
  58. package/dist/modules/documentation/renderer.js +105 -0
  59. package/dist/modules/documentation/runtime-batch.d.ts +10 -0
  60. package/dist/modules/documentation/runtime-batch.js +67 -0
  61. package/dist/modules/documentation/runtime-config.d.ts +11 -0
  62. package/dist/modules/documentation/runtime-config.js +54 -0
  63. package/dist/modules/documentation/runtime-render-support.d.ts +8 -0
  64. package/dist/modules/documentation/runtime-render-support.js +36 -0
  65. package/dist/modules/documentation/runtime.js +22 -510
  66. package/dist/modules/documentation/types.d.ts +182 -0
  67. package/dist/modules/documentation/validator.d.ts +8 -0
  68. package/dist/modules/documentation/validator.js +234 -0
  69. package/dist/modules/documentation/view-models.d.ts +3 -0
  70. package/dist/modules/documentation/view-models.js +124 -0
  71. package/dist/modules/improvement/generate-recommendations-runtime.js +3 -3
  72. package/dist/modules/improvement/improvement-state.d.ts +2 -2
  73. package/dist/modules/improvement/improvement-state.js +52 -23
  74. package/dist/modules/improvement/index.js +140 -138
  75. package/dist/modules/improvement/ingest.d.ts +1 -1
  76. package/dist/modules/improvement/policy-sensitive-commands.d.ts +4 -0
  77. package/dist/modules/improvement/policy-sensitive-commands.js +7 -0
  78. package/dist/modules/index.d.ts +6 -0
  79. package/dist/modules/index.js +17 -0
  80. package/dist/modules/planning/index.js +384 -50
  81. package/dist/modules/planning/question-engine.d.ts +2 -0
  82. package/dist/modules/planning/question-engine.js +8 -1
  83. package/dist/modules/task-engine/doctor-planning-persistence.js +21 -13
  84. package/dist/modules/task-engine/index.d.ts +1 -2
  85. package/dist/modules/task-engine/index.js +1 -1143
  86. package/dist/modules/task-engine/migrate-task-persistence-runtime.js +31 -4
  87. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.d.ts +2 -0
  88. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.js +146 -0
  89. package/dist/modules/task-engine/planning-open.d.ts +2 -9
  90. package/dist/modules/task-engine/planning-open.js +4 -15
  91. package/dist/modules/task-engine/policy-sensitive-commands.d.ts +5 -0
  92. package/dist/modules/task-engine/policy-sensitive-commands.js +5 -0
  93. package/dist/modules/task-engine/sqlite-dual-planning.d.ts +11 -2
  94. package/dist/modules/task-engine/sqlite-dual-planning.js +134 -28
  95. package/dist/modules/task-engine/strict-task-validation.js +3 -0
  96. package/dist/modules/task-engine/suggestions.js +2 -1
  97. package/dist/modules/task-engine/task-engine-internal.d.ts +2 -0
  98. package/dist/modules/task-engine/task-engine-internal.js +1304 -0
  99. package/dist/modules/task-engine/task-type-validation.js +40 -0
  100. package/dist/modules/task-engine/wishlist-intake.d.ts +22 -0
  101. package/dist/modules/task-engine/wishlist-intake.js +180 -0
  102. package/dist/modules/task-engine/wishlist-validation.d.ts +4 -0
  103. package/dist/modules/task-engine/wishlist-validation.js +19 -0
  104. package/dist/modules/workspace-config/index.js +9 -11
  105. package/package.json +2 -2
  106. package/schemas/agent-behavior-profile.schema.json +52 -0
  107. package/schemas/task-engine-run-contracts.schema.json +80 -5
  108. package/src/modules/documentation/README.md +16 -25
  109. package/src/modules/documentation/RULES.md +9 -9
  110. package/src/modules/documentation/index.ts +54 -49
  111. package/src/modules/documentation/instructions/document-project.md +6 -6
  112. package/src/modules/documentation/instructions/generate-document.md +4 -4
  113. package/src/modules/documentation/normalizer.ts +187 -0
  114. package/src/modules/documentation/parser.ts +41 -0
  115. package/src/modules/documentation/policy-sensitive-commands.ts +8 -0
  116. package/src/modules/documentation/renderer.ts +121 -0
  117. package/src/modules/documentation/runtime-batch.ts +74 -0
  118. package/src/modules/documentation/runtime-config.ts +68 -0
  119. package/src/modules/documentation/runtime-render-support.ts +39 -0
  120. package/src/modules/documentation/runtime.ts +28 -600
  121. package/src/modules/documentation/schemas/documentation-schema.md +37 -54
  122. package/src/modules/documentation/types.ts +228 -0
  123. package/src/modules/documentation/validator.ts +247 -0
  124. package/src/modules/documentation/view-models.ts +132 -0
  125. package/src/modules/documentation/views/agents.view.yaml +18 -0
  126. package/src/modules/documentation/views/architecture.view.yaml +18 -0
  127. package/src/modules/documentation/views/principles.view.yaml +18 -0
  128. package/src/modules/documentation/views/readme.view.yaml +18 -0
  129. package/src/modules/documentation/views/releasing.view.yaml +18 -0
  130. package/src/modules/documentation/views/roadmap.view.yaml +18 -0
  131. package/src/modules/documentation/views/runbooks-consumer-cadence.view.yaml +18 -0
  132. package/src/modules/documentation/views/runbooks-parity-validation-flow.view.yaml +18 -0
  133. package/src/modules/documentation/views/runbooks-release-channels.view.yaml +18 -0
  134. package/src/modules/documentation/views/security.view.yaml +18 -0
  135. package/src/modules/documentation/views/support.view.yaml +18 -0
  136. package/src/modules/documentation/views/terms.view.yaml +18 -0
  137. package/src/modules/documentation/views/workbooks-phase2-config-policy-workbook.view.yaml +18 -0
  138. package/src/modules/documentation/views/workbooks-task-engine-workbook.view.yaml +18 -0
  139. package/src/modules/documentation/views/workbooks-transcript-automation-baseline.view.yaml +18 -0
  140. package/src/modules/documentation/state.md +0 -8
@@ -1,516 +1,19 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
- import { dirname, resolve, sep } from "node:path";
4
- import { readdir } from "node:fs/promises";
5
- import { fileURLToPath } from "node:url";
6
- import type {
7
- DocumentationBatchResult,
8
- DocumentationConflict,
9
- DocumentationGenerateOptions,
10
- DocumentationGenerateResult,
11
- DocumentationValidationIssue
12
- } from "./types.js";
3
+ import { dirname, resolve } from "node:path";
4
+ import type { DocumentationBatchResult, DocumentationConflict, DocumentationGenerateOptions, DocumentationGenerateResult, DocumentationValidationIssue } from "./types.js";
13
5
  import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
6
+ import { parseAiDocument } from "./parser.js";
7
+ import { normalizeDocument } from "./normalizer.js";
8
+ import { renderDocument } from "./renderer.js";
9
+ import { autoResolveAiSchema, validateAiSchema } from "./validator.js";
10
+ import { isPathWithinRoot, loadRuntimeConfig } from "./runtime-config.js";
11
+ import { detectConflicts, renderTemplate, resolveExpectedDocFamily, validateSectionCoverage } from "./runtime-render-support.js";
12
+ import { runGenerateAllDocuments } from "./runtime-batch.js";
14
13
 
15
- type DocumentationRuntimeConfig = {
16
- aiRoot: string;
17
- humanRoot: string;
18
- templatesRoot: string;
19
- instructionsRoot: string;
20
- schemasRoot: string;
21
- maxValidationAttempts: number;
22
- sourceRoot: string;
23
- };
14
+ type GenerateDocumentArgs = { documentType?: string; options?: DocumentationGenerateOptions };
24
15
 
25
- function isPathWithinRoot(path: string, root: string): boolean {
26
- return path === root || path.startsWith(`${root}${sep}`);
27
- }
28
-
29
- function parseDefaultValue(fileContent: string, key: string, fallback: string): string {
30
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
- const regex = new RegExp(`\\\`${escaped}\\\`[^\\n]*default:\\s*\\\`([^\\\`]+)\\\``);
32
- const match = fileContent.match(regex);
33
- return match?.[1] ?? fallback;
34
- }
35
-
36
- async function loadRuntimeConfig(workspacePath: string): Promise<DocumentationRuntimeConfig> {
37
- const runtimeSourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
38
- const sourceRoots = [workspacePath, runtimeSourceRoot];
39
- let sourceRoot = workspacePath;
40
- let configContent: string | undefined;
41
- for (const candidateRoot of sourceRoots) {
42
- const candidate = resolve(candidateRoot, "src/modules/documentation/config.md");
43
- if (!existsSync(candidate)) {
44
- continue;
45
- }
46
- configContent = await readFile(candidate, "utf8");
47
- sourceRoot = candidateRoot;
48
- break;
49
- }
50
-
51
- if (!configContent) {
52
- return {
53
- aiRoot: "/.ai",
54
- humanRoot: "docs/maintainers",
55
- templatesRoot: "src/modules/documentation/templates",
56
- instructionsRoot: "src/modules/documentation/instructions",
57
- schemasRoot: "src/modules/documentation/schemas",
58
- maxValidationAttempts: 3,
59
- sourceRoot
60
- };
61
- }
62
-
63
- const aiRoot = parseDefaultValue(configContent, "sources.aiRoot", "/.ai");
64
- const humanRoot = parseDefaultValue(configContent, "sources.humanRoot", "docs/maintainers");
65
- const templatesRoot = parseDefaultValue(
66
- configContent,
67
- "sources.templatesRoot",
68
- "src/modules/documentation/templates"
69
- );
70
- const instructionsRoot = parseDefaultValue(
71
- configContent,
72
- "sources.instructionsRoot",
73
- "src/modules/documentation/instructions"
74
- );
75
- const schemasRoot = parseDefaultValue(
76
- configContent,
77
- "sources.schemasRoot",
78
- "src/modules/documentation/schemas"
79
- );
80
- const maxValidationAttemptsRaw = parseDefaultValue(configContent, "generation.maxValidationAttempts", "3");
81
- const maxValidationAttempts = Number.parseInt(maxValidationAttemptsRaw, 10);
82
-
83
- return {
84
- aiRoot,
85
- humanRoot,
86
- templatesRoot,
87
- instructionsRoot,
88
- schemasRoot,
89
- maxValidationAttempts: Number.isFinite(maxValidationAttempts) ? maxValidationAttempts : 3,
90
- sourceRoot
91
- };
92
- }
93
-
94
- type AiValidationContext = {
95
- strict: boolean;
96
- workspacePath: string;
97
- expectedDoc?: "rules" | "runbook" | "workbook";
98
- };
99
-
100
- type AiRecord = {
101
- type: string;
102
- positional: string[];
103
- kv: Record<string, string>;
104
- raw: string;
105
- };
106
-
107
- function parseAiRecordLine(line: string): AiRecord | null {
108
- const trimmed = line.trim();
109
- if (!trimmed || trimmed.startsWith("#")) return null;
110
- const parts = trimmed.split("|");
111
- // Record format is `type|token|token...`. Ignore non-record markdown lines.
112
- if (parts.length < 2) return null;
113
- const type = parts[0] ?? "";
114
- if (!type) return null;
115
- const positional: string[] = [];
116
- const kv: Record<string, string> = {};
117
- for (const token of parts.slice(1)) {
118
- if (!token) continue;
119
- const idx = token.indexOf("=");
120
- if (idx >= 0) {
121
- const k = token.slice(0, idx).trim();
122
- const v = token.slice(idx + 1).trim();
123
- if (!k) continue;
124
- kv[k] = v;
125
- } else {
126
- positional.push(token);
127
- }
128
- }
129
- return { type, positional, kv, raw: line };
130
- }
131
-
132
- function isAllowedMetaDoc(doc: string): boolean {
133
- return (
134
- doc === "rules" ||
135
- doc === "runbook" ||
136
- doc === "workbook" ||
137
- doc === "generator" ||
138
- doc === "map" ||
139
- doc === "workflows" ||
140
- doc === "commands" ||
141
- doc === "decisions" ||
142
- doc === "glossary" ||
143
- doc === "observed" ||
144
- doc === "planned" ||
145
- doc === "checks" ||
146
- doc === "manifest"
147
- );
148
- }
149
-
150
- function validateAiSchema(aiOutput: string, ctx: AiValidationContext): DocumentationValidationIssue[] {
151
- const issues: DocumentationValidationIssue[] = [];
152
- const lines = aiOutput.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
153
- if (lines.length === 0) {
154
- return [
155
- {
156
- check: "schema",
157
- message: "AI output is empty",
158
- resolved: false,
159
- }
160
- ];
161
- }
162
-
163
- const metaLine = lines[0];
164
- const meta = parseAiRecordLine(metaLine);
165
- if (!meta || meta.type !== "meta") {
166
- return [
167
- {
168
- check: "schema",
169
- message: "AI output must start with a meta record",
170
- resolved: false,
171
- }
172
- ];
173
- }
174
-
175
- const v = meta.kv["v"];
176
- const doc = meta.kv["doc"];
177
- const truth = meta.kv["truth"];
178
- const st = meta.kv["st"];
179
-
180
- if (v !== "1") {
181
- issues.push({
182
- check: "schema",
183
- message: "AI meta schemaVersion must be v=1",
184
- resolved: false,
185
- });
186
- }
187
-
188
- if (!doc || !isAllowedMetaDoc(doc)) {
189
- issues.push({
190
- check: "schema",
191
- message: `Unsupported meta.doc '${doc ?? ""}'`,
192
- resolved: false,
193
- });
194
- }
195
-
196
- if (!truth || truth.length === 0) {
197
- issues.push({
198
- check: "schema",
199
- message: "AI meta.truth is required",
200
- resolved: false,
201
- });
202
- }
203
-
204
- if (!st || st.length === 0) {
205
- issues.push({
206
- check: "schema",
207
- message: "AI meta.st is required",
208
- resolved: false,
209
- });
210
- }
211
-
212
- if (ctx.expectedDoc && doc && ctx.expectedDoc !== doc) {
213
- issues.push({
214
- check: "schema",
215
- message: `meta.doc '${doc}' does not match expected doc family for '${ctx.expectedDoc}'`,
216
- resolved: !ctx.strict,
217
- });
218
- }
219
-
220
- const requireActiveRecords = st === "active";
221
-
222
- const allowedTypes = new Set([
223
- // Global ai record families used across .ai/*.
224
- "project",
225
- "stack",
226
- "prio",
227
- "ref",
228
- "rule",
229
- "check",
230
- "path",
231
- "role",
232
- "has",
233
- "xhas",
234
- "deps",
235
- "xdeps",
236
- "module",
237
- "wf",
238
- "cmd",
239
- "decision",
240
- "term",
241
- "observed",
242
- "planned",
243
- "map",
244
- // Runbooks
245
- "runbook",
246
- "intent",
247
- "chain",
248
- "artifact",
249
- "state",
250
- "transition",
251
- "promotion",
252
- "rollback",
253
- // Workbooks
254
- "workbook",
255
- "scope",
256
- "command",
257
- "config",
258
- "cadence",
259
- "guardrail",
260
- ]);
261
-
262
- const presentByType: Record<string, boolean> = {};
263
- const missingRequired: string[] = [];
264
-
265
- for (const line of lines.slice(1)) {
266
- const rec = parseAiRecordLine(line);
267
- if (!rec) continue;
268
- presentByType[rec.type] = true;
269
-
270
- if (!allowedTypes.has(rec.type)) {
271
- issues.push({
272
- check: "schema",
273
- message: `Unknown AI record type '${rec.type}'`,
274
- resolved: !ctx.strict,
275
- });
276
- continue;
277
- }
278
-
279
- // Minimal record-level validation for current runbook/workbook families.
280
- if (rec.type === "ref") {
281
- const p = rec.kv["path"];
282
- const n = rec.kv["name"];
283
- if (!p || !n) {
284
- issues.push({
285
- check: "schema",
286
- message: "ref records require both 'name' and 'path'",
287
- resolved: !ctx.strict,
288
- });
289
- } else {
290
- const abs = resolve(ctx.workspacePath, p);
291
- const ok = existsSync(abs);
292
- if (!ok) {
293
- issues.push({
294
- check: "schema",
295
- message: `ref.path does not exist: '${p}'`,
296
- resolved: !ctx.strict,
297
- });
298
- }
299
- }
300
- continue;
301
- }
302
-
303
- if (rec.type === "rule") {
304
- const rid = rec.positional[0];
305
- const lvl = rec.positional[1] ?? rec.kv["lvl"];
306
- const directive = (() => {
307
- // rule lines can be either:
308
- // rule|RID|lvl|scope|directive|...
309
- // or the scope can be omitted:
310
- // rule|RID|lvl|directive|...
311
- const nonKey = rec.positional.slice(2);
312
- return nonKey[nonKey.length - 1];
313
- })();
314
-
315
- if (!rid || !/^R\d{3,}$/.test(rid)) {
316
- issues.push({
317
- check: "schema",
318
- message: "rule records require RID formatted like R### or R####",
319
- resolved: !ctx.strict,
320
- });
321
- }
322
- if (!lvl || !["must", "must_not", "should", "may"].includes(lvl)) {
323
- issues.push({
324
- check: "schema",
325
- message: `rule lvl is invalid: '${lvl ?? ""}'`,
326
- resolved: !ctx.strict,
327
- });
328
- }
329
- if (!directive || directive.length < 2) {
330
- issues.push({
331
- check: "schema",
332
- message: "rule directive cannot be empty",
333
- resolved: !ctx.strict,
334
- });
335
- }
336
- continue;
337
- }
338
-
339
- if (rec.type === "runbook") {
340
- if (!rec.kv["name"] || !rec.kv["scope"]) {
341
- issues.push({
342
- check: "schema",
343
- message: "runbook records require at least name and scope",
344
- resolved: !ctx.strict,
345
- });
346
- }
347
- continue;
348
- }
349
-
350
- if (rec.type === "workbook") {
351
- if (!rec.kv["name"]) {
352
- issues.push({
353
- check: "schema",
354
- message: "workbook records require 'name'",
355
- resolved: !ctx.strict,
356
- });
357
- }
358
- continue;
359
- }
360
-
361
- if (rec.type === "chain") {
362
- const step = rec.kv["step"];
363
- const command = rec.kv["command"];
364
- const expect = rec.kv["expect_exit"];
365
- if (!step || !command || expect === undefined) {
366
- issues.push({
367
- check: "schema",
368
- message: "chain records require step, command, and expect_exit",
369
- resolved: !ctx.strict,
370
- });
371
- }
372
- continue;
373
- }
374
-
375
- if (rec.type === "transition") {
376
- if (!rec.kv["from"] || !rec.kv["to"] || !rec.kv["requires"]) {
377
- issues.push({
378
- check: "schema",
379
- message: "transition records require from, to, requires",
380
- resolved: !ctx.strict,
381
- });
382
- }
383
- continue;
384
- }
385
-
386
- if (rec.type === "state") {
387
- if (!rec.kv["name"]) {
388
- issues.push({
389
- check: "schema",
390
- message: "state records require name",
391
- resolved: !ctx.strict,
392
- });
393
- }
394
- continue;
395
- }
396
-
397
- if (rec.type === "artifact") {
398
- if (!rec.kv["path"] || !rec.kv["schema"]) {
399
- issues.push({
400
- check: "schema",
401
- message: "artifact records require path and schema",
402
- resolved: !ctx.strict,
403
- });
404
- }
405
- continue;
406
- }
407
-
408
- if (rec.type === "command") {
409
- if (!rec.kv["name"]) {
410
- issues.push({
411
- check: "schema",
412
- message: "command records require name",
413
- resolved: !ctx.strict,
414
- });
415
- }
416
- continue;
417
- }
418
-
419
- if (rec.type === "config") {
420
- if (!rec.kv["key"]) {
421
- issues.push({
422
- check: "schema",
423
- message: "config records require key",
424
- resolved: !ctx.strict,
425
- });
426
- }
427
- continue;
428
- }
429
- }
430
-
431
- // Per-doc required record sets.
432
- if (requireActiveRecords) {
433
- if (doc === "runbook") {
434
- if (!presentByType["runbook"]) missingRequired.push("runbook| record");
435
- if (!presentByType["rule"] && !presentByType["chain"]) missingRequired.push("at least one rule| or chain| record");
436
- }
437
- if (doc === "workbook") {
438
- if (!presentByType["workbook"]) missingRequired.push("workbook| record");
439
- if (!presentByType["command"]) missingRequired.push("at least one command| record");
440
- if (!presentByType["config"]) missingRequired.push("at least one config| record");
441
- }
442
- if (doc === "rules") {
443
- if (!presentByType["rule"] && !presentByType["check"]) missingRequired.push("at least one rule| or check| record");
444
- }
445
- }
446
-
447
- if (missingRequired.length > 0) {
448
- issues.push({
449
- check: "schema",
450
- message: `Missing required AI records for doc family '${doc}': ${missingRequired.join(", ")}`,
451
- resolved: !ctx.strict,
452
- });
453
- }
454
-
455
- return issues;
456
- }
457
-
458
- function autoResolveAiSchema(aiOutput: string): string {
459
- if (aiOutput.startsWith("meta|v=")) {
460
- return aiOutput;
461
- }
462
- return `meta|v=1|doc=rules|truth=canonical|st=draft\n\n${aiOutput}`;
463
- }
464
-
465
- function renderTemplate(templateContent: string): { output: string; unresolvedBlocks: boolean } {
466
- const output = templateContent.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (_match, instructionText: string) => {
467
- const normalized = instructionText.trim().split("\n")[0] ?? "template instructions";
468
- return `Generated content based on instruction: ${normalized}`;
469
- });
470
- return {
471
- output,
472
- unresolvedBlocks: output.includes("{{{")
473
- };
474
- }
475
-
476
- function validateSectionCoverage(templateContent: string, output: string): DocumentationValidationIssue[] {
477
- const issues: DocumentationValidationIssue[] = [];
478
- const sectionRegex = /^##\s+(.+)$/gm;
479
- const expectedSections = [...templateContent.matchAll(sectionRegex)].map((match) => match[1]);
480
- for (const section of expectedSections) {
481
- if (!output.includes(`## ${section}`)) {
482
- issues.push({
483
- check: "section-coverage",
484
- message: `Missing required section: ${section}`,
485
- resolved: false
486
- });
487
- }
488
- }
489
- return issues;
490
- }
491
-
492
- function detectConflicts(aiOutput: string, humanOutput: string): DocumentationConflict[] {
493
- const conflicts: DocumentationConflict[] = [];
494
- const combined = `${aiOutput}\n${humanOutput}`;
495
- if (combined.includes("CONFLICT:")) {
496
- conflicts.push({
497
- source: "generated-output",
498
- reason: "Generated output flagged a conflict marker",
499
- severity: "stop"
500
- });
501
- }
502
- return conflicts;
503
- }
504
-
505
- type GenerateDocumentArgs = {
506
- documentType?: string;
507
- options?: DocumentationGenerateOptions;
508
- };
509
-
510
- export async function generateDocument(
511
- args: GenerateDocumentArgs,
512
- ctx: ModuleLifecycleContext
513
- ): Promise<DocumentationGenerateResult> {
16
+ export async function generateDocument(args: GenerateDocumentArgs, ctx: ModuleLifecycleContext): Promise<DocumentationGenerateResult> {
514
17
  const documentType = args.documentType;
515
18
  if (!documentType) {
516
19
  return {
@@ -606,12 +109,6 @@ export async function generateDocument(
606
109
  await readFile(schemaPath, "utf8");
607
110
  }
608
111
 
609
- function resolveExpectedDocFamily(docType: string): "rules" | "runbook" | "workbook" {
610
- if (docType.includes("runbooks/") || docType.startsWith("runbooks/")) return "runbook";
611
- if (docType.includes("workbooks/") || docType.startsWith("workbooks/")) return "workbook";
612
- return "rules";
613
- }
614
-
615
112
  const expectedDoc = resolveExpectedDocFamily(documentType);
616
113
 
617
114
  // Default AI output for draft generation. When AI files already exist and overwriteAi is false,
@@ -669,6 +166,20 @@ export async function generateDocument(
669
166
  };
670
167
  }
671
168
 
169
+ // Build normalized model now to keep parser/validator/normalizer wiring exercised.
170
+ const normalized = normalizeDocument(parseAiDocument(aiOutput));
171
+ void renderDocument(normalized, {
172
+ id: "runtime-preview",
173
+ version: 1,
174
+ docType: expectedDoc,
175
+ target: documentType,
176
+ profile: expectedDoc === "runbook" ? "runbook" : expectedDoc === "workbook" ? "workbook" : "core",
177
+ sections: [
178
+ { id: "meta", source: "meta", renderer: "renderMetaSection" },
179
+ { id: "rules", source: "rules", renderer: "renderRuleSection" }
180
+ ]
181
+ });
182
+
672
183
  let humanOutput = `# ${documentType}\n\nGenerated without template.`;
673
184
  if (templateFound) {
674
185
  const rendered = renderTemplate(templateContent);
@@ -780,91 +291,8 @@ export async function generateDocument(
780
291
  };
781
292
  }
782
293
 
783
- type GenerateAllDocumentsArgs = {
784
- options?: DocumentationGenerateOptions;
785
- };
294
+ type GenerateAllDocumentsArgs = { options?: DocumentationGenerateOptions };
786
295
 
787
- export async function generateAllDocuments(
788
- args: GenerateAllDocumentsArgs,
789
- ctx: ModuleLifecycleContext
790
- ): Promise<DocumentationBatchResult> {
791
- const config = await loadRuntimeConfig(ctx.workspacePath);
792
- const templatesDir = resolve(config.sourceRoot, config.templatesRoot);
793
-
794
- async function listTemplateFiles(dir: string, baseDir: string): Promise<string[]> {
795
- const entries = await readdir(dir, { withFileTypes: true });
796
- const files: string[] = [];
797
- for (const entry of entries) {
798
- const absPath = resolve(dir, entry.name);
799
- if (entry.isDirectory()) {
800
- files.push(...(await listTemplateFiles(absPath, baseDir)));
801
- continue;
802
- }
803
- if (!entry.isFile() || !entry.name.endsWith(".md")) {
804
- continue;
805
- }
806
- const relPath = absPath.slice(baseDir.length + 1).split("\\").join("/");
807
- files.push(relPath);
808
- }
809
- return files;
810
- }
811
-
812
- let templateFiles: string[] = [];
813
- try {
814
- templateFiles = (await listTemplateFiles(templatesDir, templatesDir)).sort();
815
- } catch {
816
- return {
817
- ok: false,
818
- results: [],
819
- summary: {
820
- total: 0,
821
- succeeded: 0,
822
- failed: 1,
823
- skipped: 0,
824
- timestamp: new Date().toISOString()
825
- }
826
- };
827
- }
828
-
829
- const results: DocumentationGenerateResult[] = [];
830
- let succeeded = 0;
831
- let failed = 0;
832
- let skipped = 0;
833
-
834
- const batchOptions: DocumentationGenerateOptions = {
835
- ...args.options,
836
- overwriteAi: args.options?.overwriteAi ?? false,
837
- overwriteHuman: args.options?.overwriteHuman ?? true,
838
- strict: args.options?.strict ?? false,
839
- };
840
-
841
- for (const templateFile of templateFiles) {
842
- const result = await generateDocument(
843
- { documentType: templateFile, options: batchOptions },
844
- ctx
845
- );
846
- results.push(result);
847
-
848
- if (result.ok) {
849
- if (result.evidence.filesWritten.length > 0) {
850
- succeeded++;
851
- } else {
852
- skipped++;
853
- }
854
- } else {
855
- failed++;
856
- }
857
- }
858
-
859
- return {
860
- ok: failed === 0,
861
- results,
862
- summary: {
863
- total: templateFiles.length,
864
- succeeded,
865
- failed,
866
- skipped,
867
- timestamp: new Date().toISOString()
868
- }
869
- };
296
+ export async function generateAllDocuments(args: GenerateAllDocumentsArgs, ctx: ModuleLifecycleContext): Promise<DocumentationBatchResult> {
297
+ return runGenerateAllDocuments(args, ctx, generateDocument);
870
298
  }