@topogram/cli 0.3.72 → 0.3.74

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 (84) hide show
  1. package/README.md +24 -195
  2. package/package.json +1 -1
  3. package/src/adoption/plan/index.js +2 -1
  4. package/src/agent-brief.js +46 -2
  5. package/src/archive/archive.js +1 -1
  6. package/src/archive/jsonl.js +18 -8
  7. package/src/archive/resolver-bridge.js +34 -1
  8. package/src/archive/schema.js +1 -1
  9. package/src/archive/unarchive.js +26 -0
  10. package/src/cli/command-parsers/sdlc.js +66 -0
  11. package/src/cli/commands/import/help.js +1 -0
  12. package/src/cli/commands/import/plan.js +9 -0
  13. package/src/cli/commands/import/workspace.js +3 -0
  14. package/src/cli/commands/query/definitions.js +11 -10
  15. package/src/cli/commands/query/workspace.js +23 -2
  16. package/src/cli/commands/release-rollout.js +191 -10
  17. package/src/cli/commands/release-shared.js +51 -2
  18. package/src/cli/commands/release.js +16 -3
  19. package/src/cli/commands/sdlc.js +213 -5
  20. package/src/cli/dispatcher.js +8 -0
  21. package/src/cli/help.js +15 -3
  22. package/src/cli/options.js +1 -0
  23. package/src/generator/context/shared/domain-sdlc.js +27 -0
  24. package/src/generator/context/shared/relationships.js +2 -1
  25. package/src/generator/context/shared/types.d.ts +1 -0
  26. package/src/generator/context/shared.d.ts +2 -0
  27. package/src/generator/context/shared.js +2 -0
  28. package/src/generator/context/slice/core.js +3 -0
  29. package/src/generator/context/slice/sdlc.js +57 -2
  30. package/src/generator/context/task-mode.js +7 -0
  31. package/src/generator/sdlc/board.js +2 -0
  32. package/src/generator/sdlc/traceability-matrix.js +5 -1
  33. package/src/import/core/context.js +1 -1
  34. package/src/import/core/contracts.js +3 -3
  35. package/src/import/core/registry.js +3 -0
  36. package/src/import/core/runner/candidates.js +7 -0
  37. package/src/import/core/runner/reports.js +9 -1
  38. package/src/import/core/runner/tracks.js +3 -0
  39. package/src/import/extractors/cli/generic.js +340 -0
  40. package/src/new-project/project-files.js +10 -3
  41. package/src/resolver/enrich/task.js +3 -1
  42. package/src/resolver/index.js +6 -0
  43. package/src/resolver/normalize.js +31 -0
  44. package/src/resolver/projections-cli.js +158 -0
  45. package/src/sdlc/adopt.js +4 -1
  46. package/src/sdlc/check.js +24 -2
  47. package/src/sdlc/complete.js +47 -0
  48. package/src/sdlc/dod/index.js +2 -0
  49. package/src/sdlc/dod/plan.js +15 -0
  50. package/src/sdlc/dod/task.js +7 -3
  51. package/src/sdlc/explain.js +53 -1
  52. package/src/sdlc/gate.js +352 -0
  53. package/src/sdlc/history.d.ts +7 -0
  54. package/src/sdlc/history.js +50 -5
  55. package/src/sdlc/link.js +172 -0
  56. package/src/sdlc/paths.d.ts +4 -0
  57. package/src/sdlc/paths.js +8 -0
  58. package/src/sdlc/plan-steps.js +71 -0
  59. package/src/sdlc/plan.js +245 -0
  60. package/src/sdlc/policy.js +249 -0
  61. package/src/sdlc/prep.js +186 -0
  62. package/src/sdlc/scaffold.js +4 -2
  63. package/src/sdlc/status-filter.js +2 -0
  64. package/src/sdlc/transitions/index.js +3 -0
  65. package/src/sdlc/transitions/plan.js +32 -0
  66. package/src/validator/common.js +25 -4
  67. package/src/validator/index.js +10 -0
  68. package/src/validator/kinds.d.ts +7 -0
  69. package/src/validator/kinds.js +32 -0
  70. package/src/validator/per-kind/plan.js +128 -0
  71. package/src/validator/per-kind/task.js +19 -0
  72. package/src/validator/projections/cli.js +267 -0
  73. package/src/validator.d.ts +1 -0
  74. package/src/workflows/import-app/shared.js +1 -1
  75. package/src/workflows/reconcile/adoption-plan/build.js +3 -1
  76. package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
  77. package/src/workflows/reconcile/bundle-core/index.js +3 -0
  78. package/src/workflows/reconcile/candidate-model.js +15 -0
  79. package/src/workflows/reconcile/gap-report.js +4 -2
  80. package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
  81. package/src/workflows/reconcile/renderers.js +82 -0
  82. package/src/workflows/reconcile/summary.js +4 -0
  83. package/src/workflows/reconcile/workflow.js +2 -1
  84. package/src/workspace-paths.js +26 -2
package/README.md CHANGED
@@ -1,223 +1,52 @@
1
- # Topogram Engine
1
+ # Topogram CLI Package
2
2
 
3
- This folder contains the Topogram implementation: parser, validator, resolver, generators, runtime bundle emitters, and CLI.
3
+ This directory is the npm package for `@topogram/cli`. It exposes the
4
+ `topogram` executable.
4
5
 
5
- The active product workflow is authoring-to-generated-app. Engine development should stay separate from user-facing demos.
6
+ The repo root owns product docs. Start with:
6
7
 
7
- ## Package Shape
8
+ - [README](../README.md)
9
+ - [Docs map](../docs/README.md)
10
+ - [CLI Reference](../docs/reference/cli.md)
11
+ - [Engine Development](../docs/maintainers/engine-development.md)
8
12
 
9
- The engine is the publishable CLI package:
13
+ ## Package shape
10
14
 
11
15
  ```json
12
16
  {
13
17
  "name": "@topogram/cli",
14
18
  "bin": {
15
- "topogram": "./src/cli.js"
19
+ "topogram": "src/cli.js"
16
20
  }
17
21
  }
18
22
  ```
19
23
 
20
- This lets source checkouts and package consumers call:
24
+ ## Local development
21
25
 
22
- ```bash
23
- topogram new ../my-app
24
- topogram version
25
- topogram version --json
26
- topogram doctor
27
- topogram check
28
- topogram generate
29
- topogram catalog list
30
- topogram catalog show todo
31
- topogram catalog check topograms.catalog.json
32
- topogram catalog copy hello ../hello-topogram
33
- topogram import ../existing-app --out ../imported-topogram
34
- topogram import check ../imported-topogram
35
- topogram release status
36
- topogram package update-cli --latest
37
- topogram source status ../hello-topogram --local
38
- topogram source status ../hello-topogram --remote
39
- topogram template list
40
- topogram template explain
41
- topogram template status
42
- topogram template policy check
43
- topogram template policy pin @scope/template@0.2.0
44
- topogram template check ../my-template
45
- topogram template update --plan
46
- topogram trust template
47
- ```
48
-
49
- Publishing is manual through the repo-level `Publish CLI Package` workflow. Create generated projects outside `engine/`; this directory is source and test code.
50
-
51
- From the repo root, prefer:
52
-
53
- ```bash
54
- npm run smoke:test-app
55
- npm run cli:check
56
- npm run new -- ./my-topogram-app
57
- ```
58
-
59
- ## Layout
60
-
61
- - `src/` - engine source
62
- - `tests/active/` - retained active engine tests
63
- - `tests/fixtures/templates/` - local template fixtures used by engine tests
64
- - `tests/fixtures/workspaces/` - engine-owned Topogram workspaces
65
- - `tests/fixtures/expected/` - engine-owned golden outputs
66
- - `tests/fixtures/invalid/` - invalid model cases
67
-
68
- The generated Todo demo and Todo starter template live outside this repo in `topogram-demo-todo` and `topogram-template-todo`.
69
-
70
- ## Active Fixture
71
-
72
- The current generated-app regression fixture is:
73
-
74
- ```text
75
- tests/fixtures/workspaces/app-basic/
76
- tests/fixtures/expected/app-basic/
77
- ```
78
-
79
- It is a fixture, not a user example.
80
-
81
- ## Commands
82
-
83
- Run the engine gate:
84
-
85
- ```bash
86
- npm run check
87
- ```
88
-
89
- Create a starter project from the catalog-backed default `hello-web` alias:
26
+ From the repo root:
90
27
 
91
28
  ```bash
92
- topogram new ../my-topogram-app --template hello-web
93
- cd ../my-topogram-app
94
29
  npm install
95
- npm run doctor
96
- npm run check
97
- ```
98
-
99
- Choose another catalog starter with the template commands:
100
-
101
- ```bash
102
- topogram template list
103
- topogram template show web-api
104
- topogram new ../web-api-demo --template web-api
105
- ```
106
-
107
- Create a starter project from a shared template package:
108
-
109
- ```bash
110
- topogram new ../todo-demo --template @topogram/template-todo
111
- topogram new ../todo-demo --template todo
112
- ```
113
-
114
- Catalog aliases resolve through the public catalog index at
115
- `github:attebury/topograms/topograms.catalog.json`. The catalog is package
116
- backed; executable starter content still lives in template packages. Use
117
- `topogram catalog show <id>` to inspect an entry and get the correct `new` or
118
- `copy` command for that kind. Use `topogram template show <id>` when the entry
119
- is known to be a starter template and you want the direct `topogram new` flow.
120
- Pure
121
- topogram catalog entries can be copied for editing with
122
- `topogram catalog copy <id> <target>`. Copied topogram projects record
123
- `.topogram-source.json`; inspect local drift from that import baseline with
124
- `topogram source status <target> --local`. This metadata is provenance only and does not
125
- block local edits, checks, or generation. Template baseline divergence means the
126
- local project owns those edits; executable implementation trust is the separate
127
- state that can block generation until reviewed.
128
- Run `topogram template detach <target>` when the project should stop tracking
129
- template update metadata while keeping normal check/generate behavior.
130
-
131
- Do not create generated projects under `engine/`. The CLI refuses paths inside the engine directory.
132
-
133
- Template pack authoring and trust policy are documented in `../docs/template-authoring.md`.
134
- Catalog layout and optional private-source access are documented in `../docs/catalog.md`.
135
- Projects created from executable templates include `.topogram-template-trust.json`;
136
- regenerate it with `topogram trust template` after reviewing copied
137
- `implementation/` code. Use `topogram template status` for the lifecycle
138
- summary, then `topogram trust status` and `topogram trust diff` to inspect
139
- changed files before refreshing trust.
140
-
141
- Projects also include `topogram.template-policy.json`, the allow policy used by
142
- template update and template check commands. It can restrict candidate template
143
- sources, template ids, package scopes, executable-template behavior, and pinned
144
- template versions:
145
-
146
- ```bash
147
- topogram template policy check
148
- topogram template policy explain
149
- topogram template policy init
150
- topogram template policy pin @scope/template@0.2.0
151
- ```
152
-
153
- Use `topogram template policy explain` for a rule-by-rule view of the current
154
- project template, package scope, catalog provenance, executable implementation
155
- setting, and pinned version state.
156
-
157
- Use `topogram template status --latest` and `topogram template update --latest`
158
- only for package-backed templates when an explicit registry lookup is desired.
159
- Plain status and update commands do not query the registry.
160
-
161
- Use `topogram template update --status [--template <spec>]` to inspect current
162
- template adoption state with baseline and conflict analysis. Use
163
- `topogram template update --recommend [--template <spec>]` for a concise
164
- human/agent next-step summary before applying, adopting, or deleting files. Use
165
- `topogram template update --plan [--template <spec>]` to compare the current
166
- project with a candidate template without writing files. Use
167
- `topogram template update --check [--template <spec>]` as the no-write CI guard;
168
- it exits nonzero when the project is not aligned with the recorded or supplied
169
- template. Any update mode can write a machine-readable review report with
170
- `--out <path>`. After review, `topogram template update --apply [--template
171
- <spec>]` writes added/changed template-owned files, records a new
172
- `.topogram-template-files.json` baseline, skips deletes, and refuses local
173
- conflicts. Existing projects can run `topogram trust template` after review to
174
- record the first template-owned file baseline. JSON output includes structured
175
- diagnostics with codes, paths, suggested fixes, and workflow steps.
176
-
177
- Single-file adoption actions are explicit and baseline-aware:
178
- `--accept-current <file>` records the current file as the trusted baseline,
179
- `--accept-candidate <file>` applies one candidate file after baseline checks,
180
- and `--delete-current <file>` deletes one current-only file only when it still
181
- matches the trusted baseline.
182
-
183
- Template authors can run `topogram template check <template-spec-or-path>` to
184
- validate manifest/layout, temporary starter creation, starter checks, trust
185
- metadata, and no-write update planning. The JSON form reports structured
186
- diagnostics with codes, paths, and suggested fixes for authoring feedback.
187
-
188
- Run the same gate directly:
189
-
190
- ```bash
191
30
  npm test
31
+ bash ./scripts/verify-engine.sh
32
+ bash ./scripts/verify-cli-package.sh
192
33
  ```
193
34
 
194
- Run only the active fixture validity check:
35
+ From `engine/`:
195
36
 
196
37
  ```bash
38
+ npm test
197
39
  npm run fixture:check
198
- ```
199
-
200
- Inspect the active fixture topology as JSON:
201
-
202
- ```bash
203
- npm run fixture:check:json
204
- ```
205
-
206
- Generate the active fixture app bundle:
207
-
208
- ```bash
209
40
  npm run fixture:generate
210
41
  ```
211
42
 
212
- Run the app-generation workflow test:
43
+ ## What belongs here
213
44
 
214
- ```bash
215
- node --test ./tests/active/generated-app-workflow.test.js
216
- ```
45
+ - parser, validator, resolver;
46
+ - CLI command dispatch;
47
+ - import, reconcile, and adoption workflows;
48
+ - context/query/agent packets;
49
+ - generator dispatch and bundled fallback adapters;
50
+ - engine fixtures and active tests.
217
51
 
218
- Validate or generate through the public CLI shape:
219
-
220
- ```bash
221
- node ./src/cli.js check ./tests/fixtures/workspaces/app-basic
222
- node ./src/cli.js generate ./tests/fixtures/workspaces/app-basic --out /tmp/topogram-app
223
- ```
52
+ Generated projects should live outside `engine/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.72",
3
+ "version": "0.3.74",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -6,7 +6,7 @@ import {
6
6
  reviewBoundaryForImportProposal
7
7
  } from "../../policy/review-boundaries.js";
8
8
 
9
- const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "widgets", "docs", "journeys", "workflows", "verification", "ui"]);
9
+ const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "widgets", "docs", "journeys", "workflows", "verification", "cli", "ui"]);
10
10
 
11
11
  function stableSortedStrings(values) {
12
12
  return [...new Set((values || []).filter(Boolean))].sort();
@@ -360,6 +360,7 @@ export function selectorMatchesItem(selector, item) {
360
360
  if (selector === "journeys") return item.track === "docs" && String(item.canonical_rel_path || "").startsWith("docs/journeys/");
361
361
  if (selector === "workflows") return item.track === "workflows" || item.kind === "decision";
362
362
  if (selector === "verification") return item.kind === "verification";
363
+ if (selector === "cli") return item.bundle === "cli" || item.track === "cli" || item.suggested_action === "promote_cli_surface";
363
364
  if (selector === "ui") return item.track === "ui" || item.kind === "widget" || item.source_kind === "ui_widget_event";
364
365
  if (selector.startsWith("bundle:")) return item.bundle === selector.slice("bundle:".length);
365
366
  return false;
@@ -20,6 +20,10 @@ import {
20
20
  getTemplateTrustStatus,
21
21
  TEMPLATE_TRUST_FILE
22
22
  } from "./template-trust.js";
23
+ import {
24
+ loadSdlcPolicy,
25
+ SDLC_POLICY_FILE
26
+ } from "./sdlc/policy.js";
23
27
  import { DEFAULT_TOPO_FOLDER_NAME, resolveTopoRoot, resolveWorkspaceContext } from "./workspace-paths.js";
24
28
 
25
29
  /**
@@ -164,6 +168,17 @@ function readImportSummary(projectRoot) {
164
168
  */
165
169
  function buildWorkflows(config, hasImportRecord) {
166
170
  const workflows = [
171
+ {
172
+ id: "sdlc-task-plan",
173
+ title: "SDLC task and plan loop",
174
+ commands: [
175
+ "topogram sdlc explain <task-or-bug-id> --json",
176
+ "topogram query slice ./topo --task <task-id> --json",
177
+ "topogram sdlc plan explain <plan-id> --json",
178
+ "topogram sdlc plan step complete <plan-id> <step-id> --actor <actor> --write"
179
+ ],
180
+ rule: "Plans are optional; edit plan text directly, but use CLI for status, history, step progress, and archive state."
181
+ },
167
182
  {
168
183
  id: "greenfield-generated",
169
184
  title: "Generated project loop",
@@ -235,9 +250,10 @@ function buildWorkflows(config, hasImportRecord) {
235
250
  * @param {Record<string, any>} trust
236
251
  * @param {Record<string, any>|null} importSummary
237
252
  * @param {Record<string, any>} generatorPolicy
253
+ * @param {Record<string, any>} sdlcPolicy
238
254
  * @returns {string[]}
239
255
  */
240
- function buildWarnings(projectRoot, config, trust, importSummary, generatorPolicy) {
256
+ function buildWarnings(projectRoot, config, trust, importSummary, generatorPolicy, sdlcPolicy) {
241
257
  /** @type {string[]} */
242
258
  const warnings = [];
243
259
  if (config?.implementation) {
@@ -249,6 +265,9 @@ function buildWarnings(projectRoot, config, trust, importSummary, generatorPolic
249
265
  if (generatorPolicy?.diagnostics?.errors > 0) {
250
266
  warnings.push(`${GENERATOR_POLICY_FILE} has generator policy errors. Fix policy before generating.`);
251
267
  }
268
+ if (sdlcPolicy?.status === "adopted" && sdlcPolicy?.mode === "enforced") {
269
+ warnings.push("SDLC is enforced. Protected changes need a valid SDLC item, a topo/sdlc/*.tg record update, or an explicit allowed exemption.");
270
+ }
252
271
  if (summarizeOutputBoundaries(config).some((output) => output.ownership === "generated")) {
253
272
  warnings.push("Generated-owned outputs are replaceable by Topogram; do not make lasting edits under generated output paths.");
254
273
  }
@@ -312,12 +331,25 @@ export function buildAgentBrief(inputPath, workspaceAst) {
312
331
  const generatorBindings = packageBackedGeneratorBindings(config);
313
332
  const generatorDiagnostics = generatorPolicyDiagnosticsForBindings(generatorPolicyInfo, generatorBindings, "agent-brief");
314
333
  const importSummary = readImportSummary(configDir);
334
+ const sdlcPolicyInfo = loadSdlcPolicy(configDir);
335
+ const sdlcPolicy = {
336
+ exists: sdlcPolicyInfo.exists,
337
+ path: relativeProjectPath(projectRoot, sdlcPolicyInfo.path),
338
+ status: sdlcPolicyInfo.status,
339
+ mode: sdlcPolicyInfo.mode,
340
+ protectedPaths: sdlcPolicyInfo.policy?.protectedPaths || [],
341
+ requiredItemKinds: sdlcPolicyInfo.policy?.requiredItemKinds || [],
342
+ allowExemptions: sdlcPolicyInfo.policy?.allowExemptions ?? false,
343
+ gateCommand: "topogram sdlc gate . --require-adopted",
344
+ diagnostics: sdlcPolicyInfo.diagnostics
345
+ };
315
346
 
316
347
  const topogramReadPath = path.resolve(topogramRoot) === path.resolve(projectRoot) ? "." : `${DEFAULT_TOPO_FOLDER_NAME}/`;
317
348
  const readOrder = [
318
349
  readItem(projectRoot, "AGENTS.md", "Human-readable first-run guidance generated with this project.", false),
319
350
  readItem(projectRoot, "README.md", "Project workflow and template provenance summary.", true),
320
351
  readItem(projectRoot, "topogram.project.json", "Topology, outputs, template metadata, generator bindings, and implementation provider settings.", true),
352
+ readItem(projectRoot, SDLC_POLICY_FILE, "SDLC adoption, enforcement mode, protected paths, required item kinds, and exemption policy.", false),
321
353
  readItem(projectRoot, "topogram.template-policy.json", "Template trust/update policy for attached templates.", false),
322
354
  readItem(projectRoot, GENERATOR_POLICY_FILE, "Package-backed generator policy and allowed scopes.", false),
323
355
  readItem(projectRoot, TEMPLATE_TRUST_FILE, "Executable implementation trust record, if the template copied implementation code.", Boolean(config.implementation)),
@@ -331,6 +363,11 @@ export function buildAgentBrief(inputPath, workspaceAst) {
331
363
  commandItem("npm run source:status", "See whether template-derived files diverged locally."),
332
364
  commandItem("npm run template:explain", "Understand whether the project is template-attached or detached."),
333
365
  commandItem("npm run generator:policy:check", "Validate package-backed generator policy before generation."),
366
+ ...(sdlcPolicy.status === "adopted" ? [
367
+ commandItem("topogram sdlc policy explain --json", "Read current SDLC enforcement mode and protected paths.", "sdlc"),
368
+ commandItem("topogram sdlc gate . --require-adopted --json", "Verify protected work has SDLC linkage before PR/CI.", "sdlc"),
369
+ commandItem("topogram sdlc explain <task-or-bug-id> --json", "Start SDLC-backed implementation from the current task or bug.", "sdlc")
370
+ ] : []),
334
371
  ...(config.implementation ? [
335
372
  commandItem("npm run trust:status", "Check executable implementation trust before generation.", "trust")
336
373
  ] : []),
@@ -385,6 +422,7 @@ export function buildAgentBrief(inputPath, workspaceAst) {
385
422
  safe_paths: [
386
423
  `${DEFAULT_TOPO_FOLDER_NAME}/**`,
387
424
  "topogram.project.json",
425
+ SDLC_POLICY_FILE,
388
426
  "topogram.template-policy.json",
389
427
  GENERATOR_POLICY_FILE,
390
428
  ...(config.implementation ? ["implementation/** after review and trust status"] : [])
@@ -396,8 +434,10 @@ export function buildAgentBrief(inputPath, workspaceAst) {
396
434
  file_organization: {
397
435
  small: [`${DEFAULT_TOPO_FOLDER_NAME}/actors`, `${DEFAULT_TOPO_FOLDER_NAME}/entities`, `${DEFAULT_TOPO_FOLDER_NAME}/shapes`, `${DEFAULT_TOPO_FOLDER_NAME}/capabilities`, `${DEFAULT_TOPO_FOLDER_NAME}/widgets`, `${DEFAULT_TOPO_FOLDER_NAME}/projections`, `${DEFAULT_TOPO_FOLDER_NAME}/verifications`],
398
436
  large: [`${DEFAULT_TOPO_FOLDER_NAME}/domains/<domain>`, `${DEFAULT_TOPO_FOLDER_NAME}/shared`, `${DEFAULT_TOPO_FOLDER_NAME}/domains/<domain>/widgets`, `${DEFAULT_TOPO_FOLDER_NAME}/domains/<domain>/projections`],
437
+ sdlc: [`${DEFAULT_TOPO_FOLDER_NAME}/sdlc/pitches`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/requirements`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/acceptance_criteria`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/tasks`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/plans`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/bugs`, `${DEFAULT_TOPO_FOLDER_NAME}/sdlc/decisions`],
399
438
  parserRule: "Folder layout is for humans and agents; Topogram flattens statements into one graph."
400
439
  },
440
+ sdlc_policy: sdlcPolicy,
401
441
  topology: {
402
442
  runtimes: summarizeRuntimes(config),
403
443
  outputs: summarizeOutputBoundaries(config)
@@ -415,7 +455,7 @@ export function buildAgentBrief(inputPath, workspaceAst) {
415
455
  import: importSummary,
416
456
  warnings: []
417
457
  };
418
- payload.warnings = buildWarnings(projectRoot, config, payload.trust, importSummary, generatorPolicy);
458
+ payload.warnings = buildWarnings(projectRoot, config, payload.trust, importSummary, generatorPolicy, sdlcPolicy);
419
459
  return { ok: true, payload };
420
460
  }
421
461
 
@@ -430,6 +470,7 @@ export function formatAgentBrief(brief) {
430
470
  lines.push(`Topogram: ${brief.project?.topogram || "unknown"}`);
431
471
  lines.push(`Template: ${brief.template?.id || "none"}${brief.template?.version ? `@${brief.template.version}` : ""}`);
432
472
  lines.push(`Implementation trust: ${brief.trust?.requiresTrust ? (brief.trust.ok ? "trusted" : "review required") : "not required"}`);
473
+ lines.push(`SDLC policy: ${brief.sdlc_policy?.status || "not_adopted"}${brief.sdlc_policy?.mode ? `/${brief.sdlc_policy.mode}` : ""}`);
433
474
  lines.push(`Package-backed generators: ${brief.generator_policy?.packageBackedGenerators || 0}`);
434
475
  lines.push("");
435
476
  lines.push("Read order:");
@@ -473,6 +514,9 @@ export function formatAgentBrief(brief) {
473
514
  lines.push("");
474
515
  lines.push("Verification gates:");
475
516
  lines.push(" - npm run check");
517
+ if (brief.sdlc_policy?.status === "adopted") {
518
+ lines.push(` - ${brief.sdlc_policy.gateCommand || "topogram sdlc gate . --require-adopted"}`);
519
+ }
476
520
  lines.push(" - topogram widget check --json when UI/widget contracts change");
477
521
  lines.push(" - topogram widget behavior --json when widget behavior changes");
478
522
  lines.push(" - npm run generate");
@@ -4,7 +4,7 @@
4
4
  // Strategy:
5
5
  // 1. Validate the statement is in a status eligible for archiving.
6
6
  // 2. Build the archive entry (frozen snapshot + transitions).
7
- // 3. Append to `_archive/{kind}s-{year}.jsonl`.
7
+ // 3. Append to `sdlc/_archive/{kind}s-{year}.jsonl`.
8
8
  // 4. Surgically remove the statement block from its source `.tg` file.
9
9
  //
10
10
  // `archiveBatch` is the bulk counterpart used by `release`.
@@ -1,7 +1,7 @@
1
1
  // Year-bucketed JSONL archive I/O.
2
2
  //
3
- // File layout: `<project-root>/topo/_archive/{kind}s-{year}.jsonl`
4
- // or `<workspace-root>/_archive/{kind}s-{year}.jsonl`
3
+ // File layout: `<project-root>/topo/sdlc/_archive/{kind}s-{year}.jsonl`
4
+ // or `<workspace-root>/sdlc/_archive/{kind}s-{year}.jsonl`
5
5
  // (e.g. `tasks-2026.jsonl`, `bugs-2026.jsonl`).
6
6
  //
7
7
  // Each line is a self-contained archived statement. The format is JSONL so
@@ -9,11 +9,15 @@
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, appendFileSync, readdirSync, writeFileSync } from "node:fs";
11
11
  import path from "node:path";
12
- import { topogramRootForSdlc } from "../sdlc/paths.js";
12
+ import { sdlcRootForSdlc, topogramRootForSdlc } from "../sdlc/paths.js";
13
13
 
14
14
  const ARCHIVE_DIR = "_archive";
15
15
 
16
16
  export function archiveDir(workspaceRoot) {
17
+ return path.join(sdlcRootForSdlc(workspaceRoot), ARCHIVE_DIR);
18
+ }
19
+
20
+ function legacyArchiveDir(workspaceRoot) {
17
21
  return path.join(topogramRootForSdlc(workspaceRoot), ARCHIVE_DIR);
18
22
  }
19
23
 
@@ -29,11 +33,17 @@ function ensureArchiveDir(workspaceRoot) {
29
33
  }
30
34
 
31
35
  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));
36
+ const dirs = [archiveDir(workspaceRoot), legacyArchiveDir(workspaceRoot)];
37
+ const files = [];
38
+ for (const dir of dirs) {
39
+ if (!existsSync(dir)) continue;
40
+ files.push(
41
+ ...readdirSync(dir)
42
+ .filter((name) => name.endsWith(".jsonl"))
43
+ .map((name) => path.join(dir, name))
44
+ );
45
+ }
46
+ return files;
37
47
  }
38
48
 
39
49
  export function parseArchiveFile(filePath) {
@@ -1,7 +1,7 @@
1
1
  // Bridge between archived JSONL entries and the live resolver graph.
2
2
  //
3
3
  // At workspace load time the resolver bridge:
4
- // 1. Walks the workspace `_archive/*.jsonl`
4
+ // 1. Walks the workspace `sdlc/_archive/*.jsonl`
5
5
  // 2. Builds a flat list of frozen entries (each with `archived: true`)
6
6
  // 3. Returns `{ entries, byId }` so the caller can merge them into the
7
7
  // registry / graph
@@ -36,6 +36,11 @@ export function loadArchive(workspaceRoot) {
36
36
  errors.push(`${file}: entry id='${raw.id}' has kind '${raw.kind}', expected '${expectedKind}'`);
37
37
  continue;
38
38
  }
39
+ const schemaErrors = validateArchivedEntry(file, raw, expectedKind);
40
+ if (schemaErrors.length > 0) {
41
+ errors.push(...schemaErrors);
42
+ continue;
43
+ }
39
44
  entries.push(normalizeArchivedEntry(raw));
40
45
  }
41
46
  }
@@ -43,6 +48,34 @@ export function loadArchive(workspaceRoot) {
43
48
  return { entries, byId, errors };
44
49
  }
45
50
 
51
+ function validateArchivedEntry(file, raw, expectedKind) {
52
+ const errors = [];
53
+ const label = `${file}: archive entry`;
54
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
55
+ return [`${label} must be an object`];
56
+ }
57
+ for (const key of ["id", "kind", "status"]) {
58
+ if (typeof raw[key] !== "string" || raw[key].trim() === "") {
59
+ errors.push(`${label} must include string '${key}'`);
60
+ }
61
+ }
62
+ if (expectedKind && typeof raw.kind === "string" && raw.kind !== expectedKind) {
63
+ errors.push(`${label} id='${raw.id}' has kind '${raw.kind}', expected '${expectedKind}'`);
64
+ }
65
+ if (raw.fields !== undefined && (!raw.fields || typeof raw.fields !== "object" || Array.isArray(raw.fields))) {
66
+ errors.push(`${label} id='${raw.id}' field 'fields' must be an object when present`);
67
+ }
68
+ if (!Array.isArray(raw.transitions)) {
69
+ errors.push(`${label} id='${raw.id}' must include transitions array`);
70
+ }
71
+ if (!raw.archived || typeof raw.archived !== "object" || Array.isArray(raw.archived)) {
72
+ errors.push(`${label} id='${raw.id}' must include archived metadata object`);
73
+ } else if (typeof raw.archived.at !== "string" || raw.archived.at.trim() === "") {
74
+ errors.push(`${label} id='${raw.id}' archived metadata must include string 'at'`);
75
+ }
76
+ return errors;
77
+ }
78
+
46
79
  function normalizeArchivedEntry(raw) {
47
80
  const fields = raw.fields && typeof raw.fields === "object" && !Array.isArray(raw.fields) ? raw.fields : {};
48
81
  return {
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Documents archive includes the body verbatim.
10
10
 
11
- const ALLOWED_KINDS = new Set(["pitch", "task", "bug", "document"]);
11
+ const ALLOWED_KINDS = new Set(["pitch", "task", "plan", "bug", "document"]);
12
12
 
13
13
  export function isArchivableKind(kind) {
14
14
  return ALLOWED_KINDS.has(kind);
@@ -8,6 +8,7 @@
8
8
  // Status is reset to a sensible "re-opened" value per kind:
9
9
  // - bug: open
10
10
  // - task: claimed (caller must set claimed_by)
11
+ // - plan: draft
11
12
  // - pitch: draft
12
13
  // - document: draft
13
14
 
@@ -24,6 +25,7 @@ import { resolveTopoRoot } from "../workspace-paths.js";
24
25
  const REOPEN_STATUSES = {
25
26
  bug: "open",
26
27
  task: "claimed",
28
+ plan: "draft",
27
29
  pitch: "draft",
28
30
  document: "draft"
29
31
  };
@@ -44,6 +46,30 @@ function renderStatement(entry, newStatus) {
44
46
  lines.push(`${entry.kind} ${entry.id} {`);
45
47
  if (entry.name) lines.push(` name "${entry.name.replace(/"/g, "\\\"")}"`);
46
48
  if (entry.description) lines.push(` description "${entry.description.replace(/"/g, "\\\"")}"`);
49
+ if (entry.kind === "plan") {
50
+ if (entry.fields?.task?.id) lines.push(` task ${entry.fields.task.id}`);
51
+ if (entry.fields?.priority) lines.push(` priority ${entry.fields.priority}`);
52
+ if (entry.fields?.notes) lines.push(` notes "${String(entry.fields.notes).replace(/"/g, "\\\"")}"`);
53
+ if (entry.fields?.outcome) lines.push(` outcome "${String(entry.fields.outcome).replace(/"/g, "\\\"")}"`);
54
+ lines.push(" steps {");
55
+ for (const step of entry.fields?.steps || []) {
56
+ const parts = [
57
+ "step",
58
+ step.id,
59
+ "status",
60
+ step.status,
61
+ "description",
62
+ `"${String(step.description || "").replace(/"/g, "\\\"")}"`
63
+ ];
64
+ if (step.notes) parts.push("notes", `"${String(step.notes).replace(/"/g, "\\\"")}"`);
65
+ if (step.outcome) parts.push("outcome", `"${String(step.outcome).replace(/"/g, "\\\"")}"`);
66
+ lines.push(` ${parts.join(" ")}`);
67
+ }
68
+ lines.push(" }");
69
+ lines.push(` status ${newStatus}`);
70
+ lines.push("}");
71
+ return lines.join("\n") + "\n";
72
+ }
47
73
  for (const [key, value] of Object.entries(entry.fields || {})) {
48
74
  if (value == null) continue;
49
75
  if (Array.isArray(value)) {
@@ -7,6 +7,72 @@ import { commandPath } from "./shared.js";
7
7
  * @returns {import("./shared.js").SplitCommandArgs|null}
8
8
  */
9
9
  export function parseSdlcCommandArgs(args) {
10
+ if (args[0] === "sdlc" && args[1] === "policy" && ["init", "check", "explain"].includes(args[2])) {
11
+ return {
12
+ sdlcCommand: `policy:${args[2]}`,
13
+ inputPath: commandPath(args, 3, ".")
14
+ };
15
+ }
16
+ if (args[0] === "sdlc" && args[1] === "gate") {
17
+ return { sdlcCommand: "gate", inputPath: commandPath(args, 2, ".") };
18
+ }
19
+ if (args[0] === "sdlc" && args[1] === "prep" && args[2] === "commit") {
20
+ return { sdlcCommand: "prep:commit", inputPath: commandPath(args, 3, ".") };
21
+ }
22
+ if (args[0] === "sdlc" && args[1] === "link") {
23
+ return {
24
+ sdlcCommand: "link",
25
+ sdlcFromId: args[2],
26
+ sdlcToId: args[3],
27
+ inputPath: commandPath(args, 4, ".")
28
+ };
29
+ }
30
+ if (args[0] === "sdlc" && args[1] === "complete") {
31
+ return {
32
+ sdlcCommand: "complete",
33
+ sdlcId: args[2],
34
+ inputPath: commandPath(args, 3, ".")
35
+ };
36
+ }
37
+ if (args[0] === "sdlc" && args[1] === "plan" && args[2] === "create") {
38
+ return {
39
+ sdlcCommand: "plan:create",
40
+ sdlcId: args[3],
41
+ sdlcSlug: args[4],
42
+ inputPath: commandPath(args, 5, ".")
43
+ };
44
+ }
45
+ if (args[0] === "sdlc" && args[1] === "plan" && args[2] === "explain") {
46
+ return {
47
+ sdlcCommand: "plan:explain",
48
+ sdlcId: args[3],
49
+ inputPath: commandPath(args, 4, ".")
50
+ };
51
+ }
52
+ if (args[0] === "sdlc" && args[1] === "plan" && args[2] === "step" && args[3] === "transition") {
53
+ return {
54
+ sdlcCommand: "plan:step:transition",
55
+ sdlcId: args[4],
56
+ sdlcStepId: args[5],
57
+ sdlcTargetStatus: args[6],
58
+ inputPath: commandPath(args, 7, ".")
59
+ };
60
+ }
61
+ if (args[0] === "sdlc" && args[1] === "plan" && args[2] === "step" && ["start", "complete", "skip"].includes(args[3])) {
62
+ /** @type {Record<string, string>} */
63
+ const statusByAction = {
64
+ start: "in-progress",
65
+ complete: "done",
66
+ skip: "skipped"
67
+ };
68
+ return {
69
+ sdlcCommand: `plan:step:${args[3]}`,
70
+ sdlcId: args[4],
71
+ sdlcStepId: args[5],
72
+ sdlcTargetStatus: statusByAction[args[3]],
73
+ inputPath: commandPath(args, 6, ".")
74
+ };
75
+ }
10
76
  if (args[0] === "sdlc" && args[1] === "transition") {
11
77
  return {
12
78
  sdlcCommand: "transition",
@@ -31,6 +31,7 @@ export function printImportHelp() {
31
31
  console.log("Examples:");
32
32
  console.log(" topogram import ./existing-app --out ./imported-topogram");
33
33
  console.log(" topogram import ./existing-app --out ./imported-topogram --from db,api,ui");
34
+ console.log(" topogram import ./existing-cli --out ./imported-topogram --from cli");
34
35
  console.log(" topogram import diff ./imported-topogram");
35
36
  console.log(" topogram import refresh ./imported-topogram --from ./existing-app --dry-run");
36
37
  console.log(" topogram import refresh ./imported-topogram --from ./existing-app");