superdev-cli 0.1.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 (182) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/.codex-plugin/plugin.json +27 -0
  4. package/CODE_OF_CONDUCT.md +109 -0
  5. package/CONTRIBUTING.md +205 -0
  6. package/LICENSE +202 -0
  7. package/NOTICE +11 -0
  8. package/README.md +1051 -0
  9. package/SECURITY.md +135 -0
  10. package/THIRD-PARTY-NOTICES.md +253 -0
  11. package/hooks/hooks.json +61 -0
  12. package/hooks/run.mjs +88 -0
  13. package/package.json +65 -0
  14. package/references/confidentiality.md +48 -0
  15. package/references/evidence-and-risk.md +73 -0
  16. package/references/operating-model.md +105 -0
  17. package/references/platform-capabilities.md +51 -0
  18. package/references/project-record.md +91 -0
  19. package/references/provider-contracts.md +102 -0
  20. package/scripts/doctor/doctor.mjs +539 -0
  21. package/scripts/privacy/scan-history.mjs +163 -0
  22. package/scripts/privacy/scan.mjs +368 -0
  23. package/scripts/validate/README.md +94 -0
  24. package/scripts/validate/common.mjs +149 -0
  25. package/scripts/validate/data-model.mjs +225 -0
  26. package/scripts/validate/dependencies.mjs +69 -0
  27. package/scripts/validate/docs-templates.mjs +99 -0
  28. package/scripts/validate/footprint.mjs +78 -0
  29. package/scripts/validate/imports.mjs +61 -0
  30. package/scripts/validate/manifests.mjs +173 -0
  31. package/scripts/validate/markdown.mjs +123 -0
  32. package/scripts/validate/migrations.mjs +187 -0
  33. package/scripts/validate/no-tests.mjs +136 -0
  34. package/scripts/validate/privacy.mjs +115 -0
  35. package/scripts/validate/record-links.mjs +127 -0
  36. package/scripts/validate/skill-commands.mjs +190 -0
  37. package/scripts/validate/skills.mjs +112 -0
  38. package/scripts/validate/specification.mjs +88 -0
  39. package/scripts/validate/style.mjs +78 -0
  40. package/scripts/validate/validate-all.mjs +175 -0
  41. package/skills/debug/SKILL.md +68 -0
  42. package/skills/decision/SKILL.md +99 -0
  43. package/skills/docs/SKILL.md +86 -0
  44. package/skills/docs/assets/fragments/api/events.md +14 -0
  45. package/skills/docs/assets/fragments/api/graphql.md +14 -0
  46. package/skills/docs/assets/fragments/api/local-only.md +13 -0
  47. package/skills/docs/assets/fragments/api/rest.md +13 -0
  48. package/skills/docs/assets/fragments/api/rpc.md +13 -0
  49. package/skills/docs/assets/fragments/async/event-bus.md +13 -0
  50. package/skills/docs/assets/fragments/async/none.md +11 -0
  51. package/skills/docs/assets/fragments/async/platform-jobs.md +13 -0
  52. package/skills/docs/assets/fragments/async/queue.md +13 -0
  53. package/skills/docs/assets/fragments/async/scheduler.md +13 -0
  54. package/skills/docs/assets/fragments/auth/detected-provider.md +15 -0
  55. package/skills/docs/assets/fragments/auth/neutral-base.md +14 -0
  56. package/skills/docs/assets/fragments/data/document.md +13 -0
  57. package/skills/docs/assets/fragments/data/external-saas.md +13 -0
  58. package/skills/docs/assets/fragments/data/key-value.md +13 -0
  59. package/skills/docs/assets/fragments/data/none.md +12 -0
  60. package/skills/docs/assets/fragments/data/sql-orm.md +13 -0
  61. package/skills/docs/assets/fragments/data/sql-plain.md +13 -0
  62. package/skills/docs/assets/fragments/env/conventional.md +13 -0
  63. package/skills/docs/assets/fragments/env/envx.md +13 -0
  64. package/skills/docs/assets/fragments/env/managed-platform.md +13 -0
  65. package/skills/docs/assets/fragments/env/none.md +11 -0
  66. package/skills/docs/assets/fragments/ui/api-only.md +13 -0
  67. package/skills/docs/assets/fragments/ui/cli.md +14 -0
  68. package/skills/docs/assets/fragments/ui/desktop.md +14 -0
  69. package/skills/docs/assets/fragments/ui/mobile.md +14 -0
  70. package/skills/docs/assets/fragments/ui/web.md +14 -0
  71. package/skills/docs/assets/templates/adr.md +62 -0
  72. package/skills/docs/assets/templates/api.md +30 -0
  73. package/skills/docs/assets/templates/architecture.md +52 -0
  74. package/skills/docs/assets/templates/change-impact-drift-report.md +29 -0
  75. package/skills/docs/assets/templates/compliance.md +29 -0
  76. package/skills/docs/assets/templates/data-schema.md +43 -0
  77. package/skills/docs/assets/templates/feature.md +43 -0
  78. package/skills/docs/assets/templates/foundations.md +55 -0
  79. package/skills/docs/assets/templates/jobs-webhooks.md +36 -0
  80. package/skills/docs/assets/templates/module-inventory.md +19 -0
  81. package/skills/docs/assets/templates/module.md +50 -0
  82. package/skills/docs/assets/templates/nfr.md +22 -0
  83. package/skills/docs/assets/templates/observability.md +28 -0
  84. package/skills/docs/assets/templates/pages-ui-actions.md +47 -0
  85. package/skills/docs/assets/templates/project-summary.md +44 -0
  86. package/skills/docs/assets/templates/roles-permissions.md +26 -0
  87. package/skills/docs/assets/templates/test-plan.md +23 -0
  88. package/skills/docs/assets/templates/workflow-state-machine.md +43 -0
  89. package/skills/docs/references/adr-authoring.md +24 -0
  90. package/skills/docs/references/apis-and-data.md +23 -0
  91. package/skills/docs/references/capability-fragments.md +25 -0
  92. package/skills/docs/references/change-tracking.md +62 -0
  93. package/skills/docs/references/diagrams.md +31 -0
  94. package/skills/docs/references/discovery.md +59 -0
  95. package/skills/docs/references/edge-cases.md +45 -0
  96. package/skills/docs/references/foundations-modules.md +18 -0
  97. package/skills/docs/references/ingestion.md +52 -0
  98. package/skills/docs/references/initialize-adopt.md +25 -0
  99. package/skills/docs/references/module-decomposition.md +38 -0
  100. package/skills/docs/references/profiles.md +60 -0
  101. package/skills/docs/references/quality-attributes.md +27 -0
  102. package/skills/docs/references/reverse-engineer.md +21 -0
  103. package/skills/docs/references/spec-depths.md +31 -0
  104. package/skills/docs/references/summarize.md +18 -0
  105. package/skills/docs/references/surfaces-and-actions.md +24 -0
  106. package/skills/docs/references/validation.md +47 -0
  107. package/skills/docs/references/workflows-and-jobs.md +25 -0
  108. package/skills/docs/scripts/ingest.mjs +984 -0
  109. package/skills/docs/scripts/profile-detect.mjs +299 -0
  110. package/skills/docs/scripts/screen.mjs +96 -0
  111. package/skills/docs/scripts/template-lint.mjs +143 -0
  112. package/skills/docs/scripts/validate-docs.mjs +363 -0
  113. package/skills/doctor/SKILL.md +167 -0
  114. package/skills/feature/SKILL.md +132 -0
  115. package/skills/init/SKILL.md +137 -0
  116. package/skills/project/SKILL.md +261 -0
  117. package/skills/project/references/commands.md +213 -0
  118. package/skills/resume/SKILL.md +79 -0
  119. package/skills/review/SKILL.md +91 -0
  120. package/skills/status/SKILL.md +85 -0
  121. package/skills/task/SKILL.md +183 -0
  122. package/src/cli/product-map.mjs +468 -0
  123. package/src/cli/render.mjs +544 -0
  124. package/src/cli.mjs +2774 -0
  125. package/src/cloud/crypto.mjs +118 -0
  126. package/src/cloud/merge.mjs +186 -0
  127. package/src/cloud/policy.mjs +115 -0
  128. package/src/cloud/sync.mjs +512 -0
  129. package/src/cloud/transport.mjs +116 -0
  130. package/src/db/connect.mjs +189 -0
  131. package/src/db/maintenance.mjs +419 -0
  132. package/src/db/migrate.mjs +188 -0
  133. package/src/db/migrations/001_initial.sql +1024 -0
  134. package/src/db/migrations/002_docs_coverage.sql +126 -0
  135. package/src/db/migrations/003_memory_retrieval_and_integrity.sql +168 -0
  136. package/src/db/migrations/004_task_categories.sql +59 -0
  137. package/src/db/migrations/005_executable_evidence.sql +32 -0
  138. package/src/db/migrations/006_module_and_feature_boundaries.sql +27 -0
  139. package/src/db/migrations/007_drop_redundant_module_users.sql +13 -0
  140. package/src/db/migrations/008_changes_assumptions_test_plans_api_services.sql +166 -0
  141. package/src/db/migrations/009_retired_documents.sql +54 -0
  142. package/src/db/migrations/010_test_plan_evidence.sql +20 -0
  143. package/src/db/migrations/011_criterion_waiver.sql +13 -0
  144. package/src/db/migrations/012_synchronization.sql +56 -0
  145. package/src/db/store.mjs +327 -0
  146. package/src/decisions/record.mjs +167 -0
  147. package/src/docs/proposals.mjs +871 -0
  148. package/src/docs/render.mjs +1424 -0
  149. package/src/docs/templates.mjs +1281 -0
  150. package/src/features/acceptance.mjs +315 -0
  151. package/src/features/specify.mjs +248 -0
  152. package/src/init/discovery.mjs +901 -0
  153. package/src/init/index.mjs +784 -0
  154. package/src/init/questions.mjs +562 -0
  155. package/src/memory/benchmark.mjs +171 -0
  156. package/src/memory/capture.mjs +135 -0
  157. package/src/memory/consolidate.mjs +255 -0
  158. package/src/memory/index.mjs +810 -0
  159. package/src/model/ids.mjs +185 -0
  160. package/src/model/screening.mjs +150 -0
  161. package/src/model/toolkit.mjs +258 -0
  162. package/src/model/vocabulary.mjs +190 -0
  163. package/src/product/assumptions.mjs +120 -0
  164. package/src/product/changes.mjs +180 -0
  165. package/src/product/test-plans.mjs +183 -0
  166. package/src/progress/index.mjs +1364 -0
  167. package/src/runtime/harness.mjs +264 -0
  168. package/src/runtime/hooks.mjs +1021 -0
  169. package/src/runtime/identity.mjs +305 -0
  170. package/src/runtime/resume.mjs +343 -0
  171. package/src/runtime/session.mjs +679 -0
  172. package/src/runtime/version.mjs +315 -0
  173. package/src/service/assets/control-center.html +206 -0
  174. package/src/service/assets/control-center.manifest.json +7 -0
  175. package/src/service/manage.mjs +542 -0
  176. package/src/service/mutations.mjs +860 -0
  177. package/src/service/read-model.mjs +1557 -0
  178. package/src/service/server.mjs +783 -0
  179. package/src/tasks/categories.mjs +154 -0
  180. package/src/tasks/derive.mjs +977 -0
  181. package/src/tasks/lifecycle.mjs +830 -0
  182. package/src/verify/index.mjs +236 -0
@@ -0,0 +1,1281 @@
1
+ // One pure renderer per generated artifact, mapped field by field onto the Docs
2
+ // skill templates in skills/docs/assets/templates.
3
+ //
4
+ // Nothing here touches the database, the clock or the filesystem. Every renderer
5
+ // takes a plain data object assembled by render.mjs and returns a Markdown body,
6
+ // so the same data always produces the same bytes. Ordering decisions belong to
7
+ // the caller: whatever order the arrays arrive in is the order that appears.
8
+ //
9
+ // Two characters are deliberately absent: U+2014 and the tick mark U+2713 the
10
+ // permission matrices use. Both are refused by the storage screener, and a
11
+ // generated body is stored, so a template that emitted them could never be
12
+ // written down.
13
+
14
+ import { titleCase } from "../model/vocabulary.mjs";
15
+
16
+ // ------------------------------------------------------------------ primitives
17
+
18
+ const NOT_RECORDED = "Not recorded";
19
+
20
+ const text = (value, fallback = NOT_RECORDED) => {
21
+ const s = String(value ?? "").trim();
22
+ return s || fallback;
23
+ };
24
+
25
+ /** Table cells and inline slots: one line, pipes escaped. */
26
+ const inline = (value, fallback = "-") =>
27
+ text(value, fallback).replace(/\s*\r?\n\s*/g, " ").replace(/\|/g, "\\|");
28
+
29
+ const label = (name, value, fallback) => `- **${name}:** ${inline(value, fallback ?? NOT_RECORDED)}`;
30
+
31
+ const commas = (items, fallback = "none") =>
32
+ items?.length ? items.map((i) => inline(i)).join(", ") : fallback;
33
+
34
+ const bullets = (items, fallback = "- None recorded.") =>
35
+ items?.length ? items.map((i) => `- ${text(i, "-")}`).join("\n") : fallback;
36
+
37
+ const numbered = (items, fallback = "None recorded.") =>
38
+ items?.length ? items.map((i, n) => `${n + 1}. ${text(i, "-")}`).join("\n") : fallback;
39
+
40
+ const table = (headers, rows, fallback = "None recorded.") => {
41
+ if (!rows?.length) return fallback;
42
+ return [
43
+ `| ${headers.join(" | ")} |`,
44
+ `|${headers.map(() => "---").join("|")}|`,
45
+ ...rows.map((r) => `| ${r.map((c) => inline(c)).join(" | ")} |`),
46
+ ].join("\n");
47
+ };
48
+
49
+ const fence = (lang, body) => "```" + lang + "\n" + body + "\n" + "```";
50
+
51
+ const section = (title, body) => `## ${title}\n\n${body}`;
52
+
53
+ const doc = (...parts) => parts.filter((p) => p !== null && p !== undefined && p !== "").join("\n\n");
54
+
55
+ const link = (ref) => (ref?.link ? `[${inline(ref.label)}](${ref.link})` : inline(ref?.label));
56
+
57
+ const links = (refs, fallback = "none") =>
58
+ refs?.length ? refs.map((r) => link(r)).join(", ") : fallback;
59
+
60
+ const stat = (value) => titleCase(text(value, "unknown"));
61
+
62
+ /** The provenance line every template wants. The marker above the body carries
63
+ * the revision and hash, so repeating them here would change the hash on every
64
+ * unrelated write and churn the file for no reader. */
65
+ const VERIFIED = "- **Last verified:** see the generation marker at the top of this file.";
66
+
67
+ const REGENERATED = "<!-- REGENERATED - do not hand-edit. Rebuild it with `superdev docs generate`. -->";
68
+
69
+ /** Mermaid node identifier. Stable for a given name, never empty. */
70
+ const nodeId = (name, index = 0) => {
71
+ const base = String(name ?? "").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
72
+ return base ? `n_${base}`.slice(0, 48) : `n_${index}`;
73
+ };
74
+
75
+ const mermaidLabel = (value, fallback = "unnamed") =>
76
+ text(value, fallback).replace(/\s*\r?\n\s*/g, " ").replace(/["`]/g, "'").slice(0, 80);
77
+
78
+ const applicability = (row) =>
79
+ row.state === "not_applicable" || row.applicability === "not_applicable"
80
+ ? `N/A - ${text(row.reason ?? row.reason_not_applicable, "no reason recorded")}`
81
+ : text(row.summary ?? row.behavior ?? row.choice, NOT_RECORDED);
82
+
83
+ // ---------------------------------------------------------------- foundations
84
+
85
+ export function foundationsProduct(data) {
86
+ const p = data.project;
87
+ return doc(
88
+ `# ${text(p.name, "Project")} - Foundations: Product`,
89
+ [label("Status", stat(p.status)), label("Working mode", stat(p.working_mode)), VERIFIED].join("\n"),
90
+ section("Product", text(p.statement, "No product statement recorded.")),
91
+ section("Problem", text(p.problem, "No problem statement recorded.")),
92
+ section(
93
+ "Non-goals",
94
+ bullets(
95
+ data.nonGoals.map((n) => (n.why ? `${n.statement} (${n.why})` : n.statement)),
96
+ "- None declared. An empty list here is a gap, not a statement.",
97
+ ),
98
+ ),
99
+ section(
100
+ "Goals",
101
+ table(
102
+ ["Goal", "Why it matters", "Priority", "Status"],
103
+ data.goals.map((g) => [`${g.id} ${g.name}`, g.why_it_matters, g.priority, stat(g.status)]),
104
+ "No goals recorded.",
105
+ ),
106
+ ),
107
+ section(
108
+ "Success criteria",
109
+ table(
110
+ ["Goal", "Criterion", "Target", "Measured how", "Status"],
111
+ data.successCriteria.map((c) => [c.goal, c.criterion, c.target, c.measurement_method, stat(c.status)]),
112
+ "No success criteria recorded.",
113
+ ),
114
+ ),
115
+ section(
116
+ "Milestones",
117
+ table(
118
+ ["Milestone", "Outcome", "Entry conditions", "Exit conditions", "Target", "Status"],
119
+ data.milestones.map((m) => [
120
+ `${m.id} ${m.name}`, m.outcome, commas(m.entry, "none"), commas(m.exit, "none"),
121
+ m.target_date, stat(m.status),
122
+ ]),
123
+ "No milestones recorded.",
124
+ ),
125
+ ),
126
+ );
127
+ }
128
+
129
+ export function foundationsUsersAndRoles(data) {
130
+ return doc(
131
+ `# ${text(data.project.name, "Project")} - Foundations: Users and Roles`,
132
+ [label("Status", stat(data.project.status)), VERIFIED].join("\n"),
133
+ section(
134
+ "Users and roles",
135
+ table(
136
+ ["Role", "Who", "Primary goals"],
137
+ data.roles.map((r) => [r.name, r.who ?? r.description, r.primary_goals]),
138
+ "No roles recorded.",
139
+ ),
140
+ ),
141
+ section(
142
+ "Users identified in discovery",
143
+ bullets(
144
+ data.users.map((u) => `${u.statement} (${titleCase(u.epistemic_status)})`),
145
+ "- None recorded.",
146
+ ),
147
+ ),
148
+ section("Permission matrices", `The three role matrices live in ${link(data.permissionsDoc)}.`),
149
+ );
150
+ }
151
+
152
+ export function foundationsRolesAndPermissions(data) {
153
+ const roleNames = data.roles.map((r) => r.name);
154
+ return doc(
155
+ `# ${text(data.project.name, "Project")} - Roles and Permissions`,
156
+ [
157
+ label("Status", stat(data.project.status)),
158
+ label("Permission source of truth (code)", commas(data.enforcementPoints, "not declared")),
159
+ VERIFIED,
160
+ ].join("\n"),
161
+ section(
162
+ "1. Role by module visibility",
163
+ table(
164
+ ["Module", ...roleNames],
165
+ data.visibility.map((row) => [row.module, ...roleNames.map((r) => row.access[r] ?? "-")]),
166
+ "No module-scoped permissions recorded.",
167
+ ),
168
+ ),
169
+ section(
170
+ "2. Role by action capability",
171
+ doc(
172
+ table(
173
+ ["Role", "Scope", "Capability", "Access", "Enforced at"],
174
+ data.capabilities.map((c) => [c.role, c.scope, c.capability, c.access_level, c.enforcement_point]),
175
+ "No action-level permissions recorded.",
176
+ ),
177
+ data.moduleDocs.length
178
+ ? `Per-module action matrices: ${links(data.moduleDocs)}.`
179
+ : "No module surface documents to link.",
180
+ ),
181
+ ),
182
+ section(
183
+ "3. Role by field sensitivity",
184
+ table(
185
+ ["Entity.field", "Class", ...roleNames],
186
+ data.sensitiveFields.map((f) => [f.field, f.sensitivity_class, ...roleNames.map((r) => f.access[r] ?? "-")]),
187
+ "No fields carry a sensitivity class above none.",
188
+ ),
189
+ ),
190
+ section(
191
+ "Enforcement",
192
+ doc(
193
+ "Legend: `full` full access, `own` only own records, `read` read only, `redacted` masked, `-` blocked.",
194
+ bullets(data.enforcementPoints, "- No enforcement points recorded. A matrix nobody enforces is drift."),
195
+ ),
196
+ ),
197
+ );
198
+ }
199
+
200
+ export function foundationsScope(data) {
201
+ return doc(
202
+ `# ${text(data.project.name, "Project")} - Foundations: Scope`,
203
+ [label("Status", stat(data.project.status)), VERIFIED].join("\n"),
204
+ section(
205
+ "In (current horizon)",
206
+ numbered(
207
+ data.inScope.map((s) => (s.horizon ? `${s.statement} (horizon: ${s.horizon})` : s.statement)),
208
+ "Nothing declared in scope.",
209
+ ),
210
+ ),
211
+ section(
212
+ "Out (deliberate)",
213
+ numbered(
214
+ data.outScope.map((s) => `${s.statement} - ${text(s.why, "no reason recorded")}`),
215
+ "Nothing declared out of scope.",
216
+ ),
217
+ ),
218
+ section(
219
+ "Non-goals",
220
+ numbered(
221
+ data.nonGoals.map((s) => `${s.statement} - ${text(s.why, "no reason recorded")}`),
222
+ "No non-goals declared.",
223
+ ),
224
+ ),
225
+ section(
226
+ "Feature scope by module",
227
+ table(
228
+ ["Feature", "Module", "In", "Out"],
229
+ data.featureScope.map((f) => [link(f.doc), f.module, commas(f.in, "none"), commas(f.out, "none")]),
230
+ "No feature scope recorded.",
231
+ ),
232
+ ),
233
+ );
234
+ }
235
+
236
+ export function foundationsArchitecture(data) {
237
+ const system = nodeId(data.project.name || "system");
238
+ const context = [
239
+ "graph LR",
240
+ ...data.actors.map((a, i) => ` ${nodeId(a, i)}["${mermaidLabel(a)}"] --> ${system}`),
241
+ ` ${system}["${mermaidLabel(data.project.name, "System")}"]`,
242
+ ...data.externals.map((e, i) => ` ${system} --> ${nodeId(`ext_${e}`, i)}["${mermaidLabel(e)}"]`),
243
+ ].join("\n");
244
+
245
+ const moduleMap = [
246
+ "graph TD",
247
+ ...data.modules.map((m, i) => ` ${nodeId(m.slug, i)}["${mermaidLabel(m.name)}"]`),
248
+ ...data.moduleEdges.map((e) => ` ${nodeId(e.from)} --> ${nodeId(e.to)}`),
249
+ ].join("\n");
250
+
251
+ const critical = data.criticalPath.length
252
+ ? ["sequenceDiagram", ...data.criticalPath.map((s) =>
253
+ ` ${nodeId(s.from, 0)}->>${nodeId(s.to, 1)}: ${mermaidLabel(s.action)}`)].join("\n")
254
+ : "sequenceDiagram\n note over system: No workflow recorded yet.";
255
+
256
+ return doc(
257
+ `# ${text(data.project.name, "Project")} - Architecture`,
258
+ [label("Status", stat(data.project.status)), VERIFIED].join("\n"),
259
+ section("Shape", text(data.shape, "No system shape recorded.")),
260
+ section("System context", `${fence("mermaid", context)}\n\n*Claim: ${text(data.contextClaim)}*`),
261
+ section(
262
+ "Runtime pieces",
263
+ table(
264
+ ["Piece", "Runs where", "Talks to", "Evidence"],
265
+ data.pieces.map((p) => [p.name, p.runs_where, commas(p.talksTo, "nothing recorded"), p.evidence_ref]),
266
+ "No runtime pieces recorded. The module map below is a logical decomposition, not a topology.",
267
+ ),
268
+ ),
269
+ section("Module map", `${fence("mermaid", moduleMap)}\n\n*Claim: ${text(data.moduleClaim)}*`),
270
+ section(
271
+ "Data ownership",
272
+ table(
273
+ ["Entity group", "Owning module", "Consumers"],
274
+ data.ownership.map((o) => [commas(o.entities, "none"), o.module, commas(o.consumers, "none")]),
275
+ "No entities recorded.",
276
+ ),
277
+ ),
278
+ section("Critical path", `${fence("mermaid", critical)}\n\n*Claim: ${text(data.criticalClaim)}*`),
279
+ section(
280
+ "Boundaries and constraints",
281
+ bullets(
282
+ data.boundaries.map((b) => `${b.area}: ${applicability(b)}${b.evidence_ref ? ` (${b.evidence_ref})` : ""}`),
283
+ "- No boundaries recorded.",
284
+ ),
285
+ ),
286
+ );
287
+ }
288
+
289
+ export function foundationsStack(data) {
290
+ return doc(
291
+ `# ${text(data.project.name, "Project")} - Foundations: Stack`,
292
+ [label("Status", stat(data.project.status)), VERIFIED].join("\n"),
293
+ "Capability slots are filled by evidence or an accepted decision only. An empty slot is a question, never an assumption.",
294
+ section(
295
+ "Capability slots",
296
+ table(
297
+ ["Capability", "Choice", "State", "Evidence / decision"],
298
+ data.slots.map((s) => [
299
+ s.area,
300
+ s.state === "specified" ? s.choice : applicability(s),
301
+ stat(s.state),
302
+ s.decision_id ? `${s.decision_id} ${text(s.evidence_ref, "")}`.trim() : s.evidence_ref,
303
+ ]),
304
+ "No stack slots recorded.",
305
+ ),
306
+ ),
307
+ section("Design direction", text(data.designDirection, "No design system recorded, and none deliberately declined.")),
308
+ );
309
+ }
310
+
311
+ export function foundationsGlossary(data) {
312
+ return doc(
313
+ `# ${text(data.project.name, "Project")} - Foundations: Glossary`,
314
+ VERIFIED,
315
+ "One meaning per term. A term with two meanings is a defect, not a nuance.",
316
+ section(
317
+ "Terms",
318
+ table(
319
+ ["Term", "Meaning", "Source"],
320
+ data.terms.map((t) => [t.term, t.meaning, t.source_ref]),
321
+ "No terms recorded.",
322
+ ),
323
+ ),
324
+ );
325
+ }
326
+
327
+ export function foundationsNfrs(data) {
328
+ return doc(
329
+ `# ${text(data.project.name, "Project")} - Non-Functional Requirements`,
330
+ [label("Status", stat(data.project.status)), VERIFIED].join("\n"),
331
+ section(
332
+ "Requirements",
333
+ table(
334
+ ["#", "Requirement", "Target", "Source", "Measured how", "Status", "Scope"],
335
+ data.nfrs.map((n) => [
336
+ n.id, n.requirement, n.target, n.target_source, n.measurement_method, stat(n.status), n.scope,
337
+ ]),
338
+ "No non-functional requirements recorded.",
339
+ ),
340
+ ),
341
+ section(
342
+ "Rules",
343
+ [
344
+ "- A target without a source is a question for the owner, not a requirement. It is listed under Open below.",
345
+ "- A requirement without a measurement method is a draft.",
346
+ "- `unmeasured` is an honest status. Fabricated compliance is drift.",
347
+ ].join("\n"),
348
+ ),
349
+ section(
350
+ "Open (awaiting owner targets)",
351
+ table(
352
+ ["Area", "Why it matters here", "Proposed measurement"],
353
+ data.open.map((o) => [o.category, o.requirement, o.measurement_method]),
354
+ "Every recorded requirement carries a source.",
355
+ ),
356
+ ),
357
+ );
358
+ }
359
+
360
+ export function foundationsCompliance(data) {
361
+ return doc(
362
+ `# ${text(data.project.name, "Project")} - Compliance`,
363
+ [
364
+ label("Status", stat(data.project.status)),
365
+ label("Declared regimes", links(data.regimes, "none declared")),
366
+ VERIFIED,
367
+ ].join("\n"),
368
+ "Regimes are only ever declared by a recorded decision. None is inferred from geography or vertical.",
369
+ section(
370
+ "Regulated data inventory",
371
+ table(
372
+ ["Entity.field", "Class", "Modules touching it"],
373
+ data.inventory.map((i) => [i.field, i.sensitivity_class, commas(i.modules, "none")]),
374
+ "No field carries a sensitivity class above none.",
375
+ ),
376
+ ),
377
+ section(
378
+ "Handling rules",
379
+ table(
380
+ ["Concern", "Rule", "Enforced at"],
381
+ data.rules.map((r) => [r.concern, r.rule, r.enforced_at]),
382
+ "No handling rules recorded.",
383
+ ),
384
+ ),
385
+ section(
386
+ "Gaps (open items)",
387
+ table(
388
+ ["Gap", "Risk", "Owner decision needed"],
389
+ data.gaps.map((g) => [g.gap, g.risk, g.question]),
390
+ "No open compliance gaps recorded.",
391
+ ),
392
+ ),
393
+ );
394
+ }
395
+
396
+ // -------------------------------------------------------------------- modules
397
+
398
+ export function moduleInventory(data) {
399
+ return doc(
400
+ `# ${text(data.project.name, "Project")} - Module Inventory`,
401
+ [
402
+ "- **Status:** living registry, regenerated from the database.",
403
+ VERIFIED,
404
+ ].join("\n"),
405
+ section(
406
+ "Modules",
407
+ table(
408
+ ["Module", "Purpose", "Primary users", "Owns", "Status", "Doc root"],
409
+ data.modules.map((m) => [
410
+ m.name, m.purpose, commas(m.primary_users, "none"), commas(m.owns, "none"),
411
+ stat(m.status), link(m.doc),
412
+ ]),
413
+ "No modules recorded.",
414
+ ),
415
+ ),
416
+ section(
417
+ "Status rules",
418
+ [
419
+ "- `implemented` requires implementation parity to have been verified at least once.",
420
+ "- A deprecated module keeps its row and its doc root. History is never deleted.",
421
+ ].join("\n"),
422
+ ),
423
+ section("Deliberate exclusions", bullets(data.exclusions, "- None recorded.")),
424
+ );
425
+ }
426
+
427
+ export function moduleSpec(data) {
428
+ const m = data.module;
429
+ return doc(
430
+ `# Module: ${text(m.name, "Unnamed module")}`,
431
+ [
432
+ label("Status", stat(m.status)),
433
+ label("Purpose", m.purpose),
434
+ label("Primary users", commas(data.primaryUsers, "none recorded")),
435
+ label("Owns", commas(data.owns, "none recorded")),
436
+ label("Does not own (consumes)", commas(data.consumes, "nothing recorded")),
437
+ VERIFIED,
438
+ ].join("\n"),
439
+ section(
440
+ "Surfaces",
441
+ table(
442
+ ["Route", "Surface", "Purpose", "Primary role", "Doc"],
443
+ data.surfaces.map((s) => [s.route, s.name, s.purpose, s.primary_role, link(s.doc)]),
444
+ "No surfaces recorded.",
445
+ ),
446
+ ),
447
+ section(
448
+ "API surface",
449
+ table(
450
+ ["Operation", "Purpose", "Doc"],
451
+ data.apis.map((a) => [`${text(a.method_or_procedure, "")} ${text(a.path_or_topic, a.name)}`.trim(), a.purpose, link(a.doc)]),
452
+ "No API operations recorded.",
453
+ ),
454
+ ),
455
+ section(
456
+ "Data",
457
+ table(
458
+ ["Entity", "Role in module", "Doc"],
459
+ data.entities.map((e) => [e.name, e.role, link(e.doc)]),
460
+ "No entities recorded.",
461
+ ),
462
+ ),
463
+ section(
464
+ "Wiring (key actions end to end)",
465
+ table(
466
+ ["Action", "Path"],
467
+ data.wiring.map((w) => [w.action, w.path]),
468
+ "No actions recorded, so no end-to-end path can be asserted.",
469
+ ),
470
+ ),
471
+ section(
472
+ "State machines",
473
+ table(
474
+ ["Entity", "Initial state", "States", "Doc"],
475
+ data.stateMachines.map((s) => [s.entity_name, s.initial_state, s.stateCount, link(s.doc)]),
476
+ "None recorded.",
477
+ ),
478
+ ),
479
+ section(
480
+ "Events",
481
+ table(
482
+ ["Event", "Direction", "Payload owner", "Consumers"],
483
+ data.events.map((e) => [e.event, e.direction, e.owner, commas(e.consumers, "none")]),
484
+ "No events recorded.",
485
+ ),
486
+ ),
487
+ section(
488
+ "Edge cases",
489
+ table(
490
+ ["Category", "Outcome", "Features"],
491
+ data.edgeCases.map((e) => [titleCase(e.category), e.outcome, commas(e.features, "none")]),
492
+ "No edge-case walk recorded.",
493
+ ),
494
+ ),
495
+ section(
496
+ "Twenty-step completeness",
497
+ table(
498
+ ["#", "Step", "State", "Outcome"],
499
+ data.steps.map((s) => [s.step, s.step_name, stat(s.state), applicability(s)]),
500
+ "The twenty-step loop has not been started for this module.",
501
+ ),
502
+ ),
503
+ );
504
+ }
505
+
506
+ // -------------------------------------------------------------------- feature
507
+
508
+ export function featureSpec(data) {
509
+ const f = data.feature;
510
+ const depth = text(f.spec_depth, "standard");
511
+ const parts = [
512
+ `# Feature: ${text(f.name, "Unnamed feature")}`,
513
+ [
514
+ label("Status", stat(f.status)),
515
+ label("Depth", stat(depth)),
516
+ label("Module", link(data.moduleDoc)),
517
+ label("Risk level", f.risk_level),
518
+ label("Milestone", data.milestone ?? "none"),
519
+ label("Goals", links(data.goals, "none linked")),
520
+ VERIFIED,
521
+ ].join("\n"),
522
+ section(
523
+ "Microspec",
524
+ [
525
+ label("Purpose", f.purpose),
526
+ label("User", f.user_statement),
527
+ label("User value", f.user_value),
528
+ `- **Scope:** in: ${commas(data.scopeIn, "not recorded")}; out: ${commas(data.scopeOut, "not recorded")}`,
529
+ label("Affected contracts", links(data.contracts, "none linked")),
530
+ ].join("\n") +
531
+ "\n\n" +
532
+ doc(
533
+ `### Primary flow\n\n${numbered(data.flow, "No flow recorded.")}`,
534
+ `### Acceptance criteria\n\n${table(
535
+ ["Criterion", "Verified how", "Status", "Evidence"],
536
+ data.acceptance.map((a) => [a.criterion, a.verification_method, stat(a.status), a.evidence_id]),
537
+ "No acceptance criteria recorded.",
538
+ )}`,
539
+ `### Error and edge behavior\n\n${table(
540
+ ["Category", "Applicability", "Behavior or reason"],
541
+ data.edgeCases.map((e) => [titleCase(e.category), stat(e.applicability), applicability(e)]),
542
+ "No edge-case walk recorded.",
543
+ )}`,
544
+ `### Test evidence\n\n${table(
545
+ ["Evidence", "Type", "Result", "Reference"],
546
+ data.evidence.map((e) => [e.summary, e.evidence_type, e.result, e.reference]),
547
+ "No verification evidence recorded.",
548
+ )}`,
549
+ ),
550
+ ),
551
+ ];
552
+
553
+ if (depth === "standard" || depth === "full") {
554
+ parts.push(
555
+ section(
556
+ "Standard",
557
+ doc(
558
+ `### Surfaces and actions\n\n${table(
559
+ ["Surface", "Route", "Actions", "Doc"],
560
+ data.surfaces.map((s) => [s.name, s.route, s.actionCount, link(s.doc)]),
561
+ "No surfaces recorded.",
562
+ )}`,
563
+ `### API and data impact\n\n${table(
564
+ ["Kind", "Name", "Purpose", "Doc"],
565
+ data.apiAndData.map((a) => [a.kind, a.name, a.purpose, link(a.doc)]),
566
+ "No API operations or entities recorded.",
567
+ )}`,
568
+ `### Roles and permissions delta\n\n${table(
569
+ ["Role", "Capability", "Access", "Enforced at"],
570
+ data.permissions.map((p) => [p.role, p.capability, p.access_level, p.enforcement_point]),
571
+ "No permission changes recorded.",
572
+ )}`,
573
+ `### Workflow and states\n\n${table(
574
+ ["Workflow or machine", "Trigger or entity", "Doc"],
575
+ data.workflows.map((w) => [w.name, w.trigger, link(w.doc)]),
576
+ "No workflows or state machines recorded.",
577
+ )}`,
578
+ `### Non-happy paths\n\n${table(
579
+ ["Workflow", "Step", "Condition", "Behavior"],
580
+ data.failurePaths.map((p) => [p.workflow, p.step, p.condition, p.behavior]),
581
+ "No failure branches recorded.",
582
+ )}`,
583
+ `### Observability\n\n${bullets(data.observability, "- No signals recorded that would prove this works.")}`,
584
+ `### Rollout\n\n${text(data.rollout, "No rollout approach recorded.")}`,
585
+ `### Test plan\n\n${link(data.testPlanDoc)}`,
586
+ ),
587
+ ),
588
+ );
589
+ }
590
+
591
+ if (depth === "full") {
592
+ parts.push(
593
+ section(
594
+ "Full",
595
+ doc(
596
+ `### Alternatives and decisions\n\n${table(
597
+ ["Decision", "Status", "Choice"],
598
+ data.decisions.map((d) => [link(d.doc), stat(d.status), d.decision]),
599
+ "No decision records linked.",
600
+ )}`,
601
+ `### Architecture\n\n${text(data.architectureNote, "No architecture note recorded.")}`,
602
+ `### Migrations\n\n${table(
603
+ ["Migration", "Forward", "Rollback", "Status"],
604
+ data.migrations.map((m) => [m.name, m.forward_plan, m.rollback_plan, stat(m.status)]),
605
+ "No schema migrations recorded.",
606
+ )}`,
607
+ `### Security, privacy and compliance\n\n${table(
608
+ ["Entity.field", "Class", "Retention"],
609
+ data.sensitive.map((s) => [s.field, s.sensitivity_class, s.retention_rule]),
610
+ "No sensitive data recorded for this feature.",
611
+ )}`,
612
+ `### Performance and capacity\n\n${table(
613
+ ["Requirement", "Target", "Source", "Status"],
614
+ data.nfrs.map((n) => [n.requirement, n.target, n.target_source, stat(n.status)]),
615
+ "No non-functional requirements scoped to this feature.",
616
+ )}`,
617
+ `### Failure recovery and rollback\n\n${bullets(data.recovery, "- No recovery behavior recorded.")}`,
618
+ `### Compatibility\n\n${bullets(data.compatibility, "- No public contract impact recorded.")}`,
619
+ ),
620
+ ),
621
+ );
622
+ }
623
+
624
+ parts.push(
625
+ section(
626
+ "Delivery state",
627
+ [
628
+ label("What works now", f.what_works_now),
629
+ label("What remains", f.what_remains),
630
+ label("Next action", f.next_action),
631
+ ].join("\n"),
632
+ ),
633
+ );
634
+
635
+ return doc(...parts);
636
+ }
637
+
638
+ // --------------------------------------------------- workflow and state machine
639
+
640
+ export function workflowSpec(data) {
641
+ const w = data.workflow;
642
+ const heading = w ? text(w.name, "Unnamed workflow") : text(data.machines[0]?.machine.entity_name, "State machine");
643
+
644
+ const head = [
645
+ label("Status", stat(w?.status ?? data.machines[0]?.machine.status)),
646
+ label("Module", link(data.moduleDoc)),
647
+ label("Feature", data.featureDoc ? link(data.featureDoc) : "none"),
648
+ VERIFIED,
649
+ ].join("\n");
650
+
651
+ const parts = [`# ${heading}`, head];
652
+
653
+ if (w) {
654
+ const actors = data.actors.map((a) => a.actor);
655
+ const sequence = data.steps.length
656
+ ? [
657
+ "sequenceDiagram",
658
+ ...actors.map((a, i) => ` participant ${nodeId(a, i)} as ${mermaidLabel(a)}`),
659
+ ...data.steps.map((s, i) => {
660
+ const from = nodeId(s.owner_ref || s.owner_type, i);
661
+ const to = nodeId(data.steps[i + 1]?.owner_ref || data.steps[i + 1]?.owner_type || s.owner_ref || s.owner_type, i + 1);
662
+ return ` ${from}->>${to}: ${mermaidLabel(s.action)}`;
663
+ }),
664
+ ].join("\n")
665
+ : null;
666
+
667
+ parts.push(
668
+ section(
669
+ "Workflow",
670
+ doc(
671
+ [
672
+ label("Purpose", w.purpose),
673
+ label("Actors", commas(actors, "none recorded")),
674
+ label("Trigger", w.trigger),
675
+ label("Preconditions", commas(data.preconditions, "none recorded")),
676
+ ].join("\n"),
677
+ table(
678
+ ["Step", "Owner", "Action", "Input", "Expected result", "On failure"],
679
+ data.steps.map((s) => [
680
+ s.sequence, `${text(s.owner_ref, s.owner_type)}`, s.action, s.input_contract,
681
+ s.expected_result, s.failure_behavior,
682
+ ]),
683
+ "No steps recorded.",
684
+ ),
685
+ [label("Completion", w.completion_criteria), label("Observability", w.observability)].join("\n"),
686
+ table(
687
+ ["From step", "Condition", "Goes to", "Type", "Failure policy"],
688
+ data.branches.map((b) => [b.from, b.condition, b.to, b.branch_type, b.failure_policy]),
689
+ "No branches recorded. A workflow with no alternate path is either trivial or unfinished.",
690
+ ),
691
+ sequence ? fence("mermaid", sequence) : "No steps recorded, so no sequence diagram is drawn.",
692
+ ),
693
+ ),
694
+ );
695
+ }
696
+
697
+ for (const m of data.machines) {
698
+ const diagram = [
699
+ "stateDiagram-v2",
700
+ ...m.states.map((s) => ` state "${mermaidLabel(s.name)}" as ${nodeId(s.name, s.sequence)}`),
701
+ m.initialState ? ` [*] --> ${nodeId(m.initialState)}` : null,
702
+ ...m.transitions.map((t) => ` ${nodeId(t.from)} --> ${nodeId(t.to)}: ${mermaidLabel(t.event)}`),
703
+ ...m.states.filter((s) => s.terminal).map((s) => ` ${nodeId(s.name, s.sequence)} --> [*]`),
704
+ ].filter(Boolean).join("\n");
705
+
706
+ parts.push(
707
+ section(
708
+ `State machine: ${text(m.machine.entity_name, "entity")}`,
709
+ doc(
710
+ table(
711
+ ["State", "Meaning", "Permits", "Terminal"],
712
+ m.states.map((s) => [s.name, s.meaning, commas(s.permitted, "nothing recorded"), s.terminal ? "yes" : "no"]),
713
+ "No states recorded.",
714
+ ),
715
+ table(
716
+ ["From", "Event", "Guard", "To", "Actor", "Enforced at"],
717
+ m.transitions.map((t) => [t.from, t.event, t.guard, t.to, t.actor, t.enforcement_point]),
718
+ "No transitions recorded.",
719
+ ),
720
+ [
721
+ `- **Illegal transitions:** rejected at ${commas(m.enforcementPoints, "no enforcement point recorded, which is drift")}.`,
722
+ `- **Terminal states:** ${commas(m.terminalStates, "none recorded")}`,
723
+ `- **Timeouts and expiry:** ${commas(m.timeouts, "none recorded")}`,
724
+ ].join("\n"),
725
+ m.states.length ? fence("mermaid", diagram) : "No states recorded, so no diagram is drawn.",
726
+ ),
727
+ ),
728
+ );
729
+ }
730
+
731
+ if (!w && !data.machines.length) parts.push("No workflow or state machine content recorded.");
732
+ return doc(...parts);
733
+ }
734
+
735
+ // -------------------------------------------------------- surfaces and actions
736
+
737
+ const ACTION_FIELDS = [
738
+ ["Trigger", "trigger"],
739
+ ["Who", "who"],
740
+ ["Precondition", "precondition"],
741
+ ["Effect", "effect"],
742
+ ["Input and validation", "input"],
743
+ ["Side effects", "side_effects"],
744
+ ["Confirmation", "confirmation"],
745
+ ["Loading", "loading_behavior"],
746
+ ["Disabled", "disabled_behavior"],
747
+ ["Success", "success_behavior"],
748
+ ["Empty", "empty"],
749
+ ["Error", "error_behavior"],
750
+ ["Offline", "offline"],
751
+ ["Keyboard", "keyboard"],
752
+ ["Accessible name", "accessible_name"],
753
+ ["Focus behavior", "focus_behavior"],
754
+ ["Responsive", "responsive"],
755
+ ["Telemetry", "telemetry"],
756
+ ["Acceptance test", "acceptance"],
757
+ ];
758
+
759
+ export function surfaceSpec(data) {
760
+ const s = data.surface;
761
+ const roleNames = data.roles.map((r) => r.name);
762
+
763
+ const inventories = data.actions.map((a) =>
764
+ doc(
765
+ `### Action: ${text(a.label ?? a.name, "unnamed action")}`,
766
+ table(["Field", "Value"], ACTION_FIELDS.map(([name, key]) => [name, a[key]]), "Nothing recorded."),
767
+ ),
768
+ );
769
+
770
+ return doc(
771
+ `# Surface: ${text(s.name, "Unnamed surface")}`,
772
+ [
773
+ label("Status", stat(s.status)),
774
+ label("Module", link(data.moduleDoc)),
775
+ label("Feature", data.featureDoc ? link(data.featureDoc) : "none"),
776
+ VERIFIED,
777
+ ].join("\n"),
778
+ section(
779
+ "Page or surface",
780
+ table(
781
+ ["Route", "Surface", "Type", "Purpose", "Primary role", "Key components", "Entities shown"],
782
+ [[s.route, s.name, s.surface_type, s.purpose, s.primary_role, commas(data.components, "none"), commas(data.entities, "none")]],
783
+ ),
784
+ ),
785
+ section("Action inventory", inventories.length ? inventories.join("\n\n") : "No actions recorded."),
786
+ section(
787
+ "Role by action matrix",
788
+ doc(
789
+ table(
790
+ ["Action", ...roleNames],
791
+ data.matrix.map((row) => [row.action, ...roleNames.map((r) => row.access[r] ?? "-")]),
792
+ "No actions or no roles recorded.",
793
+ ),
794
+ "Legend: `full` full access, `own` only own records, `read` read only, `-` blocked. The matrix must agree with the enforcement points above.",
795
+ ),
796
+ ),
797
+ section(
798
+ "State completeness",
799
+ doc(
800
+ table(
801
+ ["State", "Behavior", "Copy"],
802
+ data.states.map((st) => [titleCase(st.state_type), st.behavior, st.copy]),
803
+ "No surface states recorded.",
804
+ ),
805
+ data.missingStates.length
806
+ ? `Undocumented states: ${commas(data.missingStates)}. Each is a gap until it is described or deliberately declined.`
807
+ : "Loading, empty, error and success are all documented.",
808
+ [
809
+ label("Responsive behavior", s.responsive_behavior),
810
+ label("Accessibility notes", s.accessibility_notes),
811
+ ].join("\n"),
812
+ ),
813
+ ),
814
+ );
815
+ }
816
+
817
+ // ------------------------------------------------------------------------ api
818
+
819
+ export function apiSpec(data) {
820
+ const a = data.api;
821
+ return doc(
822
+ `# API: ${text(a.name, "Unnamed operation")}`,
823
+ [
824
+ label("Status", stat(a.status)),
825
+ label("Module", link(data.moduleDoc)),
826
+ label("Feature", data.featureDoc ? link(data.featureDoc) : "none"),
827
+ label("Style", a.style),
828
+ label("Address", `${text(a.method_or_procedure, "")} ${text(a.path_or_topic, "not recorded")}`.trim()),
829
+ label("Implemented at", a.implementation_path),
830
+ VERIFIED,
831
+ ].join("\n"),
832
+ section(
833
+ "Contract",
834
+ [
835
+ label("Purpose", a.purpose),
836
+ label("Callers", links(data.callers, "none recorded")),
837
+ `- **Auth:** ${inline(a.auth_requirement, NOT_RECORDED)}, permission ${inline(a.permission, "not recorded")}, enforced at ${inline(a.enforcement_point, "no enforcement point recorded")}`,
838
+ label("Idempotency", a.idempotency, "no idempotency declared"),
839
+ label("Limits", a.limits, "none declared"),
840
+ label("Side effects", commas(data.sideEffects, "none recorded")),
841
+ label("Versioning", a.versioning),
842
+ ].join("\n"),
843
+ ),
844
+ section(
845
+ "Request",
846
+ table(["Field", "Type", "Required", "Rules"],
847
+ data.request.map((f) => [f.name, f.type, f.required, f.rules]),
848
+ "No request contract recorded."),
849
+ ),
850
+ section(
851
+ "Response",
852
+ table(["Field", "Type", "Notes"],
853
+ data.response.map((f) => [f.name, f.type, f.notes]),
854
+ "No response contract recorded."),
855
+ ),
856
+ section(
857
+ "Errors",
858
+ table(["Code", "Meaning", "User-facing behavior"],
859
+ data.errors.map((e) => [e.code, e.meaning, e.behavior]),
860
+ "No error contract recorded. An operation with no declared failure mode is unfinished."),
861
+ ),
862
+ section(
863
+ "Tests",
864
+ table(["Evidence", "Type", "Result", "Reference"],
865
+ data.tests.map((t) => [t.summary, t.evidence_type, t.result, t.reference]),
866
+ "No test evidence recorded."),
867
+ ),
868
+ );
869
+ }
870
+
871
+ // ---------------------------------------------------------------- data entity
872
+
873
+ const CARDINALITY = { "1:1": "||--||", "1:n": "||--o{", "n:1": "}o--||", "n:m": "}o--o{" };
874
+
875
+ export function dataEntitySpec(data) {
876
+ const e = data.entity;
877
+ const erName = (n) => String(n ?? "ENTITY").replace(/[^A-Za-z0-9_]+/g, "_").toUpperCase().slice(0, 40) || "ENTITY";
878
+ const diagram = [
879
+ "erDiagram",
880
+ ...data.relationships.map((r) =>
881
+ ` ${erName(r.fromName)} ${CARDINALITY[r.cardinality] ?? "||--o{"} ${erName(r.toName)} : ${erName(r.name)}`),
882
+ data.relationships.length ? null : ` ${erName(e.name)} {`,
883
+ data.relationships.length ? null : ` string id`,
884
+ data.relationships.length ? null : ` }`,
885
+ ].filter(Boolean).join("\n");
886
+
887
+ return doc(
888
+ `# Entity: ${text(e.name, "Unnamed entity")}`,
889
+ [
890
+ label("Status", stat(e.status)),
891
+ label("Owning module", link(data.moduleDoc)),
892
+ label("Store", e.store),
893
+ label("Schema source", e.schema_source),
894
+ label("Sensitivity", e.sensitivity_class),
895
+ VERIFIED,
896
+ ].join("\n"),
897
+ section("Purpose", text(e.purpose, "No purpose recorded.")),
898
+ section(
899
+ "Fields",
900
+ table(
901
+ ["Field", "Type", "Null", "Default", "Constraints", "Sensitivity"],
902
+ data.fields.map((f) => [
903
+ f.name, f.type, f.nullable ? "y" : "n", f.default_value, commas(f.constraints, "none"), f.sensitivity_class,
904
+ ]),
905
+ "No fields recorded.",
906
+ ),
907
+ ),
908
+ section(
909
+ "Relationships",
910
+ doc(
911
+ table(
912
+ ["Relation", "Direction", "Target", "Cardinality", "On delete", "Ownership"],
913
+ data.relationships.map((r) => [r.name, r.direction, r.target, r.cardinality, r.on_delete, r.ownership_note]),
914
+ "No relationships recorded.",
915
+ ),
916
+ fence("mermaid", diagram),
917
+ ),
918
+ ),
919
+ section(
920
+ "Lifecycle",
921
+ [
922
+ label("Created by", links(data.createdBy, "no operation recorded")),
923
+ label("Read or updated by", links(data.updatedBy, "no operation recorded")),
924
+ label("Deleted", e.deletion_semantics),
925
+ label("Retention", e.retention_rule, "none declared"),
926
+ ].join("\n"),
927
+ ),
928
+ section("Indexes and uniqueness", bullets(data.constraints, "- None recorded. The schema source outranks this prose.")),
929
+ section(
930
+ "Migration notes",
931
+ table(
932
+ ["Migration", "Forward", "Rollback", "Compatibility", "Status"],
933
+ data.migrations.map((m) => [m.name, m.forward_plan, m.rollback_plan, m.compatibility_notes, stat(m.status)]),
934
+ "No migrations affect this entity.",
935
+ ),
936
+ ),
937
+ );
938
+ }
939
+
940
+ // ---------------------------------------------------------------- integration
941
+
942
+ export function integrationSpec(data) {
943
+ const i = data.integration;
944
+ return doc(
945
+ `# Integration: ${text(i.name, "Unnamed integration")}`,
946
+ [
947
+ label("Status", stat(i.status)),
948
+ label("Module", link(data.moduleDoc)),
949
+ label("Feature", data.featureDoc ? link(data.featureDoc) : "none"),
950
+ label("Provider", i.provider),
951
+ label("Configuration", stat(i.configuration_status)),
952
+ label("Verification", stat(i.verification_status)),
953
+ VERIFIED,
954
+ ].join("\n"),
955
+ section("Purpose", text(i.purpose, "No purpose recorded.")),
956
+ section(
957
+ "Configuration",
958
+ [
959
+ label("Environments", commas(data.environments, "none recorded")),
960
+ label("Auth approach", i.auth_approach),
961
+ label("Contract references", commas(data.contractRefs, "none recorded")),
962
+ ].join("\n") +
963
+ "\n\nEnvironment names only. A credential value never appears in a generated file.",
964
+ ),
965
+ section("Failure behavior", text(i.failure_behavior, "No failure behavior recorded. An integration with no declared failure mode is unfinished.")),
966
+ section(
967
+ "Verification",
968
+ table(
969
+ ["Evidence", "Type", "Result", "Reference"],
970
+ data.evidence.map((e) => [e.summary, e.evidence_type, e.result, e.reference]),
971
+ `Status is ${stat(i.verification_status)} with no evidence recorded.`,
972
+ ),
973
+ ),
974
+ );
975
+ }
976
+
977
+ // ----------------------------------------------------------- jobs and webhooks
978
+
979
+ export function asyncSpec(data) {
980
+ const j = data.job;
981
+ const w = data.webhook;
982
+ const head = [
983
+ label("Status", stat((j ?? w).status)),
984
+ label("Module", link(data.moduleDoc)),
985
+ label("Feature", data.featureDoc ? link(data.featureDoc) : "none"),
986
+ label("Mechanism", data.mechanism, "none declared"),
987
+ VERIFIED,
988
+ ].join("\n");
989
+
990
+ if (j) {
991
+ return doc(
992
+ `# Job: ${text(j.name, "Unnamed job")}`,
993
+ head,
994
+ section(
995
+ "Async job",
996
+ [
997
+ label("Trigger", j.trigger),
998
+ label("Input", commas(data.input, "no contract recorded")),
999
+ label("Idempotency", j.idempotency, "not declared, so re-running is unsafe until it is"),
1000
+ label("Retry", j.retry_policy),
1001
+ label("Failure destination", j.failure_destination),
1002
+ label("Timeout", j.timeout),
1003
+ label("Concurrency", j.concurrency),
1004
+ label("Observability", j.observability, "no way to notice a stuck run is recorded"),
1005
+ label("Delivery guarantee", j.delivery_guarantee, "undeclared"),
1006
+ ].join("\n"),
1007
+ ),
1008
+ );
1009
+ }
1010
+
1011
+ const incoming = w.direction === "incoming";
1012
+ return doc(
1013
+ `# Webhook: ${text(w.name, "Unnamed webhook")}`,
1014
+ head,
1015
+ section(
1016
+ incoming ? "Incoming webhook" : "Outgoing webhook",
1017
+ incoming
1018
+ ? [
1019
+ label("Endpoint", w.endpoint_or_registration),
1020
+ label("Sender verification", w.identity_verification),
1021
+ label("Replay protection", w.replay_protection),
1022
+ label("Ordering", w.ordering_behavior),
1023
+ label("Failure semantics", w.retry_behavior),
1024
+ label("Payload versioning", w.payload_version),
1025
+ ].join("\n")
1026
+ : [
1027
+ label("Registration", w.endpoint_or_registration),
1028
+ label("Delivery", w.retry_behavior),
1029
+ label("Signing", w.signing),
1030
+ label("Failure visibility", w.failure_visibility),
1031
+ label("Payload versioning", w.payload_version),
1032
+ ].join("\n"),
1033
+ ),
1034
+ );
1035
+ }
1036
+
1037
+ // -------------------------------------------------------------- observability
1038
+
1039
+ export function observabilitySpec(data) {
1040
+ return doc(
1041
+ `# ${text(data.scopeName, "Project")} - Observability`,
1042
+ [
1043
+ label("Tooling in use", commas(data.tooling, "none declared")),
1044
+ VERIFIED,
1045
+ ].join("\n"),
1046
+ section(
1047
+ "Signals that prove it works",
1048
+ table(
1049
+ ["Capability", "Signal", "Where emitted"],
1050
+ data.signals.map((s) => [s.capability, s.signal, s.where]),
1051
+ "No signals recorded. Nothing here proves the module works in production.",
1052
+ ),
1053
+ ),
1054
+ section(
1055
+ "Operator views",
1056
+ table(["View", "Shows", "Exists at"], data.views.map((v) => [v.view, v.shows, v.where]), "No operator views recorded."),
1057
+ ),
1058
+ section(
1059
+ "Alerts",
1060
+ table(
1061
+ ["Condition", "Threshold", "Who is notified"],
1062
+ data.alerts.map((a) => [a.condition, a.threshold, a.destination]),
1063
+ "No alerts recorded.",
1064
+ ),
1065
+ ),
1066
+ section(
1067
+ "First-response pointers",
1068
+ bullets(
1069
+ data.firstResponse.map((f) => `When ${f.failure}, look at ${f.signal} first.`),
1070
+ "- None recorded.",
1071
+ ),
1072
+ ),
1073
+ );
1074
+ }
1075
+
1076
+ // ------------------------------------------------------------------ test plan
1077
+
1078
+ export function testPlan(data) {
1079
+ return doc(
1080
+ `# ${text(data.module.name, "Module")} - Test Plan`,
1081
+ [label("Test tooling in use", commas(data.tooling, "not recorded")), VERIFIED].join("\n"),
1082
+ section(
1083
+ "What must be true",
1084
+ table(
1085
+ ["Feature", "Criterion", "Verified how", "Status"],
1086
+ data.acceptance.map((a) => [a.feature, a.criterion, a.verification_method, stat(a.status)]),
1087
+ "No acceptance criteria recorded for this module.",
1088
+ ),
1089
+ ),
1090
+ section(
1091
+ "Coverage map",
1092
+ table(
1093
+ ["Area", "Level", "Cases", "Status"],
1094
+ data.coverage.map((c) => [c.area, c.level, c.cases, c.status]),
1095
+ "No coverage recorded.",
1096
+ ),
1097
+ ),
1098
+ section(
1099
+ "Evidence conventions",
1100
+ doc(
1101
+ "A claim of tested cites a run. Tests claimed but absent is a parity finding, not a rounding error.",
1102
+ table(
1103
+ ["Evidence", "Type", "Result", "Reference", "State"],
1104
+ data.evidence.map((e) => [e.summary, e.evidence_type, e.result, e.reference, stat(e.status)]),
1105
+ "No verification evidence recorded.",
1106
+ ),
1107
+ ),
1108
+ ),
1109
+ );
1110
+ }
1111
+
1112
+ // ------------------------------------------------------------------------ adr
1113
+
1114
+ export function adr(data) {
1115
+ const d = data.decision;
1116
+ return doc(
1117
+ `# ${d.id}: ${text(d.title, "Untitled decision")}`,
1118
+ [
1119
+ label("Status", stat(d.status)),
1120
+ label("Date", data.date, "not recorded"),
1121
+ label("Owner or approver", d.accepted_by),
1122
+ label("Scope", `${text(d.scope_type, "project")} ${text(d.scope_id, "")}`.trim()),
1123
+ label("Supersedes", links(data.supersedes, "none")),
1124
+ label("Superseded by", links(data.supersededBy, "none")),
1125
+ label("Partially supersedes", links(data.partiallySupersedes, "none")),
1126
+ label("Expiry", d.expires_at, "none"),
1127
+ VERIFIED,
1128
+ ].join("\n"),
1129
+ section("Context", text(d.context, "No context recorded.")),
1130
+ section(
1131
+ "Evidence",
1132
+ bullets(data.evidence.map((e) => (typeof e === "string" ? e : `${text(e.claim ?? e.summary)} (${text(e.label ?? e.epistemic_status, "unlabelled")}, ${text(e.source, "no source")})`)), "- None recorded."),
1133
+ ),
1134
+ section("Decision criteria", bullets(data.criteria, "- None recorded.")),
1135
+ section(
1136
+ "Options considered",
1137
+ data.options.length
1138
+ ? data.options
1139
+ .map((o, i) =>
1140
+ `${i + 1}. **${text(o.name ?? o.option, "Option")}** - ${text(o.pros, "no benefits recorded")} / ${text(o.cons, "no costs recorded")}${o.why_not ? ` / rejected because ${o.why_not}` : ""}`)
1141
+ .join("\n")
1142
+ : "None recorded.",
1143
+ ),
1144
+ section("Decision", text(d.decision, "No decision recorded.")),
1145
+ section("Rationale", text(d.observable_rationale, "No observable rationale recorded.")),
1146
+ section(
1147
+ "Consequences",
1148
+ [
1149
+ `- Positive: ${commas(data.consequences.positive, "none recorded")}`,
1150
+ `- Negative: ${commas(data.consequences.negative, "none recorded")}`,
1151
+ `- Neutral: ${commas(data.consequences.neutral, "none recorded")}`,
1152
+ ].join("\n"),
1153
+ ),
1154
+ section("Risks", bullets(data.risks, "- None recorded.")),
1155
+ section("Enforcement", bullets(data.enforcement, "- Not enforced anywhere, which makes this a preference rather than a decision.")),
1156
+ section("Verification", text(d.verification, "No verification recorded.")),
1157
+ section("Revisit triggers", bullets(data.revisitTriggers, "- None recorded.")),
1158
+ section("Related", links(data.related, "Nothing linked.")),
1159
+ section(
1160
+ "History",
1161
+ table(
1162
+ ["#", "From", "To", "Scope delta", "Reason"],
1163
+ data.transitions.map((t) => [t.sequence, t.from_status, t.to_status, t.scope_delta, t.reason]),
1164
+ "No transitions recorded.",
1165
+ ),
1166
+ ),
1167
+ );
1168
+ }
1169
+
1170
+ // -------------------------------------------------------------- derived views
1171
+
1172
+ export function changelog(data) {
1173
+ return doc(
1174
+ REGENERATED,
1175
+ `# ${text(data.project.name, "Project")} - Changelog`,
1176
+ "Specification and decision changes, newest first, taken from the append-only activity log. Task and session traffic stays in the control center.",
1177
+ table(
1178
+ ["#", "Date", "Change", "Actor"],
1179
+ data.entries.map((e) => [e.sequence, e.date, e.summary, e.actor]),
1180
+ "No changes recorded yet.",
1181
+ ),
1182
+ );
1183
+ }
1184
+
1185
+ export function projectSummary(data) {
1186
+ return doc(
1187
+ REGENERATED,
1188
+ `# ${text(data.project.name, "Project")} - Project Summary`,
1189
+ "- **Method:** database-first read. Every claim below is a stored record, and anything unestablished is listed at the end rather than guessed.",
1190
+ section("What it is", text(data.project.statement, "No product statement recorded.")),
1191
+ section(
1192
+ "Who uses it",
1193
+ table(["Role", "Goal", "Evidence"], data.roles.map((r) => [r.name, r.primary_goals, r.who]), "No roles recorded."),
1194
+ ),
1195
+ section(
1196
+ "What it does",
1197
+ table(
1198
+ ["Module", "Capabilities", "Evidence"],
1199
+ data.modules.map((m) => [m.name, commas(m.features, "no features recorded"), link(m.doc)]),
1200
+ "No modules recorded.",
1201
+ ),
1202
+ ),
1203
+ section(
1204
+ "How it is built",
1205
+ table(["Capability", "Choice", "Evidence"], data.stack.map((s) => [s.area, s.choice, s.evidence]), "No stack slots recorded."),
1206
+ ),
1207
+ section(
1208
+ "How it runs",
1209
+ [
1210
+ label("Runtime pieces", commas(data.pieces, "none recorded")),
1211
+ label("Environments", commas(data.environments, "none recorded")),
1212
+ label("Integrations", commas(data.integrations, "none recorded")),
1213
+ ].join("\n"),
1214
+ ),
1215
+ section(
1216
+ "State of the project",
1217
+ table(["Measure", "Value"], data.state.map((s) => [s.measure, s.value]), "Nothing measured."),
1218
+ ),
1219
+ section(
1220
+ "Could not be established",
1221
+ bullets(
1222
+ data.unknowns.map((u) => `${u.item} - ${text(u.resolution, "no resolution path recorded")}`),
1223
+ "- Nothing outstanding.",
1224
+ ),
1225
+ ),
1226
+ );
1227
+ }
1228
+
1229
+ export function statusReport(data) {
1230
+ return doc(
1231
+ REGENERATED,
1232
+ `# ${text(data.project.name, "Project")} - Status`,
1233
+ "Counts only. Individual tasks, sessions and assignments live in the database and the control center, never in a file.",
1234
+ section("Headline", table(["Measure", "Value"], data.headline.map((h) => [h.measure, h.value]), "Nothing recorded.")),
1235
+ section(
1236
+ "Milestones",
1237
+ table(["Milestone", "Status", "Features", "Complete"], data.milestones.map((m) => [m.name, stat(m.status), m.features, m.complete]), "No milestones recorded."),
1238
+ ),
1239
+ section(
1240
+ "Modules",
1241
+ table(["Module", "Status", "Features", "Steps filled"], data.modules.map((m) => [m.name, stat(m.status), m.features, m.steps]), "No modules recorded."),
1242
+ ),
1243
+ section(
1244
+ "Features needing attention",
1245
+ table(["Feature", "Status", "Next action"], data.attention.map((f) => [link(f.doc), stat(f.status), f.next_action]), "Nothing is waiting."),
1246
+ ),
1247
+ section(
1248
+ "Open questions",
1249
+ table(["Question", "Why it matters", "Scope"], data.questions.map((q) => [q.question, q.why_it_matters, q.scope]), "No open questions."),
1250
+ ),
1251
+ );
1252
+ }
1253
+
1254
+ export function driftReport(data) {
1255
+ return doc(
1256
+ REGENERATED,
1257
+ `# ${text(data.project.name, "Project")} - Change Impact and Drift`,
1258
+ "- **Scope:** the whole project database and the documents generated from it.",
1259
+ section(
1260
+ "Change impact",
1261
+ table(
1262
+ ["Document", "Sync state", "Scope", "Action"],
1263
+ data.impact.map((i) => [link(i.doc), stat(i.sync_status), i.scope, i.action]),
1264
+ "Every generated document is in sync.",
1265
+ ),
1266
+ ),
1267
+ section(
1268
+ "Drift findings",
1269
+ table(
1270
+ ["#", "Class", "Database side", "Document side", "Resolution owner"],
1271
+ data.findings.map((f, i) => [i + 1, f.class, f.database, f.document, f.owner]),
1272
+ "No drift found.",
1273
+ ),
1274
+ ),
1275
+ section(
1276
+ "Contradictions still open",
1277
+ table(["Sides", "Detail", "Blocking"], data.contradictions.map((c) => [c.sides, c.detail, c.blocking]), "None open."),
1278
+ ),
1279
+ section("Pending sync", bullets(data.pending, "- Nothing pending.")),
1280
+ );
1281
+ }