@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.
- package/README.md +24 -195
- package/package.json +1 -1
- package/src/adoption/plan/index.js +2 -1
- package/src/agent-brief.js +46 -2
- package/src/archive/archive.js +1 -1
- package/src/archive/jsonl.js +18 -8
- package/src/archive/resolver-bridge.js +34 -1
- package/src/archive/schema.js +1 -1
- package/src/archive/unarchive.js +26 -0
- package/src/cli/command-parsers/sdlc.js +66 -0
- package/src/cli/commands/import/help.js +1 -0
- package/src/cli/commands/import/plan.js +9 -0
- package/src/cli/commands/import/workspace.js +3 -0
- package/src/cli/commands/query/definitions.js +11 -10
- package/src/cli/commands/query/workspace.js +23 -2
- package/src/cli/commands/release-rollout.js +191 -10
- package/src/cli/commands/release-shared.js +51 -2
- package/src/cli/commands/release.js +16 -3
- package/src/cli/commands/sdlc.js +213 -5
- package/src/cli/dispatcher.js +8 -0
- package/src/cli/help.js +15 -3
- package/src/cli/options.js +1 -0
- package/src/generator/context/shared/domain-sdlc.js +27 -0
- package/src/generator/context/shared/relationships.js +2 -1
- package/src/generator/context/shared/types.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/context/shared.js +2 -0
- package/src/generator/context/slice/core.js +3 -0
- package/src/generator/context/slice/sdlc.js +57 -2
- package/src/generator/context/task-mode.js +7 -0
- package/src/generator/sdlc/board.js +2 -0
- package/src/generator/sdlc/traceability-matrix.js +5 -1
- package/src/import/core/context.js +1 -1
- package/src/import/core/contracts.js +3 -3
- package/src/import/core/registry.js +3 -0
- package/src/import/core/runner/candidates.js +7 -0
- package/src/import/core/runner/reports.js +9 -1
- package/src/import/core/runner/tracks.js +3 -0
- package/src/import/extractors/cli/generic.js +340 -0
- package/src/new-project/project-files.js +10 -3
- package/src/resolver/enrich/task.js +3 -1
- package/src/resolver/index.js +6 -0
- package/src/resolver/normalize.js +31 -0
- package/src/resolver/projections-cli.js +158 -0
- package/src/sdlc/adopt.js +4 -1
- package/src/sdlc/check.js +24 -2
- package/src/sdlc/complete.js +47 -0
- package/src/sdlc/dod/index.js +2 -0
- package/src/sdlc/dod/plan.js +15 -0
- package/src/sdlc/dod/task.js +7 -3
- package/src/sdlc/explain.js +53 -1
- package/src/sdlc/gate.js +352 -0
- package/src/sdlc/history.d.ts +7 -0
- package/src/sdlc/history.js +50 -5
- package/src/sdlc/link.js +172 -0
- package/src/sdlc/paths.d.ts +4 -0
- package/src/sdlc/paths.js +8 -0
- package/src/sdlc/plan-steps.js +71 -0
- package/src/sdlc/plan.js +245 -0
- package/src/sdlc/policy.js +249 -0
- package/src/sdlc/prep.js +186 -0
- package/src/sdlc/scaffold.js +4 -2
- package/src/sdlc/status-filter.js +2 -0
- package/src/sdlc/transitions/index.js +3 -0
- package/src/sdlc/transitions/plan.js +32 -0
- package/src/validator/common.js +25 -4
- package/src/validator/index.js +10 -0
- package/src/validator/kinds.d.ts +7 -0
- package/src/validator/kinds.js +32 -0
- package/src/validator/per-kind/plan.js +128 -0
- package/src/validator/per-kind/task.js +19 -0
- package/src/validator/projections/cli.js +267 -0
- package/src/validator.d.ts +1 -0
- package/src/workflows/import-app/shared.js +1 -1
- package/src/workflows/reconcile/adoption-plan/build.js +3 -1
- package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
- package/src/workflows/reconcile/bundle-core/index.js +3 -0
- package/src/workflows/reconcile/candidate-model.js +15 -0
- package/src/workflows/reconcile/gap-report.js +4 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +82 -0
- package/src/workflows/reconcile/summary.js +4 -0
- package/src/workflows/reconcile/workflow.js +2 -1
- package/src/workspace-paths.js +26 -2
package/README.md
CHANGED
|
@@ -1,223 +1,52 @@
|
|
|
1
|
-
# Topogram
|
|
1
|
+
# Topogram CLI Package
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This directory is the npm package for `@topogram/cli`. It exposes the
|
|
4
|
+
`topogram` executable.
|
|
4
5
|
|
|
5
|
-
The
|
|
6
|
+
The repo root owns product docs. Start with:
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
13
|
+
## Package shape
|
|
10
14
|
|
|
11
15
|
```json
|
|
12
16
|
{
|
|
13
17
|
"name": "@topogram/cli",
|
|
14
18
|
"bin": {
|
|
15
|
-
"topogram": "
|
|
19
|
+
"topogram": "src/cli.js"
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
## Local development
|
|
21
25
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
## What belongs here
|
|
213
44
|
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
@@ -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;
|
package/src/agent-brief.js
CHANGED
|
@@ -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");
|
package/src/archive/archive.js
CHANGED
|
@@ -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`.
|
package/src/archive/jsonl.js
CHANGED
|
@@ -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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
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 {
|
package/src/archive/schema.js
CHANGED
|
@@ -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);
|
package/src/archive/unarchive.js
CHANGED
|
@@ -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");
|