@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1 @@
1
+ export { normalizeStatement } from "./index.js";
@@ -0,0 +1 @@
1
+ export * from "./resolver/index.js";
@@ -0,0 +1,65 @@
1
+ // `sdlc adopt` — brownfield onramp.
2
+ //
3
+ // Sets up the SDLC folder skeleton inside an existing topogram workspace
4
+ // without backfilling historical artifacts. After running, an author can
5
+ // `topogram sdlc new pitch <slug>` to start adding artifacts immediately.
6
+
7
+ import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const SDLC_FOLDERS = [
11
+ "pitches",
12
+ "requirements",
13
+ "acceptance_criteria",
14
+ "tasks",
15
+ "bugs",
16
+ "_archive"
17
+ ];
18
+
19
+ function ensureFolder(root, name) {
20
+ const dir = path.join(root, "topogram", name);
21
+ if (!existsSync(dir)) {
22
+ mkdirSync(dir, { recursive: true });
23
+ return { name, created: true };
24
+ }
25
+ return { name, created: false };
26
+ }
27
+
28
+ function scanPressure(root) {
29
+ // Pressure scan: count statements per kind so the operator knows the
30
+ // workspace's current shape. We do not attempt to backfill — that's the
31
+ // operator's job after they pick a starting point.
32
+ const tg = path.join(root, "topogram");
33
+ if (!existsSync(tg)) return { error: `No 'topogram/' directory found at ${root}` };
34
+ let totalFiles = 0;
35
+ function walk(dir) {
36
+ for (const entry of readdirSync(dir)) {
37
+ const full = path.join(dir, entry);
38
+ const st = statSync(full);
39
+ if (st.isDirectory()) {
40
+ if (entry === "_archive" || entry.startsWith(".")) continue;
41
+ walk(full);
42
+ } else if (entry.endsWith(".tg")) {
43
+ totalFiles += 1;
44
+ }
45
+ }
46
+ }
47
+ walk(tg);
48
+ return { totalFiles };
49
+ }
50
+
51
+ export function sdlcAdopt(workspaceRoot) {
52
+ const root = path.resolve(workspaceRoot);
53
+ if (!existsSync(path.join(root, "topogram"))) {
54
+ return { ok: false, error: `No 'topogram/' directory at ${root}; run 'topogram new' first` };
55
+ }
56
+ const folders = SDLC_FOLDERS.map((name) => ensureFolder(root, name));
57
+ const pressure = scanPressure(root);
58
+ return {
59
+ ok: true,
60
+ workspaceRoot: root,
61
+ folders_created: folders.filter((f) => f.created).map((f) => f.name),
62
+ folders_existing: folders.filter((f) => !f.created).map((f) => f.name),
63
+ pressure
64
+ };
65
+ }
@@ -0,0 +1,86 @@
1
+ // `sdlc check` — workspace-wide SDLC sanity scan.
2
+ //
3
+ // Surfaces:
4
+ // * Status drift (current status doesn't match last history entry)
5
+ // * DoD violations on currently-recorded status
6
+ // * Stale documents (linked component changed since publish)
7
+ // * Reciprocal blocks/blocked_by mismatches
8
+ //
9
+ // Returns { ok, errors, warnings } so the CLI can exit non-zero in
10
+ // `--strict` mode.
11
+
12
+ import { checkDoD } from "./dod/index.js";
13
+ import { detectDriftedStatus, readHistory } from "./history.js";
14
+
15
+ const SDLC_KINDS = new Set([
16
+ "pitch",
17
+ "requirement",
18
+ "acceptance_criterion",
19
+ "task",
20
+ "bug"
21
+ ]);
22
+
23
+ function checkBlockingReciprocity(graph) {
24
+ const warnings = [];
25
+ const tasks = (graph.byKind?.task || []).filter((task) => !task.archived);
26
+ const tasksById = new Map(tasks.map((t) => [t.id, t]));
27
+ for (const task of tasks) {
28
+ for (const ref of task.blocks || []) {
29
+ const targetId = typeof ref === "string" ? ref : ref?.id;
30
+ const target = tasksById.get(targetId);
31
+ if (!target) continue;
32
+ const reciprocal = (target.blockedBy || []).some((r) => (typeof r === "string" ? r : r?.id) === task.id);
33
+ if (!reciprocal) {
34
+ warnings.push({
35
+ id: task.id,
36
+ message: `task ${task.id} blocks ${target.id}, but ${target.id} does not list ${task.id} in blocked_by`
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return warnings;
42
+ }
43
+
44
+ export function checkWorkspace(workspaceRoot, resolved) {
45
+ const errors = [];
46
+ const warnings = [];
47
+ const history = readHistory(workspaceRoot);
48
+ if (history.__error) {
49
+ warnings.push({ message: `cannot read SDLC history sidecar: ${history.__error}` });
50
+ }
51
+
52
+ const byId = new Map(resolved.graph.statements.map((s) => [s.id, s]));
53
+
54
+ for (const statement of resolved.graph.statements) {
55
+ if (statement.archived) continue;
56
+ if (!SDLC_KINDS.has(statement.kind)) continue;
57
+
58
+ const drift = detectDriftedStatus(history, statement);
59
+ if (drift) {
60
+ warnings.push({
61
+ id: statement.id,
62
+ message: `status drift: history records '${drift.historyStatus}' but current is '${drift.currentStatus}'`
63
+ });
64
+ }
65
+
66
+ // Re-run DoD against the *current* status to surface "approved without
67
+ // ACs" or similar ongoing violations.
68
+ const dod = checkDoD(statement.kind, statement, statement.status, { byId });
69
+ for (const err of dod.errors) {
70
+ errors.push({ id: statement.id, message: `DoD: ${err}` });
71
+ }
72
+ for (const warn of dod.warnings) {
73
+ warnings.push({ id: statement.id, message: `DoD: ${warn}` });
74
+ }
75
+ }
76
+
77
+ for (const w of checkBlockingReciprocity(resolved.graph)) {
78
+ warnings.push(w);
79
+ }
80
+
81
+ return {
82
+ ok: errors.length === 0,
83
+ errors,
84
+ warnings
85
+ };
86
+ }
@@ -0,0 +1,22 @@
1
+ // Acceptance criterion DoD per status.
2
+
3
+ export function checkDoD(ac, targetStatus, graph) {
4
+ const errors = [];
5
+ const warnings = [];
6
+
7
+ if (!ac.requirement) {
8
+ errors.push("acceptance_criterion must reference a requirement");
9
+ }
10
+
11
+ if (targetStatus === "approved") {
12
+ const byId = graph?.byId;
13
+ if (byId && ac.requirement?.id) {
14
+ const req = byId.get(ac.requirement.id);
15
+ if (req && req.status === "draft") {
16
+ warnings.push(`acceptance_criterion approved while parent requirement '${req.id}' is still draft`);
17
+ }
18
+ }
19
+ }
20
+
21
+ return { satisfied: errors.length === 0, errors, warnings };
22
+ }
@@ -0,0 +1,26 @@
1
+ // Bug DoD per status.
2
+
3
+ export function checkDoD(bug, targetStatus, graph) {
4
+ const errors = [];
5
+ const warnings = [];
6
+
7
+ if (targetStatus === "fixed" || targetStatus === "verified") {
8
+ if (!bug.fixedIn || bug.fixedIn.length === 0) {
9
+ errors.push(`status '${targetStatus}' requires field 'fixed_in' (the task that fixed it)`);
10
+ }
11
+ }
12
+
13
+ if (targetStatus === "verified") {
14
+ if (!bug.fixedInVerification || bug.fixedInVerification.length === 0) {
15
+ errors.push("verified bug must reference 'fixed_in_verification' (the verification that proved the fix)");
16
+ }
17
+ }
18
+
19
+ if (targetStatus === "wont-fix") {
20
+ if (!bug.reproduction) {
21
+ warnings.push("wont-fix bug without `reproduction` is hard to revisit later");
22
+ }
23
+ }
24
+
25
+ return { satisfied: errors.length === 0, errors, warnings };
26
+ }
@@ -0,0 +1,23 @@
1
+ // Document DoD per status. Documents are markdown-only, so the input here
2
+ // is the normalized doc record from `engine/src/workspace-docs.js`.
3
+
4
+ export function checkDoD(doc, targetStatus, graph) {
5
+ const errors = [];
6
+ const warnings = [];
7
+
8
+ if (targetStatus === "review" || targetStatus === "published") {
9
+ if (!doc.title) errors.push("document must have a title");
10
+ if (!doc.summary) warnings.push("document should have a summary for indexing");
11
+ }
12
+
13
+ if (targetStatus === "published") {
14
+ if (!doc.appVersion && !doc.metadata?.app_version) {
15
+ warnings.push("published document should record `app_version` to anchor staleness");
16
+ }
17
+ if (doc.confidence === "low") {
18
+ warnings.push("publishing a low-confidence document — consider review first");
19
+ }
20
+ }
21
+
22
+ return { satisfied: errors.length === 0, errors, warnings };
23
+ }
@@ -0,0 +1,25 @@
1
+ // Per-kind dispatch for DoD checks.
2
+
3
+ import { checkDoD as checkPitch } from "./pitch.js";
4
+ import { checkDoD as checkRequirement } from "./requirement.js";
5
+ import { checkDoD as checkAcceptanceCriterion } from "./acceptance-criterion.js";
6
+ import { checkDoD as checkTask } from "./task.js";
7
+ import { checkDoD as checkBug } from "./bug.js";
8
+ import { checkDoD as checkDocument } from "./document.js";
9
+
10
+ const CHECKS = {
11
+ pitch: checkPitch,
12
+ requirement: checkRequirement,
13
+ acceptance_criterion: checkAcceptanceCriterion,
14
+ task: checkTask,
15
+ bug: checkBug,
16
+ document: checkDocument
17
+ };
18
+
19
+ export function checkDoD(kind, statement, targetStatus, graph) {
20
+ const check = CHECKS[kind];
21
+ if (!check) {
22
+ return { satisfied: true, errors: [], warnings: [] };
23
+ }
24
+ return check(statement, targetStatus, graph);
25
+ }
@@ -0,0 +1,23 @@
1
+ // Pitch DoD per status.
2
+ //
3
+ // Returns { satisfied, errors, warnings }. Errors block the transition;
4
+ // warnings advise the author/agent.
5
+
6
+ export function checkDoD(pitch, targetStatus, graph) {
7
+ const errors = [];
8
+ const warnings = [];
9
+
10
+ if (targetStatus === "shaped" || targetStatus === "submitted" || targetStatus === "approved") {
11
+ if (!pitch.appetite) errors.push("appetite must be filled before leaving draft");
12
+ if (!pitch.problem) errors.push("problem must be filled before leaving draft");
13
+ if (!pitch.solutionSketch) warnings.push("solution_sketch is recommended");
14
+ }
15
+
16
+ if (targetStatus === "approved") {
17
+ if (!pitch.affects || pitch.affects.length === 0) {
18
+ warnings.push("approved pitch has no `affects` references (no downstream impact recorded)");
19
+ }
20
+ }
21
+
22
+ return { satisfied: errors.length === 0, errors, warnings };
23
+ }
@@ -0,0 +1,34 @@
1
+ // Requirement DoD per status.
2
+
3
+ export function checkDoD(requirement, targetStatus, graph) {
4
+ const errors = [];
5
+ const warnings = [];
6
+
7
+ if (targetStatus === "in-review" || targetStatus === "approved") {
8
+ if (!requirement.affects || requirement.affects.length === 0) {
9
+ errors.push("requirement must list at least one `affects` target before review");
10
+ }
11
+ }
12
+
13
+ if (targetStatus === "approved") {
14
+ if (!requirement.acceptanceCriteria || requirement.acceptanceCriteria.length === 0) {
15
+ errors.push("approved requirement must have at least one acceptance_criterion");
16
+ }
17
+ const acs = requirement.acceptanceCriteria || [];
18
+ const byId = graph?.byId;
19
+ if (byId) {
20
+ const undraftedAcs = acs.filter((id) => byId.get(id)?.status === "draft");
21
+ if (undraftedAcs.length > 0) {
22
+ warnings.push(`approved requirement has draft ACs: ${undraftedAcs.join(", ")}`);
23
+ }
24
+ }
25
+ }
26
+
27
+ if (targetStatus === "superseded") {
28
+ if (!requirement.supersedes || requirement.supersedes.length === 0) {
29
+ warnings.push("superseded status without listing what supersedes it loses traceability");
30
+ }
31
+ }
32
+
33
+ return { satisfied: errors.length === 0, errors, warnings };
34
+ }
@@ -0,0 +1,39 @@
1
+ // Task DoD per status.
2
+
3
+ export function checkDoD(task, targetStatus, graph) {
4
+ const errors = [];
5
+ const warnings = [];
6
+
7
+ if (targetStatus === "claimed" || targetStatus === "in-progress" || targetStatus === "done") {
8
+ if (!task.claimedBy || task.claimedBy.length === 0) {
9
+ errors.push(`status '${targetStatus}' requires field 'claimed_by'`);
10
+ }
11
+ }
12
+
13
+ if (targetStatus === "in-progress") {
14
+ const byId = graph?.byId;
15
+ const blockers = task.blockedBy || [];
16
+ if (byId && blockers.length > 0) {
17
+ const stillBlocking = blockers
18
+ .map((ref) => byId.get(typeof ref === "string" ? ref : ref?.id))
19
+ .filter((b) => b && b.status !== "done");
20
+ if (stillBlocking.length > 0) {
21
+ errors.push(
22
+ `cannot start work — blocked_by tasks not yet done: ${stillBlocking.map((b) => b.id).join(", ")}`
23
+ );
24
+ }
25
+ }
26
+ }
27
+
28
+ if (targetStatus === "done") {
29
+ if (!task.satisfies || task.satisfies.length === 0) {
30
+ warnings.push("done task without `satisfies` references is hard to trace");
31
+ }
32
+ const acs = task.acceptanceRefs || [];
33
+ if (acs.length === 0 && task.workType !== "documentation" && task.workType !== "review") {
34
+ warnings.push("done task without `acceptance_refs` cannot tie back to verification");
35
+ }
36
+ }
37
+
38
+ return { satisfied: errors.length === 0, errors, warnings };
39
+ }
@@ -0,0 +1,116 @@
1
+ // `sdlc explain <id>` — agent-facing inspector.
2
+ //
3
+ // Returns a structured payload an agent can script against. The
4
+ // `next_action` shape is intentionally stable across versions:
5
+ //
6
+ // {
7
+ // kind: "transition" | "work" | "wait" | "review" | "none",
8
+ // to?: <statusName>, // when kind === "transition"
9
+ // reason: <human-readable>,
10
+ // blockers?: [<id>, ...] // when kind === "wait"
11
+ // }
12
+
13
+ import { checkDoD } from "./dod/index.js";
14
+ import { legalTransitionsFor, isTerminalStatus } from "./transitions/index.js";
15
+ import { defaultActiveStatuses } from "./status-filter.js";
16
+ import { readHistory, lastTransition, detectDriftedStatus } from "./history.js";
17
+
18
+ function pickNextStatus(legal) {
19
+ // Prefer the canonical forward path (skipping rollback options).
20
+ const FORWARD_BIAS = [
21
+ "in-review",
22
+ "approved",
23
+ "submitted",
24
+ "shaped",
25
+ "claimed",
26
+ "in-progress",
27
+ "done",
28
+ "fixed",
29
+ "verified",
30
+ "review",
31
+ "published"
32
+ ];
33
+ for (const candidate of FORWARD_BIAS) {
34
+ if (legal.includes(candidate)) return candidate;
35
+ }
36
+ return legal[0] || null;
37
+ }
38
+
39
+ function buildBlockers(statement, byId) {
40
+ if (statement.kind !== "task") return [];
41
+ const blockers = statement.blockedBy || [];
42
+ return blockers
43
+ .map((ref) => {
44
+ const id = typeof ref === "string" ? ref : ref?.id;
45
+ const target = byId.get(id);
46
+ return target && target.status !== "done" ? id : null;
47
+ })
48
+ .filter(Boolean);
49
+ }
50
+
51
+ export function explain(workspaceRoot, resolved, id, options = {}) {
52
+ const statement = resolved.graph.statements.find((s) => s.id === id);
53
+ if (!statement) {
54
+ return { ok: false, error: `Statement '${id}' not found` };
55
+ }
56
+
57
+ const byId = new Map(resolved.graph.statements.map((s) => [s.id, s]));
58
+ const history = readHistory(workspaceRoot);
59
+ const last = lastTransition(history, id);
60
+ const drift = detectDriftedStatus(history, statement);
61
+ const blockers = buildBlockers(statement, byId);
62
+
63
+ const legal = legalTransitionsFor(statement.kind, statement.status);
64
+ const nextStatus = pickNextStatus(legal);
65
+ const dod = nextStatus ? checkDoD(statement.kind, statement, nextStatus, { byId }) : { satisfied: true, errors: [], warnings: [] };
66
+ const dodCurrent = checkDoD(statement.kind, statement, statement.status, { byId });
67
+
68
+ let nextAction;
69
+ if (blockers.length > 0) {
70
+ nextAction = {
71
+ kind: "wait",
72
+ reason: `blocked_by tasks not yet done`,
73
+ blockers
74
+ };
75
+ } else if (drift) {
76
+ nextAction = {
77
+ kind: "review",
78
+ reason: `status drift: history says '${drift.historyStatus}', current says '${drift.currentStatus}'`
79
+ };
80
+ } else if (!dodCurrent.satisfied) {
81
+ nextAction = {
82
+ kind: "work",
83
+ reason: `current-status DoD failing: ${dodCurrent.errors.join("; ")}`
84
+ };
85
+ } else if (isTerminalStatus(statement.kind, statement.status)) {
86
+ nextAction = { kind: "none", reason: "terminal status" };
87
+ } else if (nextStatus && dod.satisfied) {
88
+ nextAction = {
89
+ kind: "transition",
90
+ to: nextStatus,
91
+ reason: `forward path is open (${statement.status} → ${nextStatus})`
92
+ };
93
+ } else if (nextStatus) {
94
+ nextAction = {
95
+ kind: "work",
96
+ reason: `next status '${nextStatus}' DoD not satisfied: ${dod.errors.join("; ")}`
97
+ };
98
+ } else {
99
+ nextAction = { kind: "none", reason: "no legal forward transitions" };
100
+ }
101
+
102
+ return {
103
+ ok: true,
104
+ id: statement.id,
105
+ kind: statement.kind,
106
+ status: statement.status,
107
+ legal_transitions: legal,
108
+ dod_current: dodCurrent,
109
+ dod_next: nextStatus ? { target: nextStatus, ...dod } : null,
110
+ last_transition: last,
111
+ drift,
112
+ blockers,
113
+ next_action: nextAction,
114
+ history: options.includeHistory ? history[id] || [] : undefined
115
+ };
116
+ }
@@ -0,0 +1,80 @@
1
+ // SDLC history sidecar.
2
+ //
3
+ // Stored at `<topogram-root>/.topogram-sdlc-history.json` as a
4
+ // JSON object keyed by statement id. Each entry is an append-only array of
5
+ // transition records:
6
+ //
7
+ // {
8
+ // "task_audit_logging": [
9
+ // { "from": "claimed", "to": "in-progress", "at": "2026-05-01T12:00:00Z", "by": "agent-7", "note": "..." },
10
+ // { "from": "in-progress", "to": "done", "at": "2026-05-02T09:30:00Z", "by": "agent-7" }
11
+ // ]
12
+ // }
13
+ //
14
+ // `topogram check` consults this file to surface "status edited outside the
15
+ // CLI" warnings when an artifact's current status doesn't match the last
16
+ // recorded transition.
17
+
18
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
19
+ import path from "node:path";
20
+ import { topogramRootForSdlc } from "./paths.js";
21
+
22
+ const HISTORY_FILENAME = ".topogram-sdlc-history.json";
23
+
24
+ export function historyPath(workspaceRoot) {
25
+ return path.join(topogramRootForSdlc(workspaceRoot), HISTORY_FILENAME);
26
+ }
27
+
28
+ export function readHistory(workspaceRoot) {
29
+ const file = historyPath(workspaceRoot);
30
+ if (!existsSync(file)) return {};
31
+ try {
32
+ return JSON.parse(readFileSync(file, "utf8"));
33
+ } catch (err) {
34
+ return { __error: err.message };
35
+ }
36
+ }
37
+
38
+ export function writeHistory(workspaceRoot, history) {
39
+ const file = historyPath(workspaceRoot);
40
+ writeFileSync(file, JSON.stringify(history, null, 2) + "\n", "utf8");
41
+ }
42
+
43
+ export function appendTransition(workspaceRoot, id, record) {
44
+ const history = readHistory(workspaceRoot);
45
+ if (history.__error) {
46
+ throw new Error(`Cannot read history file: ${history.__error}`);
47
+ }
48
+ if (!history[id]) history[id] = [];
49
+ history[id].push({
50
+ from: record.from,
51
+ to: record.to,
52
+ at: record.at || new Date().toISOString(),
53
+ by: record.by || null,
54
+ note: record.note || null
55
+ });
56
+ writeHistory(workspaceRoot, history);
57
+ return history[id];
58
+ }
59
+
60
+ export function lastTransition(history, id) {
61
+ const entries = history[id];
62
+ if (!entries || entries.length === 0) return null;
63
+ return entries[entries.length - 1];
64
+ }
65
+
66
+ export function detectDriftedStatus(history, statement) {
67
+ // If the last recorded transition's `to` doesn't match the statement's
68
+ // current status, the artifact was edited outside the CLI.
69
+ const last = lastTransition(history, statement.id);
70
+ if (!last) return null;
71
+ if (last.to !== statement.status) {
72
+ return {
73
+ id: statement.id,
74
+ kind: statement.kind,
75
+ historyStatus: last.to,
76
+ currentStatus: statement.status
77
+ };
78
+ }
79
+ return null;
80
+ }
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+
3
+ export function topogramRootForSdlc(inputPath) {
4
+ const absolute = path.resolve(inputPath);
5
+ return path.basename(absolute) === "topogram" ? absolute : path.join(absolute, "topogram");
6
+ }
7
+
8
+ export function projectRootForSdlc(inputPath) {
9
+ const absolute = path.resolve(inputPath);
10
+ return path.basename(absolute) === "topogram" ? path.dirname(absolute) : absolute;
11
+ }
@@ -0,0 +1,106 @@
1
+ // `topogram release --app-version X.Y.Z` — single best-effort checkpointed
2
+ // operation:
3
+ // 1. Assemble release notes (from current/archived terminal-status data)
4
+ // 2. Stamp `app_version` on documents whose `app_version` is missing or
5
+ // older than the release version
6
+ // 3. Archive eligible terminal-status artifacts
7
+ //
8
+ // `--dry-run` prints the planned mutations without touching disk.
9
+
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+
12
+ import { parsePath } from "../parser.js";
13
+ import { resolveWorkspace } from "../resolver/index.js";
14
+ import { generateSdlcReleaseNotes } from "../generator/sdlc/release-notes.js";
15
+ import { archiveBatch, archiveEligibleStatements } from "../archive/archive.js";
16
+
17
+ function parseComparableVersion(value) {
18
+ if (value == null || value === "") return null;
19
+ const match = String(value).trim().match(/^v?(\d+(?:\.\d+)*)(?:[-+].*)?$/i);
20
+ if (!match) return null;
21
+ return match[1].split(".").map((part) => Number.parseInt(part, 10));
22
+ }
23
+
24
+ function compareVersions(left, right) {
25
+ const leftParts = parseComparableVersion(left);
26
+ const rightParts = parseComparableVersion(right);
27
+ if (!leftParts || !rightParts) return null;
28
+ const length = Math.max(leftParts.length, rightParts.length);
29
+ for (let i = 0; i < length; i += 1) {
30
+ const leftPart = leftParts[i] ?? 0;
31
+ const rightPart = rightParts[i] ?? 0;
32
+ if (leftPart < rightPart) return -1;
33
+ if (leftPart > rightPart) return 1;
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ function shouldStampDocVersion(existingVersion, appVersion) {
39
+ if (existingVersion == null || existingVersion === "") return true;
40
+ const comparison = compareVersions(existingVersion, appVersion);
41
+ return comparison === -1;
42
+ }
43
+
44
+ function stampDocsWithVersion(docs, appVersion, options = {}) {
45
+ const planned = [];
46
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
47
+ for (const doc of docs) {
48
+ if (doc.parseError || !doc.file) continue;
49
+ let raw;
50
+ try {
51
+ raw = readFileSync(doc.file, "utf8");
52
+ } catch {
53
+ continue;
54
+ }
55
+ const match = raw.match(FRONTMATTER_RE);
56
+ if (!match) continue;
57
+ const frontmatter = match[1];
58
+ const existingVersion = doc.metadata?.app_version || null;
59
+ if (!shouldStampDocVersion(existingVersion, appVersion)) continue;
60
+
61
+ const updated = frontmatter.includes("app_version:")
62
+ ? frontmatter.replace(/app_version:.*$/m, `app_version: ${appVersion}`)
63
+ : frontmatter + `\napp_version: ${appVersion}`;
64
+ const newRaw = raw.replace(FRONTMATTER_RE, `---\n${updated}\n---\n`);
65
+ planned.push({ file: doc.file, before: doc.metadata?.app_version || null, after: appVersion });
66
+ if (!options.dryRun) {
67
+ writeFileSync(doc.file, newRaw, "utf8");
68
+ }
69
+ }
70
+ return planned;
71
+ }
72
+
73
+ export function runRelease(workspaceRoot, options = {}) {
74
+ if (!options.appVersion) {
75
+ return { ok: false, error: "release requires --app-version <label>" };
76
+ }
77
+
78
+ const ast = parsePath(workspaceRoot);
79
+ const resolved = resolveWorkspace(ast);
80
+ if (!resolved.ok) {
81
+ return { ok: false, error: "workspace failed validation; cannot release", validation: resolved.validation };
82
+ }
83
+
84
+ const releaseNotes = generateSdlcReleaseNotes(resolved.graph, {
85
+ appVersion: options.appVersion,
86
+ sinceTag: options.sinceTag
87
+ });
88
+
89
+ const docPlan = stampDocsWithVersion(ast.docs || [], options.appVersion, { dryRun: options.dryRun });
90
+
91
+ const archiveCandidates = archiveEligibleStatements(resolved, {});
92
+ const archiveResult = archiveBatch(workspaceRoot, archiveCandidates, {
93
+ dryRun: options.dryRun,
94
+ by: options.actor,
95
+ release: options.appVersion
96
+ });
97
+
98
+ return {
99
+ ok: true,
100
+ appVersion: options.appVersion,
101
+ dryRun: options.dryRun === true,
102
+ release_notes: releaseNotes,
103
+ document_app_version_updates: docPlan,
104
+ archive: { candidates: archiveCandidates, ...archiveResult }
105
+ };
106
+ }