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