@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,141 @@
1
+ // Archive: move a terminal-status statement out of the active workspace
2
+ // and into a year-bucketed JSONL file.
3
+ //
4
+ // Strategy:
5
+ // 1. Validate the statement is in a status eligible for archiving.
6
+ // 2. Build the archive entry (frozen snapshot + transitions).
7
+ // 3. Append to `_archive/{kind}s-{year}.jsonl`.
8
+ // 4. Surgically remove the statement block from its source `.tg` file.
9
+ //
10
+ // `archiveBatch` is the bulk counterpart used by `release`.
11
+
12
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
13
+ import path from "node:path";
14
+
15
+ import { parsePath } from "../parser.js";
16
+ import { resolveWorkspace } from "../resolver/index.js";
17
+ import { isArchivableStatus } from "../sdlc/transitions/index.js";
18
+ import { readHistory } from "../sdlc/history.js";
19
+ import { appendEntry } from "./jsonl.js";
20
+ import { buildArchiveEntry, isArchivableKind } from "./schema.js";
21
+
22
+ function findAstStatement(ast, id) {
23
+ for (const file of ast.files) {
24
+ for (const statement of file.statements) {
25
+ if (statement.id === id) {
26
+ return { file, statement };
27
+ }
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function removeStatementBlock(source, statement) {
34
+ // The statement runs from `loc.start.offset` to `loc.end.offset`. Trim
35
+ // any leading whitespace on the same line and any trailing newline.
36
+ let start = statement.loc.start.offset;
37
+ let end = statement.loc.end.offset;
38
+ while (start > 0 && (source[start - 1] === " " || source[start - 1] === "\t")) {
39
+ start -= 1;
40
+ }
41
+ while (end < source.length && (source[end] === " " || source[end] === "\t")) {
42
+ end += 1;
43
+ }
44
+ if (source[end] === "\n") end += 1;
45
+ while (start > 0 && source[start - 1] === "\n" && source[start - 2] === "\n") {
46
+ start -= 1;
47
+ }
48
+ return source.slice(0, start) + source.slice(end);
49
+ }
50
+
51
+ export function archiveStatement(workspaceRoot, id, options = {}) {
52
+ const ast = parsePath(workspaceRoot);
53
+ const resolved = resolveWorkspace(ast);
54
+ if (!resolved.ok) {
55
+ return { ok: false, error: "workspace failed validation; cannot archive", validation: resolved.validation };
56
+ }
57
+
58
+ const located = findAstStatement(ast, id);
59
+ if (!located) return { ok: false, error: `Statement '${id}' not found` };
60
+
61
+ const { statement: astStatement } = located;
62
+ const resolvedStatement = resolved.graph.statements.find((s) => s.id === id);
63
+ if (!resolvedStatement) return { ok: false, error: `Statement '${id}' not in resolved graph` };
64
+
65
+ if (!isArchivableKind(resolvedStatement.kind)) {
66
+ return { ok: false, error: `Kind '${resolvedStatement.kind}' is not archivable` };
67
+ }
68
+ if (!isArchivableStatus(resolvedStatement.kind, resolvedStatement.status) && !options.force) {
69
+ return {
70
+ ok: false,
71
+ error: `Status '${resolvedStatement.status}' is not archive-eligible for ${resolvedStatement.kind}`
72
+ };
73
+ }
74
+
75
+ const history = readHistory(workspaceRoot);
76
+ const transitions = (history[id] || []).slice();
77
+ const entry = buildArchiveEntry(resolvedStatement, transitions, {
78
+ by: options.by,
79
+ release: options.release,
80
+ reason: options.reason
81
+ });
82
+
83
+ if (options.dryRun) {
84
+ return {
85
+ ok: true,
86
+ id,
87
+ kind: resolvedStatement.kind,
88
+ status: resolvedStatement.status,
89
+ file: astStatement.loc.file,
90
+ dryRun: true,
91
+ entry
92
+ };
93
+ }
94
+
95
+ const archiveFile = appendEntry(workspaceRoot, resolvedStatement.kind, entry);
96
+
97
+ const sourcePath = astStatement.loc.file;
98
+ const original = readFileSync(sourcePath, "utf8");
99
+ const rewritten = removeStatementBlock(original, astStatement);
100
+ if (rewritten.trim().length === 0) {
101
+ unlinkSync(sourcePath);
102
+ } else {
103
+ writeFileSync(sourcePath, rewritten, "utf8");
104
+ }
105
+
106
+ return {
107
+ ok: true,
108
+ id,
109
+ kind: resolvedStatement.kind,
110
+ status: resolvedStatement.status,
111
+ file: sourcePath,
112
+ archiveFile,
113
+ dryRun: false
114
+ };
115
+ }
116
+
117
+ export function archiveBatch(workspaceRoot, ids, options = {}) {
118
+ const results = [];
119
+ for (const id of ids) {
120
+ results.push(archiveStatement(workspaceRoot, id, options));
121
+ }
122
+ return {
123
+ ok: results.every((r) => r.ok),
124
+ results
125
+ };
126
+ }
127
+
128
+ export function archiveEligibleStatements(resolved, options = {}) {
129
+ const beforeIso = options.before || null;
130
+ const wantStatuses = options.statuses ? new Set(options.statuses) : null;
131
+ const eligible = [];
132
+ for (const s of resolved.graph.statements) {
133
+ if (s.archived) continue;
134
+ if (!isArchivableKind(s.kind)) continue;
135
+ if (!isArchivableStatus(s.kind, s.status)) continue;
136
+ if (wantStatuses && !wantStatuses.has(s.status)) continue;
137
+ if (beforeIso && s.updated && s.updated > beforeIso) continue;
138
+ eligible.push(s.id);
139
+ }
140
+ return eligible;
141
+ }
@@ -0,0 +1,26 @@
1
+ // Compact a JSONL archive file into pretty-printed JSON for inspection.
2
+ //
3
+ // Reverse operation `expand` is intentionally absent — JSONL is the
4
+ // canonical on-disk format because it supports append-only growth without
5
+ // re-serialization.
6
+
7
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
8
+ import { parseArchiveFile } from "./jsonl.js";
9
+
10
+ export function compact(jsonlPath, outputPath) {
11
+ if (!existsSync(jsonlPath)) {
12
+ return { ok: false, error: `File not found: ${jsonlPath}` };
13
+ }
14
+ const entries = parseArchiveFile(jsonlPath);
15
+ const errors = entries.filter((e) => e.__error).map((e) => e.__error);
16
+ const valid = entries.filter((e) => !e.__error);
17
+ const out = outputPath || jsonlPath.replace(/\.jsonl$/, ".json");
18
+ writeFileSync(out, JSON.stringify(valid, null, 2) + "\n", "utf8");
19
+ return {
20
+ ok: true,
21
+ inputFile: jsonlPath,
22
+ outputFile: out,
23
+ count: valid.length,
24
+ errors
25
+ };
26
+ }
@@ -0,0 +1,70 @@
1
+ // Year-bucketed JSONL archive I/O.
2
+ //
3
+ // File layout: `<project-or-topogram-root>/topogram/_archive/{kind}s-{year}.jsonl`
4
+ // or `<topogram-root>/_archive/{kind}s-{year}.jsonl`
5
+ // (e.g. `tasks-2026.jsonl`, `bugs-2026.jsonl`).
6
+ //
7
+ // Each line is a self-contained archived statement. The format is JSONL so
8
+ // archives can grow append-only without requiring a full rewrite.
9
+
10
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, readdirSync, writeFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { topogramRootForSdlc } from "../sdlc/paths.js";
13
+
14
+ const ARCHIVE_DIR = "_archive";
15
+
16
+ export function archiveDir(workspaceRoot) {
17
+ return path.join(topogramRootForSdlc(workspaceRoot), ARCHIVE_DIR);
18
+ }
19
+
20
+ export function archiveFileFor(workspaceRoot, kind, year) {
21
+ return path.join(archiveDir(workspaceRoot), `${kind}s-${year}.jsonl`);
22
+ }
23
+
24
+ function ensureArchiveDir(workspaceRoot) {
25
+ const dir = archiveDir(workspaceRoot);
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true });
28
+ }
29
+ }
30
+
31
+ export function listArchiveFiles(workspaceRoot) {
32
+ const dir = archiveDir(workspaceRoot);
33
+ if (!existsSync(dir)) return [];
34
+ return readdirSync(dir)
35
+ .filter((name) => name.endsWith(".jsonl"))
36
+ .map((name) => path.join(dir, name));
37
+ }
38
+
39
+ export function parseArchiveFile(filePath) {
40
+ if (!existsSync(filePath)) return [];
41
+ const content = readFileSync(filePath, "utf8");
42
+ const entries = [];
43
+ let lineNo = 0;
44
+ for (const line of content.split(/\r?\n/)) {
45
+ lineNo += 1;
46
+ const trimmed = line.trim();
47
+ if (!trimmed) continue;
48
+ try {
49
+ entries.push(JSON.parse(trimmed));
50
+ } catch (err) {
51
+ entries.push({
52
+ __error: `${path.basename(filePath)}:${lineNo}: ${err.message}`
53
+ });
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+
59
+ export function appendEntry(workspaceRoot, kind, entry) {
60
+ ensureArchiveDir(workspaceRoot);
61
+ const year = (entry.archived?.at || new Date().toISOString()).slice(0, 4);
62
+ const file = archiveFileFor(workspaceRoot, kind, year);
63
+ appendFileSync(file, JSON.stringify(entry) + "\n", "utf8");
64
+ return file;
65
+ }
66
+
67
+ export function rewriteArchiveFile(filePath, entries) {
68
+ const lines = entries.map((entry) => JSON.stringify(entry)).join("\n");
69
+ writeFileSync(filePath, lines + (lines.length > 0 ? "\n" : ""), "utf8");
70
+ }
@@ -0,0 +1,82 @@
1
+ // Bridge between archived JSONL entries and the live resolver graph.
2
+ //
3
+ // At workspace load time the resolver bridge:
4
+ // 1. Walks `topogram/_archive/*.jsonl`
5
+ // 2. Builds a flat list of frozen entries (each with `archived: true`)
6
+ // 3. Returns `{ entries, byId }` so the caller can merge them into the
7
+ // registry / graph
8
+ //
9
+ // Frozen entries participate in cross-references and the traceability
10
+ // matrix but are filtered out of slices and boards by default.
11
+
12
+ import path from "node:path";
13
+ import { listArchiveFiles, parseArchiveFile } from "./jsonl.js";
14
+
15
+ function kindFromFilename(filePath) {
16
+ // tasks-2026.jsonl → task
17
+ const base = path.basename(filePath, ".jsonl");
18
+ const dashIndex = base.lastIndexOf("-");
19
+ if (dashIndex < 0) return null;
20
+ const plural = base.slice(0, dashIndex);
21
+ if (plural.endsWith("s")) return plural.slice(0, -1);
22
+ return plural;
23
+ }
24
+
25
+ export function loadArchive(workspaceRoot) {
26
+ const entries = [];
27
+ const errors = [];
28
+ for (const file of listArchiveFiles(workspaceRoot)) {
29
+ const expectedKind = kindFromFilename(file);
30
+ for (const raw of parseArchiveFile(file)) {
31
+ if (raw.__error) {
32
+ errors.push(raw.__error);
33
+ continue;
34
+ }
35
+ if (expectedKind && raw.kind && raw.kind !== expectedKind) {
36
+ errors.push(`${file}: entry id='${raw.id}' has kind '${raw.kind}', expected '${expectedKind}'`);
37
+ continue;
38
+ }
39
+ entries.push(normalizeArchivedEntry(raw));
40
+ }
41
+ }
42
+ const byId = new Map(entries.map((e) => [e.id, e]));
43
+ return { entries, byId, errors };
44
+ }
45
+
46
+ function normalizeArchivedEntry(raw) {
47
+ const fields = raw.fields && typeof raw.fields === "object" && !Array.isArray(raw.fields) ? raw.fields : {};
48
+ return {
49
+ ...fields,
50
+ ...raw,
51
+ fields,
52
+ archived: true,
53
+ archivedMeta: raw.archived || {},
54
+ updated: raw.updated || fields.updated || raw.archived?.at || null
55
+ };
56
+ }
57
+
58
+ export function mergeArchivedIntoGraph(graph, archive) {
59
+ // Only insert entries whose id isn't already present in the live graph
60
+ // (active workspace wins; an unarchived entry shadows the frozen one).
61
+ const liveIds = new Set(graph.statements.map((s) => s.id));
62
+ const merged = [...graph.statements];
63
+ for (const entry of archive.entries) {
64
+ if (!liveIds.has(entry.id)) {
65
+ merged.push(entry);
66
+ }
67
+ }
68
+ return {
69
+ ...graph,
70
+ statements: merged,
71
+ byKind: groupByKind(merged)
72
+ };
73
+ }
74
+
75
+ function groupByKind(statements) {
76
+ const map = new Map();
77
+ for (const s of statements) {
78
+ if (!map.has(s.kind)) map.set(s.kind, []);
79
+ map.get(s.kind).push(s);
80
+ }
81
+ return Object.fromEntries(map);
82
+ }
@@ -0,0 +1,87 @@
1
+ // Archive entry schema.
2
+ //
3
+ // An archived statement is a frozen snapshot of the resolved statement at
4
+ // the time of archiving, plus:
5
+ // - `transitions`: full history (copied from the sidecar)
6
+ // - `archived`: { at, by, release, reason }
7
+ // - `archived: true` flag (for resolver-bridge's quick filter)
8
+ //
9
+ // Documents archive includes the body verbatim.
10
+
11
+ const ALLOWED_KINDS = new Set(["pitch", "task", "bug", "document"]);
12
+
13
+ export function isArchivableKind(kind) {
14
+ return ALLOWED_KINDS.has(kind);
15
+ }
16
+
17
+ export function buildArchiveEntry(statement, transitions, archivedMeta = {}) {
18
+ if (!isArchivableKind(statement.kind)) {
19
+ throw new Error(`Kind '${statement.kind}' is not archivable`);
20
+ }
21
+ return {
22
+ id: statement.id,
23
+ kind: statement.kind,
24
+ name: statement.name,
25
+ description: statement.description,
26
+ status: statement.status,
27
+ fields: statement.archivedFields || serializeFields(statement),
28
+ transitions: transitions || [],
29
+ archived: {
30
+ at: archivedMeta.at || new Date().toISOString(),
31
+ by: archivedMeta.by || null,
32
+ release: archivedMeta.release || null,
33
+ reason: archivedMeta.reason || null
34
+ }
35
+ };
36
+ }
37
+
38
+ // Serialize a normalized resolved statement into a portable field map. We
39
+ // drop `loc`, `ast`, and resolver-derived back-link arrays (they'd be stale
40
+ // once frozen).
41
+ function serializeFields(statement) {
42
+ const skip = new Set([
43
+ "kind",
44
+ "id",
45
+ "name",
46
+ "description",
47
+ "status",
48
+ "loc",
49
+ "from",
50
+ "transformGraph",
51
+ "members",
52
+ "policy",
53
+ "plan",
54
+ "monitoring",
55
+ "vocabulary",
56
+ "componentContract",
57
+ "record",
58
+ "flow",
59
+ "affectedByPitches",
60
+ "affectedByRequirements",
61
+ "affectedByTasks",
62
+ "affectedByBugs",
63
+ "introducedByRequirements",
64
+ "respectedByRequirements",
65
+ "violatedByBugs",
66
+ "surfacedByBugs",
67
+ "introducedByTasks",
68
+ "verifiedBy",
69
+ "blockingMe",
70
+ "blockedByMe",
71
+ "tasks",
72
+ "verifications",
73
+ "documents",
74
+ "rules",
75
+ "acceptanceCriteria",
76
+ "supersededBy",
77
+ "decisionsFromPitch",
78
+ "requirements",
79
+ "projectedFields"
80
+ ]);
81
+ const out = {};
82
+ for (const [key, value] of Object.entries(statement)) {
83
+ if (skip.has(key)) continue;
84
+ out[key] = value;
85
+ }
86
+ return out;
87
+ }
@@ -0,0 +1,108 @@
1
+ // Unarchive: pull a frozen entry back into the active workspace.
2
+ //
3
+ // Strategy:
4
+ // 1. Find the entry across all archive files (kind+id).
5
+ // 2. Render it as a `.tg` statement and append to a target file.
6
+ // 3. Strip the entry from the archive file (rewrite without it).
7
+ //
8
+ // Status is reset to a sensible "re-opened" value per kind:
9
+ // - bug: open
10
+ // - task: claimed (caller must set claimed_by)
11
+ // - pitch: draft
12
+ // - document: draft
13
+
14
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs";
15
+ import path from "node:path";
16
+ import {
17
+ archiveDir,
18
+ listArchiveFiles,
19
+ parseArchiveFile,
20
+ rewriteArchiveFile
21
+ } from "./jsonl.js";
22
+
23
+ const REOPEN_STATUSES = {
24
+ bug: "open",
25
+ task: "claimed",
26
+ pitch: "draft",
27
+ document: "draft"
28
+ };
29
+
30
+ function findEntry(workspaceRoot, id) {
31
+ for (const file of listArchiveFiles(workspaceRoot)) {
32
+ const entries = parseArchiveFile(file);
33
+ const match = entries.find((e) => e.id === id);
34
+ if (match) return { file, entries, entry: match };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function renderStatement(entry, newStatus) {
40
+ // Render a minimal `.tg` representation. Unknown field types fall back to
41
+ // a string. Lists are space-separated symbols inside `[ ... ]`.
42
+ const lines = [];
43
+ lines.push(`${entry.kind} ${entry.id} {`);
44
+ if (entry.name) lines.push(` name "${entry.name.replace(/"/g, "\\\"")}"`);
45
+ if (entry.description) lines.push(` description "${entry.description.replace(/"/g, "\\\"")}"`);
46
+ for (const [key, value] of Object.entries(entry.fields || {})) {
47
+ if (value == null) continue;
48
+ if (Array.isArray(value)) {
49
+ const items = value
50
+ .map((v) => (typeof v === "string" ? v : v?.id))
51
+ .filter(Boolean);
52
+ if (items.length > 0) {
53
+ lines.push(` ${snakeCase(key)} [${items.join(", ")}]`);
54
+ }
55
+ continue;
56
+ }
57
+ if (typeof value === "object" && value.id) {
58
+ lines.push(` ${snakeCase(key)} ${value.id}`);
59
+ continue;
60
+ }
61
+ if (typeof value === "string") {
62
+ lines.push(` ${snakeCase(key)} "${value.replace(/"/g, "\\\"")}"`);
63
+ continue;
64
+ }
65
+ if (typeof value === "boolean" || typeof value === "number") {
66
+ lines.push(` ${snakeCase(key)} ${value}`);
67
+ }
68
+ }
69
+ lines.push(` status ${newStatus}`);
70
+ lines.push("}");
71
+ return lines.join("\n") + "\n";
72
+ }
73
+
74
+ function snakeCase(camel) {
75
+ return camel.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
76
+ }
77
+
78
+ export function unarchive(workspaceRoot, id, options = {}) {
79
+ const found = findEntry(workspaceRoot, id);
80
+ if (!found) {
81
+ return { ok: false, error: `No archived entry with id '${id}'` };
82
+ }
83
+
84
+ const { file, entries, entry } = found;
85
+ const reopenStatus = options.status || REOPEN_STATUSES[entry.kind] || "draft";
86
+ const targetDir = options.targetDir || path.join(workspaceRoot, "topogram", `${entry.kind}s`);
87
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
88
+ const targetFile = path.join(targetDir, `${entry.id}.tg`);
89
+
90
+ if (existsSync(targetFile)) {
91
+ return { ok: false, error: `Target file '${targetFile}' already exists; refuse to overwrite` };
92
+ }
93
+
94
+ const rendered = renderStatement(entry, reopenStatus);
95
+ writeFileSync(targetFile, rendered, "utf8");
96
+
97
+ const remaining = entries.filter((e) => e.id !== id);
98
+ rewriteArchiveFile(file, remaining);
99
+
100
+ return {
101
+ ok: true,
102
+ id,
103
+ kind: entry.kind,
104
+ targetFile,
105
+ archiveFile: file,
106
+ reopenStatus
107
+ };
108
+ }