@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
@@ -27,21 +27,18 @@ function parseOptions(raw: Record<string, unknown>): DocumentationGenerateOption
27
27
  export const documentationModule: WorkflowModule = {
28
28
  registration: {
29
29
  id: "documentation",
30
- version: "0.2.0",
30
+ version: "0.3.0",
31
31
  contractVersion: "1",
32
+ stateSchema: 1,
32
33
  capabilities: ["documentation"],
33
34
  dependsOn: [],
35
+ optionalPeers: [],
34
36
  enabledByDefault: true,
35
37
  config: {
36
38
  path: "src/modules/documentation/config.md",
37
39
  format: "md",
38
40
  description: "Documentation module configuration contract."
39
41
  },
40
- state: {
41
- path: "src/modules/documentation/state.md",
42
- format: "md",
43
- description: "Documentation module generation/runtime state contract."
44
- },
45
42
  instructions: {
46
43
  directory: "src/modules/documentation/instructions",
47
44
  entries: [
@@ -66,50 +63,58 @@ export const documentationModule: WorkflowModule = {
66
63
  : {};
67
64
  const options = parseOptions(rawOptions);
68
65
 
69
- if (command.name === "document-project") {
70
- const batchResult = await generateAllDocuments({ options }, ctx);
71
- return {
72
- ok: batchResult.ok,
73
- code: batchResult.ok ? "documented-project" : "documentation-batch-failed",
74
- message: batchResult.ok
75
- ? `Generated ${batchResult.summary.succeeded} documents (${batchResult.summary.skipped} skipped)`
76
- : `Batch failed: ${batchResult.summary.failed} of ${batchResult.summary.total} documents failed`,
77
- data: {
78
- summary: batchResult.summary,
79
- results: batchResult.results.map((r) => ({
80
- documentType: r.evidence.documentType,
81
- ok: r.ok,
82
- aiOutputPath: r.aiOutputPath,
83
- humanOutputPath: r.humanOutputPath,
84
- filesWritten: r.evidence.filesWritten,
85
- filesSkipped: r.evidence.filesSkipped,
86
- }))
87
- }
88
- };
89
- }
90
-
91
- if (command.name === "generate-document") {
92
- const result = await generateDocument(
93
- {
94
- documentType: typeof args.documentType === "string" ? args.documentType : undefined,
95
- options
96
- },
97
- ctx
98
- );
66
+ const handlers: Record<string, () => Promise<{
67
+ ok: boolean;
68
+ code: string;
69
+ message?: string;
70
+ data?: Record<string, unknown>;
71
+ }>> = {
72
+ "document-project": async () => {
73
+ const batchResult = await generateAllDocuments({ options }, ctx);
74
+ return {
75
+ ok: batchResult.ok,
76
+ code: batchResult.ok ? "documented-project" : "documentation-batch-failed",
77
+ message: batchResult.ok
78
+ ? `Generated ${batchResult.summary.succeeded} documents (${batchResult.summary.skipped} skipped)`
79
+ : `Batch failed: ${batchResult.summary.failed} of ${batchResult.summary.total} documents failed`,
80
+ data: {
81
+ summary: batchResult.summary,
82
+ results: batchResult.results.map((r) => ({
83
+ documentType: r.evidence.documentType,
84
+ ok: r.ok,
85
+ aiOutputPath: r.aiOutputPath,
86
+ humanOutputPath: r.humanOutputPath,
87
+ filesWritten: r.evidence.filesWritten,
88
+ filesSkipped: r.evidence.filesSkipped
89
+ }))
90
+ }
91
+ };
92
+ },
93
+ "generate-document": async () => {
94
+ const result = await generateDocument(
95
+ {
96
+ documentType: typeof args.documentType === "string" ? args.documentType : undefined,
97
+ options
98
+ },
99
+ ctx
100
+ );
99
101
 
100
- return {
101
- ok: result.ok,
102
- code: result.ok ? "generated-document" : "generation-failed",
103
- message: result.ok
104
- ? `Generated document '${args.documentType ?? "unknown"}'`
105
- : `Failed to generate document '${args.documentType ?? "unknown"}'`,
106
- data: {
107
- aiOutputPath: result.aiOutputPath,
108
- humanOutputPath: result.humanOutputPath,
109
- evidence: result.evidence
110
- }
111
- };
112
- }
102
+ return {
103
+ ok: result.ok,
104
+ code: result.ok ? "generated-document" : "generation-failed",
105
+ message: result.ok
106
+ ? `Generated document '${args.documentType ?? "unknown"}'`
107
+ : `Failed to generate document '${args.documentType ?? "unknown"}'`,
108
+ data: {
109
+ aiOutputPath: result.aiOutputPath,
110
+ humanOutputPath: result.humanOutputPath,
111
+ evidence: result.evidence
112
+ }
113
+ };
114
+ }
115
+ };
116
+ const handler = handlers[command.name];
117
+ if (handler) return handler();
113
118
 
114
119
  return {
115
120
  ok: false,
@@ -1,6 +1,6 @@
1
1
  # document-project
2
2
 
3
- Generate all project documentation by running `generate-document` for every template in the template library. Outputs AI-optimized docs to `.ai/` and human-readable docs to `docs/maintainers/`.
3
+ Generate all project documentation by running `generate-document` for every view model in `src/modules/documentation/views`. Outputs AI-optimized docs to `.ai/` and human-readable docs to `docs/maintainers/`.
4
4
 
5
5
  ## Inputs
6
6
 
@@ -11,9 +11,9 @@ Generate all project documentation by running `generate-document` for every temp
11
11
  - `strict?: boolean` — fail on unresolved warnings (default `false` in batch mode)
12
12
  - `maxValidationAttempts?: number` — override retry limit per document
13
13
 
14
- ## Shipped templates
14
+ ## Shipped targets
15
15
 
16
- All `.md` files under `sources.templatesRoot` (default `src/modules/documentation/templates`) are processed:
16
+ View models in `src/modules/documentation/views` map to these output targets:
17
17
 
18
18
  - `AGENTS.md`
19
19
  - `ARCHITECTURE.md`
@@ -33,10 +33,10 @@ All `.md` files under `sources.templatesRoot` (default `src/modules/documentatio
33
33
 
34
34
  ## Required behavior
35
35
 
36
- 1. Discover all `.md` templates in `sources.templatesRoot`.
37
- 2. For each template, invoke `generate-document` with the template basename as `documentType`.
36
+ 1. Discover all `.view.yaml` view models (fallback: discover templates in fixture workspaces without `views/`).
37
+ 2. For each view model, invoke `generate-document` using the view model `target` as `documentType`.
38
38
  3. Default overwrite behavior: **preserve AI docs** (`overwriteAi: false`), **overwrite human docs** (`overwriteHuman: true`).
39
- 4. Continue through all templates on individual failure; do not stop the batch.
39
+ 4. Continue through all targets on individual failure; do not stop the batch.
40
40
  5. Collect per-document results and emit a batch summary with total/succeeded/failed/skipped counts.
41
41
  6. Return `ok: true` only when zero documents failed.
42
42
 
@@ -1,10 +1,10 @@
1
1
  # generate-document
2
2
 
3
- Generate a single document for both canonical AI and human-readable surfaces using module config, schema, and templates.
3
+ Generate a single document for both canonical AI and human-readable surfaces using module config, schema, view models, and deterministic renderers.
4
4
 
5
5
  ## Inputs
6
6
 
7
- - `documentType` (required): basename of the doc to generate; must match a file under `sources.templatesRoot` (default `src/modules/documentation/templates`). Known templates:
7
+ - `documentType` (required): basename of the doc to generate; should match a `target` declared in `src/modules/documentation/views/*.view.yaml`. Known targets:
8
8
  - `AGENTS.md`
9
9
  - `ARCHITECTURE.md`
10
10
  - `PRINCIPLES.md`
@@ -34,11 +34,11 @@ Generate a single document for both canonical AI and human-readable surfaces usi
34
34
  1. Read `src/modules/documentation/RULES.md` and apply precedence order before generation.
35
35
  2. Load module config and resolve output roots from configured paths (`sources.aiRoot`, `sources.humanRoot`).
36
36
  3. Restrict writes strictly to configured output roots; reject writes outside those roots.
37
- 4. Resolve template for `documentType` from `sources.templatesRoot`.
37
+ 4. Resolve template for `documentType` from `sources.templatesRoot` (for section/coverage checks) and resolve matching view model target from `views/`.
38
38
  5. If template is missing, warn user and ask whether to continue without a template; continue only on explicit confirmation.
39
39
  6. Generate AI output first at `<aiRoot>/<documentType>` using `documentation-maintainer.md` + `documentation-schema.md`.
40
40
  7. Validate AI output against schema; on validation failure, auto-resolve/retry up to `generation.maxValidationAttempts` before failing.
41
- 8. Re-read generated AI output with project context, then generate human output at `<humanRoot>/<documentType>`.
41
+ 8. Parse AI output into keyed records, validate schema, normalize typed model, then render human output via named renderer functions from the matched view model.
42
42
  9. For templates containing `{{{ ... }}}`, execute block contents as generation instructions and ensure no unresolved blocks remain in output.
43
43
  10. Run section coverage validation (all required sections present, correct headings/order where required); retry/resolve on failure.
44
44
  11. Detect conflicts with higher-precedence docs and stop/prompt when policy-sensitive or unresolved.
@@ -0,0 +1,187 @@
1
+ import type { AiRecord } from "./parser.js";
2
+ import type {
3
+ NormalizedBaseRecord,
4
+ NormalizedCadence,
5
+ NormalizedCheck,
6
+ NormalizedCommand,
7
+ NormalizedConfig,
8
+ NormalizedDecision,
9
+ NormalizedDocument,
10
+ NormalizedExample,
11
+ NormalizedGuardrail,
12
+ NormalizedMeta,
13
+ NormalizedRef,
14
+ NormalizedRollback,
15
+ NormalizedRule,
16
+ NormalizedState,
17
+ NormalizedTerm,
18
+ NormalizedWorkflow,
19
+ NormalizedRunbook,
20
+ NormalizedWorkbook,
21
+ NormalizedChain,
22
+ NormalizedArtifact,
23
+ NormalizedPromotion,
24
+ NormalizedTransition
25
+ } from "./types.js";
26
+
27
+ function asStatus(v?: string): NormalizedBaseRecord["status"] {
28
+ if (v === "active" || v === "deprecated" || v === "draft" || v === "observed" || v === "planned") return v;
29
+ return undefined;
30
+ }
31
+
32
+ function asList(v?: string): string[] {
33
+ if (!v) return [];
34
+ return v
35
+ .split(",")
36
+ .map((x) => x.trim())
37
+ .filter(Boolean);
38
+ }
39
+
40
+ export function normalizeDocument(records: AiRecord[]): NormalizedDocument {
41
+ const refs: NormalizedRef[] = [];
42
+ const rules: NormalizedRule[] = [];
43
+ const checks: NormalizedCheck[] = [];
44
+ const decisions: NormalizedDecision[] = [];
45
+ const examples: NormalizedExample[] = [];
46
+ const terms: NormalizedTerm[] = [];
47
+ const commands: NormalizedCommand[] = [];
48
+ const workflows: NormalizedWorkflow[] = [];
49
+ const runbooks: NormalizedRunbook[] = [];
50
+ const workbooks: NormalizedWorkbook[] = [];
51
+ const chains: NormalizedChain[] = [];
52
+ const states: NormalizedState[] = [];
53
+ const transitions: NormalizedTransition[] = [];
54
+ const promotions: NormalizedPromotion[] = [];
55
+ const rollbacks: NormalizedRollback[] = [];
56
+ const artifacts: NormalizedArtifact[] = [];
57
+ const configs: NormalizedConfig[] = [];
58
+ const cadences: NormalizedCadence[] = [];
59
+ const guardrails: NormalizedGuardrail[] = [];
60
+ const refsById = new Map<string, NormalizedRef>();
61
+ const examplesByParent = new Map<string, NormalizedExample[]>();
62
+ const profileRecords = new Map<"core" | "runbook" | "workbook", Array<NormalizedBaseRecord>>([
63
+ ["core", []],
64
+ ["runbook", []],
65
+ ["workbook", []]
66
+ ]);
67
+
68
+ let meta: NormalizedMeta | null = null;
69
+ for (const rec of records) {
70
+ const status = asStatus(rec.kv["status"] ?? rec.kv["st"]);
71
+ if (rec.type === "meta") {
72
+ meta = {
73
+ schema: "base.v2",
74
+ doc: rec.kv["doc"] ?? "rules",
75
+ truth: (rec.kv["truth"] as NormalizedMeta["truth"]) ?? "canonical",
76
+ profile: rec.kv["profile"] as NormalizedMeta["profile"],
77
+ status,
78
+ title: rec.kv["title"],
79
+ owner: rec.kv["owner"],
80
+ tags: asList(rec.kv["tags"]),
81
+ refs: asList(rec.kv["refs"])
82
+ };
83
+ continue;
84
+ }
85
+
86
+ if (rec.type === "ref") {
87
+ const ref: NormalizedRef = {
88
+ id: rec.kv["id"] ?? rec.kv["name"] ?? "",
89
+ type: (rec.kv["type"] as NormalizedRef["type"]) ?? "doc",
90
+ target: rec.kv["target"] ?? rec.kv["path"] ?? "",
91
+ anchor: rec.kv["anchor"],
92
+ label: rec.kv["label"] ?? rec.kv["name"],
93
+ note: rec.kv["note"],
94
+ status
95
+ };
96
+ refs.push(ref);
97
+ if (ref.id) refsById.set(ref.id, ref);
98
+ continue;
99
+ }
100
+
101
+ if (rec.type === "rule") {
102
+ rules.push({
103
+ id: rec.kv["id"] ?? rec.kv["slot1"] ?? "",
104
+ level: (rec.kv["level"] as NormalizedRule["level"]) ?? "should",
105
+ scope: rec.kv["scope"] ?? "",
106
+ scope_kind: rec.kv["scope_kind"],
107
+ kind: rec.kv["kind"],
108
+ directive: rec.kv["directive"] ?? rec.kv["slot3"] ?? "",
109
+ why: rec.kv["why"] ?? "",
110
+ unless: rec.kv["unless"],
111
+ also: asList(rec.kv["also"]),
112
+ risk: rec.kv["risk"] as NormalizedRule["risk"],
113
+ approval: rec.kv["approval"] as NormalizedRule["approval"],
114
+ override: rec.kv["override"] as NormalizedRule["override"],
115
+ status,
116
+ refs: asList(rec.kv["refs"])
117
+ });
118
+ continue;
119
+ }
120
+
121
+ if (rec.type === "example") {
122
+ const ex: NormalizedExample = {
123
+ id: rec.kv["id"] ?? "",
124
+ for: rec.kv["for"] ?? "",
125
+ kind: (rec.kv["kind"] as NormalizedExample["kind"]) ?? "edge",
126
+ text: rec.kv["text"] ?? "",
127
+ status,
128
+ refs: asList(rec.kv["refs"])
129
+ };
130
+ examples.push(ex);
131
+ const list = examplesByParent.get(ex.for) ?? [];
132
+ list.push(ex);
133
+ examplesByParent.set(ex.for, list);
134
+ continue;
135
+ }
136
+
137
+ if (rec.type === "check") checks.push({ id: rec.kv["id"] ?? "", scope: rec.kv["scope"] ?? "", assertion: rec.kv["assertion"] ?? rec.kv["assert"] ?? "", when: rec.kv["when"], onFail: rec.kv["onFail"] as NormalizedCheck["onFail"], status, refs: asList(rec.kv["refs"]) });
138
+ if (rec.type === "decision") decisions.push({ id: rec.kv["id"] ?? "", topic: rec.kv["topic"] ?? "", choice: rec.kv["choice"] ?? "", why: rec.kv["why"] ?? "", consequence: rec.kv["consequence"] ?? rec.kv["then"], status, refs: asList(rec.kv["refs"]) });
139
+ if (rec.type === "term") terms.push({ name: rec.kv["name"] ?? "", definition: rec.kv["definition"] ?? rec.kv["def"] ?? "", status, refs: asList(rec.kv["refs"]) });
140
+ if (rec.type === "command" || rec.type === "cmd") commands.push({ id: rec.kv["id"] ?? rec.kv["slot1"] ?? "", name: rec.kv["name"] ?? "", use: rec.kv["use"] ?? "", scope: rec.kv["scope"] ?? "", expectation: rec.kv["expectation"] ?? rec.kv["expect"] ?? "", risk: rec.kv["risk"] as NormalizedCommand["risk"], sensitivity: rec.kv["sensitivity"] as NormalizedCommand["sensitivity"], status, refs: asList(rec.kv["refs"]) });
141
+ if (rec.type === "workflow" || rec.type === "wf") workflows.push({ id: rec.kv["id"] ?? rec.kv["slot1"] ?? "", name: rec.kv["name"] ?? "", when: rec.kv["when"] ?? "", steps: asList(rec.kv["steps"] ?? rec.kv["do"]), done: asList(rec.kv["done"]), forbid: asList(rec.kv["forbid"]), askIf: rec.kv["askIf"] ?? rec.kv["ask_if"], haltIf: rec.kv["haltIf"] ?? rec.kv["halt_if"], approval: rec.kv["approval"] as NormalizedWorkflow["approval"], risk: rec.kv["risk"] as NormalizedWorkflow["risk"], status, refs: asList(rec.kv["refs"]) });
142
+ if (rec.type === "runbook") runbooks.push({ name: rec.kv["name"] ?? "", scope: rec.kv["scope"] ?? "", owner: rec.kv["owner"] ?? "", status, refs: asList(rec.kv["refs"]) });
143
+ if (rec.type === "workbook") workbooks.push({ name: rec.kv["name"] ?? "", phase: rec.kv["phase"] ?? "", state: rec.kv["state"] ?? "", status, refs: asList(rec.kv["refs"]) });
144
+ if (rec.type === "chain") chains.push({ step: rec.kv["step"] ?? "", command: rec.kv["command"] ?? "", expectExit: Number.parseInt(rec.kv["expectExit"] ?? rec.kv["expect_exit"] ?? "0", 10), status, refs: asList(rec.kv["refs"]) });
145
+ if (rec.type === "state") states.push({ name: rec.kv["name"] ?? "", distTag: rec.kv["distTag"] ?? rec.kv["dist_tag"] ?? "", intent: rec.kv["intent"] ?? "", status, refs: asList(rec.kv["refs"]) });
146
+ if (rec.type === "transition") transitions.push({ from: rec.kv["from"] ?? "", to: rec.kv["to"] ?? "", requires: asList(rec.kv["requires"]), status, refs: asList(rec.kv["refs"]) });
147
+ if (rec.type === "promotion") promotions.push({ from: rec.kv["from"] ?? "", to: rec.kv["to"] ?? "", requires: asList(rec.kv["requires"]), status, refs: asList(rec.kv["refs"]) });
148
+ if (rec.type === "rollback") rollbacks.push({ strategy: rec.kv["strategy"] ?? "", note: rec.kv["note"] ?? "", status, refs: asList(rec.kv["refs"]) });
149
+ if (rec.type === "artifact") artifacts.push({ path: rec.kv["path"] ?? "", schema: rec.kv["schema"] ?? "", status, refs: asList(rec.kv["refs"]) });
150
+ if (rec.type === "config") configs.push({ key: rec.kv["key"] ?? "", default: rec.kv["default"] ?? "", status, refs: asList(rec.kv["refs"]) });
151
+ if (rec.type === "cadence") cadences.push({ rule: rec.kv["rule"] ?? "", status, refs: asList(rec.kv["refs"]) });
152
+ if (rec.type === "guardrail") guardrails.push({ id: rec.kv["id"] ?? "", level: (rec.kv["level"] as NormalizedGuardrail["level"]) ?? "should", directive: rec.kv["directive"] ?? "", why: rec.kv["why"] ?? "", status, refs: asList(rec.kv["refs"]) });
153
+ }
154
+
155
+ const core: NormalizedBaseRecord[] = [...refs, ...rules, ...checks, ...decisions, ...examples, ...terms, ...commands, ...workflows];
156
+ const runbook: NormalizedBaseRecord[] = [...runbooks, ...chains, ...states, ...transitions, ...promotions, ...rollbacks, ...artifacts, ...configs, ...cadences, ...guardrails];
157
+ const workbook: NormalizedBaseRecord[] = [...workbooks, ...states, ...transitions, ...artifacts, ...guardrails];
158
+ profileRecords.set("core", core);
159
+ profileRecords.set("runbook", runbook);
160
+ profileRecords.set("workbook", workbook);
161
+
162
+ return {
163
+ meta,
164
+ refs,
165
+ rules,
166
+ checks,
167
+ decisions,
168
+ examples,
169
+ terms,
170
+ commands,
171
+ workflows,
172
+ runbooks,
173
+ workbooks,
174
+ chains,
175
+ states,
176
+ transitions,
177
+ promotions,
178
+ rollbacks,
179
+ artifacts,
180
+ configs,
181
+ cadences,
182
+ guardrails,
183
+ refsById,
184
+ examplesByParent,
185
+ profileRecords
186
+ };
187
+ }
@@ -0,0 +1,41 @@
1
+ export type AiRecord = {
2
+ type: string;
3
+ kv: Record<string, string>;
4
+ raw: string;
5
+ };
6
+
7
+ export function parseAiRecordLine(line: string): AiRecord | null {
8
+ const trimmed = line.trim();
9
+ if (!trimmed || trimmed.startsWith("#")) return null;
10
+ const parts = trimmed.split("|");
11
+ if (parts.length < 2) return null;
12
+ const type = parts[0]?.trim() ?? "";
13
+ if (!type) return null;
14
+
15
+ const kv: Record<string, string> = {};
16
+ let slotIndex = 1;
17
+ for (const token of parts.slice(1)) {
18
+ const piece = token.trim();
19
+ if (!piece) continue;
20
+ const idx = piece.indexOf("=");
21
+ if (idx >= 0) {
22
+ const key = piece.slice(0, idx).trim();
23
+ const value = piece.slice(idx + 1).trim();
24
+ if (key) kv[key] = value;
25
+ continue;
26
+ }
27
+ // Transitional support: retain unkeyed tokens as synthetic slots.
28
+ kv[`slot${slotIndex}`] = piece;
29
+ slotIndex += 1;
30
+ }
31
+ return { type, kv, raw: line };
32
+ }
33
+
34
+ export function parseAiDocument(text: string): AiRecord[] {
35
+ const records: AiRecord[] = [];
36
+ for (const line of text.split("\n")) {
37
+ const rec = parseAiRecordLine(line);
38
+ if (rec) records.push(rec);
39
+ }
40
+ return records;
41
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Policy-gated `workspace-kit run` commands owned by the documentation module.
3
+ * Keep in sync with instruction names in `documentation/index.ts`.
4
+ */
5
+ export const DOCUMENTATION_POLICY_COMMAND_NAMES = [
6
+ ["document-project", "doc.document-project"],
7
+ ["generate-document", "doc.generate-document"]
8
+ ] as const;
@@ -0,0 +1,121 @@
1
+ import type {
2
+ NormalizedCheck,
3
+ NormalizedCommand,
4
+ NormalizedDecision,
5
+ NormalizedDocument,
6
+ NormalizedRule,
7
+ NormalizedTerm,
8
+ NormalizedWorkflow,
9
+ ViewModelDefinition,
10
+ ViewModelSection
11
+ } from "./types.js";
12
+
13
+ function stableSort(values: string[]): string[] {
14
+ return [...values].sort((a, b) => a.localeCompare(b));
15
+ }
16
+
17
+ export function brief_summary(input: string[]): string {
18
+ if (input.length === 0) return "No summary records.";
19
+ return input.map((line) => `- ${line}`).join("\n");
20
+ }
21
+
22
+ export function ordered_list(input: string[]): string {
23
+ if (input.length === 0) return "1. No entries";
24
+ return input.map((line, idx) => `${idx + 1}. ${line}`).join("\n");
25
+ }
26
+
27
+ export function rule_table(rules: NormalizedRule[]): string {
28
+ if (rules.length === 0) return "_No rules_";
29
+ const rows = [...rules].sort((a, b) => a.id.localeCompare(b.id));
30
+ const body = rows
31
+ .map((r) => `| ${r.id} | ${r.level} | ${r.scope || "-"} | ${r.directive || "-"} | ${r.why || "-"} |`)
32
+ .join("\n");
33
+ return `| ID | Level | Scope | Directive | Why |\n|---|---|---|---|---|\n${body}`;
34
+ }
35
+
36
+ export function check_table(checks: NormalizedCheck[]): string {
37
+ if (checks.length === 0) return "_No checks_";
38
+ const rows = [...checks].sort((a, b) => a.id.localeCompare(b.id));
39
+ const body = rows
40
+ .map((c) => `| ${c.id} | ${c.scope || "-"} | ${c.assertion || "-"} | ${c.onFail || "-"} |`)
41
+ .join("\n");
42
+ return `| ID | Scope | Assertion | On Fail |\n|---|---|---|---|\n${body}`;
43
+ }
44
+
45
+ export function command_reference(commands: NormalizedCommand[]): string {
46
+ if (commands.length === 0) return "_No commands_";
47
+ const rows = [...commands].sort((a, b) => a.name.localeCompare(b.name));
48
+ return rows.map((c) => `- \`${c.name}\`: ${c.expectation || c.use || "No expectation"}`).join("\n");
49
+ }
50
+
51
+ export function decision_section(decisions: NormalizedDecision[]): string {
52
+ if (decisions.length === 0) return "_No decisions_";
53
+ const rows = [...decisions].sort((a, b) => a.id.localeCompare(b.id));
54
+ return rows.map((d) => `### ${d.id}: ${d.topic}\n- Choice: ${d.choice}\n- Why: ${d.why}`).join("\n\n");
55
+ }
56
+
57
+ export function term_list(terms: NormalizedTerm[]): string {
58
+ if (terms.length === 0) return "_No terms_";
59
+ return [...terms]
60
+ .sort((a, b) => a.name.localeCompare(b.name))
61
+ .map((t) => `- **${t.name}**: ${t.definition}`)
62
+ .join("\n");
63
+ }
64
+
65
+ export function workflow_steps(workflows: NormalizedWorkflow[]): string {
66
+ if (workflows.length === 0) return "_No workflows_";
67
+ return workflows
68
+ .sort((a, b) => a.id.localeCompare(b.id))
69
+ .map((wf) => `### ${wf.id}: ${wf.name}\n${ordered_list(stableSort(wf.steps))}`)
70
+ .join("\n\n");
71
+ }
72
+
73
+ export function chain_steps(chains: Array<{ step: string; command: string; expectExit: number }>): string {
74
+ if (chains.length === 0) return "_No chain steps_";
75
+ return chains
76
+ .map((c, idx) => `${idx + 1}. ${c.step} -> \`${c.command}\` (expect ${c.expectExit})`)
77
+ .join("\n");
78
+ }
79
+
80
+ export function ref_table(refs: Array<{ id: string; type: string; target: string }>): string {
81
+ if (refs.length === 0) return "_No refs_";
82
+ const body = [...refs]
83
+ .sort((a, b) => a.id.localeCompare(b.id))
84
+ .map((r) => `| ${r.id} | ${r.type} | ${r.target} |`)
85
+ .join("\n");
86
+ return `| ID | Type | Target |\n|---|---|---|\n${body}`;
87
+ }
88
+
89
+ export const renderMetaSection = (doc: NormalizedDocument) =>
90
+ brief_summary([
91
+ `doc=${doc.meta?.doc ?? "unknown"}`,
92
+ `truth=${doc.meta?.truth ?? "unknown"}`,
93
+ `profile=${doc.meta?.profile ?? "core"}`
94
+ ]);
95
+ export const renderRuleSection = (doc: NormalizedDocument) => rule_table(doc.rules);
96
+ export const renderDecisionSection = (doc: NormalizedDocument) => decision_section(doc.decisions);
97
+
98
+ function renderSection(doc: NormalizedDocument, section: ViewModelSection): string {
99
+ const byName: Record<string, (d: NormalizedDocument) => string> = {
100
+ renderMetaSection,
101
+ renderRuleSection,
102
+ renderDecisionSection,
103
+ brief_summary: (d) => brief_summary(d.examples.map((e) => e.text)),
104
+ ordered_list: (d) => ordered_list(d.commands.map((c) => c.name)),
105
+ rule_table: (d) => rule_table(d.rules),
106
+ check_table: (d) => check_table(d.checks),
107
+ command_reference: (d) => command_reference(d.commands),
108
+ decision_section: (d) => decision_section(d.decisions),
109
+ term_list: (d) => term_list(d.terms),
110
+ workflow_steps: (d) => workflow_steps(d.workflows),
111
+ chain_steps: (d) => chain_steps(d.chains),
112
+ ref_table: (d) => ref_table(d.refs)
113
+ };
114
+ const fn = byName[section.renderer] ?? (() => "_No renderer_");
115
+ const title = section.title ?? section.id;
116
+ return `## ${title}\n\n${fn(doc)}`;
117
+ }
118
+
119
+ export function renderDocument(doc: NormalizedDocument, view: ViewModelDefinition): string {
120
+ return view.sections.map((s) => renderSection(doc, s)).join("\n\n").trim() + "\n";
121
+ }
@@ -0,0 +1,74 @@
1
+ import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ import type { DocumentationBatchResult, DocumentationGenerateOptions, DocumentationGenerateResult } from "./types.js";
3
+ import { existsSync } from "node:fs";
4
+ import { readdir } from "node:fs/promises";
5
+ import { resolve } from "node:path";
6
+ import { listViewModels, loadViewModel } from "./view-models.js";
7
+ import { loadRuntimeConfig } from "./runtime-config.js";
8
+
9
+ type GenerateAllDocumentsArgs = {
10
+ options?: DocumentationGenerateOptions;
11
+ };
12
+
13
+ export async function runGenerateAllDocuments(
14
+ args: GenerateAllDocumentsArgs,
15
+ ctx: ModuleLifecycleContext,
16
+ generateOne: (args: { documentType?: string; options?: DocumentationGenerateOptions }, ctx: ModuleLifecycleContext) => Promise<DocumentationGenerateResult>
17
+ ): Promise<DocumentationBatchResult> {
18
+ const config = await loadRuntimeConfig(ctx.workspacePath);
19
+ const workspaceViewsRoot = resolve(ctx.workspacePath, "src/modules/documentation/views");
20
+ const useWorkspaceViews = existsSync(workspaceViewsRoot);
21
+ const workItems: Array<{ documentType: string }> = [];
22
+ if (useWorkspaceViews) {
23
+ const viewFiles = await listViewModels(ctx.workspacePath);
24
+ for (const viewFile of viewFiles) {
25
+ const view = await loadViewModel(ctx.workspacePath, viewFile);
26
+ workItems.push({ documentType: view.target });
27
+ }
28
+ } else {
29
+ const templatesDir = resolve(config.sourceRoot, config.templatesRoot);
30
+ const listTemplateFiles = async (dir: string, baseDir: string): Promise<string[]> => {
31
+ const entries = await readdir(dir, { withFileTypes: true });
32
+ const files: string[] = [];
33
+ for (const entry of entries) {
34
+ const absPath = resolve(dir, entry.name);
35
+ if (entry.isDirectory()) files.push(...(await listTemplateFiles(absPath, baseDir)));
36
+ if (entry.isFile() && entry.name.endsWith(".md")) files.push(absPath.slice(baseDir.length + 1).split("\\").join("/"));
37
+ }
38
+ return files;
39
+ };
40
+ for (const templateFile of (await listTemplateFiles(templatesDir, templatesDir)).sort()) {
41
+ workItems.push({ documentType: templateFile });
42
+ }
43
+ }
44
+ const results: DocumentationGenerateResult[] = [];
45
+ let succeeded = 0;
46
+ let failed = 0;
47
+ let skipped = 0;
48
+ const batchOptions: DocumentationGenerateOptions = {
49
+ ...args.options,
50
+ overwriteAi: args.options?.overwriteAi ?? false,
51
+ overwriteHuman: args.options?.overwriteHuman ?? true,
52
+ strict: args.options?.strict ?? false
53
+ };
54
+
55
+ for (const item of workItems) {
56
+ const result = await generateOne({ documentType: item.documentType, options: batchOptions }, ctx);
57
+ results.push(result);
58
+ if (!result.ok) failed += 1;
59
+ else if (result.evidence.filesWritten.length > 0) succeeded += 1;
60
+ else skipped += 1;
61
+ }
62
+
63
+ return {
64
+ ok: failed === 0,
65
+ results,
66
+ summary: {
67
+ total: workItems.length,
68
+ succeeded,
69
+ failed,
70
+ skipped,
71
+ timestamp: new Date().toISOString()
72
+ }
73
+ };
74
+ }