@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,89 @@
|
|
|
1
|
+
// `sdlc new <kind> <slug>` — scaffold a new SDLC `.tg` file with sensible
|
|
2
|
+
// defaults so the author can fill in details rather than start blank.
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const TEMPLATES = {
|
|
8
|
+
pitch: (slug) => `pitch pitch_${slug} {
|
|
9
|
+
name "${humanize(slug)}"
|
|
10
|
+
description "Why this work matters."
|
|
11
|
+
appetite "TBD — small / medium / large"
|
|
12
|
+
problem "Describe the user-visible problem."
|
|
13
|
+
solution_sketch "Describe the proposed shape, not the implementation."
|
|
14
|
+
rabbit_holes "Known traps to avoid."
|
|
15
|
+
no_go_areas "Things this pitch deliberately won't touch."
|
|
16
|
+
affects []
|
|
17
|
+
priority medium
|
|
18
|
+
status draft
|
|
19
|
+
}
|
|
20
|
+
`,
|
|
21
|
+
requirement: (slug) => `requirement req_${slug} {
|
|
22
|
+
name "${humanize(slug)}"
|
|
23
|
+
description "What the system must do."
|
|
24
|
+
affects []
|
|
25
|
+
introduces_rules []
|
|
26
|
+
respects_rules []
|
|
27
|
+
priority medium
|
|
28
|
+
status draft
|
|
29
|
+
}
|
|
30
|
+
`,
|
|
31
|
+
acceptance_criterion: (slug) => `acceptance_criterion ac_${slug} {
|
|
32
|
+
name "${humanize(slug)}"
|
|
33
|
+
description "Given <state>, when <action>, then <observable>."
|
|
34
|
+
requirement req_TODO
|
|
35
|
+
status draft
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
task: (slug) => `task task_${slug} {
|
|
39
|
+
name "${humanize(slug)}"
|
|
40
|
+
description "What the agent or human will do."
|
|
41
|
+
satisfies []
|
|
42
|
+
acceptance_refs []
|
|
43
|
+
affects []
|
|
44
|
+
blocked_by []
|
|
45
|
+
priority medium
|
|
46
|
+
work_type implementation
|
|
47
|
+
status unclaimed
|
|
48
|
+
}
|
|
49
|
+
`,
|
|
50
|
+
bug: (slug) => `bug bug_${slug} {
|
|
51
|
+
name "${humanize(slug)}"
|
|
52
|
+
description "What goes wrong."
|
|
53
|
+
reproduction "Steps to reproduce."
|
|
54
|
+
affects []
|
|
55
|
+
violates []
|
|
56
|
+
priority medium
|
|
57
|
+
severity medium
|
|
58
|
+
status open
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function humanize(slug) {
|
|
64
|
+
return slug
|
|
65
|
+
.replace(/[_-]+/g, " ")
|
|
66
|
+
.replace(/\b\w/g, (m) => m.toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function scaffoldNew(workspaceRoot, kind, slug) {
|
|
70
|
+
if (!TEMPLATES[kind]) {
|
|
71
|
+
return { ok: false, error: `Unsupported kind '${kind}' (allowed: ${Object.keys(TEMPLATES).join(", ")})` };
|
|
72
|
+
}
|
|
73
|
+
if (!slug || !/^[a-z][a-z0-9_]*$/.test(slug)) {
|
|
74
|
+
return { ok: false, error: `Invalid slug '${slug}' — must match /^[a-z][a-z0-9_]*$/` };
|
|
75
|
+
}
|
|
76
|
+
const targetDir = path.join(workspaceRoot, "topogram", `${kind === "acceptance_criterion" ? "acceptance_criteria" : kind + "s"}`);
|
|
77
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
78
|
+
const targetFile = path.join(targetDir, `${slug}.tg`);
|
|
79
|
+
if (existsSync(targetFile)) {
|
|
80
|
+
return { ok: false, error: `Refusing to overwrite '${targetFile}'` };
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(targetFile, TEMPLATES[kind](slug), "utf8");
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
kind,
|
|
86
|
+
slug,
|
|
87
|
+
file: targetFile
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Default-active status filtering for SDLC queries.
|
|
2
|
+
//
|
|
3
|
+
// Most CLI surfaces (board, slice, traceability) want to omit terminal
|
|
4
|
+
// statuses unless the caller passes `--include-status` or
|
|
5
|
+
// `--include-archived`. This module centralizes the rules.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isArchivableStatus,
|
|
9
|
+
isTerminalStatus
|
|
10
|
+
} from "./transitions/index.js";
|
|
11
|
+
|
|
12
|
+
const ALWAYS_VISIBLE_TERMINAL_STATUSES = new Set([
|
|
13
|
+
"approved",
|
|
14
|
+
"superseded",
|
|
15
|
+
"verified",
|
|
16
|
+
"wont-fix",
|
|
17
|
+
"done"
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function defaultActiveStatuses(kind) {
|
|
21
|
+
// For a given kind, the statuses that show up in queries by default.
|
|
22
|
+
// Terminal statuses are still visible if they aren't archive-eligible
|
|
23
|
+
// (e.g. requirement.superseded is terminal but stays for traceability).
|
|
24
|
+
switch (kind) {
|
|
25
|
+
case "pitch":
|
|
26
|
+
return new Set(["draft", "shaped", "submitted", "approved"]);
|
|
27
|
+
case "requirement":
|
|
28
|
+
return new Set(["draft", "in-review", "approved", "superseded"]);
|
|
29
|
+
case "acceptance_criterion":
|
|
30
|
+
return new Set(["draft", "approved", "superseded"]);
|
|
31
|
+
case "task":
|
|
32
|
+
return new Set(["unclaimed", "claimed", "in-progress", "blocked"]);
|
|
33
|
+
case "bug":
|
|
34
|
+
return new Set(["open", "in-progress", "fixed"]);
|
|
35
|
+
case "document":
|
|
36
|
+
return new Set(["draft", "review", "published"]);
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function filterStatements(statements, options = {}) {
|
|
43
|
+
const includeArchived = options.includeArchived === true;
|
|
44
|
+
const includeStatuses = options.includeStatuses ? new Set(options.includeStatuses) : null;
|
|
45
|
+
return statements.filter((s) => {
|
|
46
|
+
if (s.archived && !includeArchived) return false;
|
|
47
|
+
const defaults = defaultActiveStatuses(s.kind);
|
|
48
|
+
if (!defaults) return true;
|
|
49
|
+
if (includeStatuses && includeStatuses.has(s.status)) return true;
|
|
50
|
+
return defaults.has(s.status);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { isTerminalStatus, isArchivableStatus, ALWAYS_VISIBLE_TERMINAL_STATUSES };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SDLC transition orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// `transitionStatement(workspace, id, targetStatus, options)`:
|
|
4
|
+
// 1. Locate the statement in the resolved graph.
|
|
5
|
+
// 2. Validate the transition with the kind's state machine.
|
|
6
|
+
// 3. Run DoD (Definition of Done) checks. Errors block; warnings advise.
|
|
7
|
+
// 4. Surgically rewrite the `status` value in the source `.tg` file.
|
|
8
|
+
// 5. Append a history record to `.topogram-sdlc-history.json`.
|
|
9
|
+
//
|
|
10
|
+
// The rewrite is byte-precise — we only swap the status symbol token, so
|
|
11
|
+
// formatting, comments, and adjacent fields are untouched.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
import { parsePath } from "../parser.js";
|
|
16
|
+
import { resolveWorkspace } from "../resolver/index.js";
|
|
17
|
+
import { validateTransition, isArchivableStatus } from "./transitions/index.js";
|
|
18
|
+
import { checkDoD } from "./dod/index.js";
|
|
19
|
+
import { appendTransition } from "./history.js";
|
|
20
|
+
|
|
21
|
+
function findStatementInAst(workspaceAst, id) {
|
|
22
|
+
for (const file of workspaceAst.files) {
|
|
23
|
+
for (const statement of file.statements) {
|
|
24
|
+
if (statement.id === id) {
|
|
25
|
+
return statement;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findStatusField(statement) {
|
|
33
|
+
for (const field of statement.fields) {
|
|
34
|
+
if (field.key === "status") return field;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rewriteStatusInSource(source, statusField, newStatus) {
|
|
40
|
+
const start = statusField.value.loc.start.offset;
|
|
41
|
+
const end = statusField.value.loc.end.offset;
|
|
42
|
+
return source.slice(0, start) + newStatus + source.slice(end);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function transitionStatement(workspaceRoot, id, targetStatus, options = {}) {
|
|
46
|
+
const ast = parsePath(workspaceRoot);
|
|
47
|
+
const resolved = resolveWorkspace(ast);
|
|
48
|
+
if (!resolved.ok) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: "workspace failed validation; cannot transition",
|
|
52
|
+
validation: resolved.validation
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const astStatement = findStatementInAst(ast, id);
|
|
57
|
+
if (!astStatement) {
|
|
58
|
+
return { ok: false, error: `Statement '${id}' not found` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resolvedStatement = resolved.graph.statements.find((s) => s.id === id);
|
|
62
|
+
if (!resolvedStatement) {
|
|
63
|
+
return { ok: false, error: `Statement '${id}' not found in resolved graph` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fromStatus = resolvedStatement.status;
|
|
67
|
+
const transition = validateTransition(astStatement.kind, fromStatus, targetStatus);
|
|
68
|
+
if (!transition.ok) {
|
|
69
|
+
return { ok: false, error: transition.error };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const byId = new Map(resolved.graph.statements.map((s) => [s.id, s]));
|
|
73
|
+
const dod = checkDoD(astStatement.kind, resolvedStatement, targetStatus, { byId });
|
|
74
|
+
if (!dod.satisfied && !options.force) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: `Definition of Done not satisfied: ${dod.errors.join("; ")}`,
|
|
78
|
+
dod
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const statusField = findStatusField(astStatement);
|
|
83
|
+
if (!statusField) {
|
|
84
|
+
return { ok: false, error: `Statement '${id}' has no status field to rewrite` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sourcePath = astStatement.loc.file;
|
|
88
|
+
const original = readFileSync(sourcePath, "utf8");
|
|
89
|
+
const rewritten = rewriteStatusInSource(original, statusField, targetStatus);
|
|
90
|
+
|
|
91
|
+
if (!options.dryRun) {
|
|
92
|
+
writeFileSync(sourcePath, rewritten, "utf8");
|
|
93
|
+
appendTransition(workspaceRoot, id, {
|
|
94
|
+
from: fromStatus,
|
|
95
|
+
to: targetStatus,
|
|
96
|
+
by: options.actor || null,
|
|
97
|
+
note: options.note || null
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
id,
|
|
104
|
+
kind: astStatement.kind,
|
|
105
|
+
from: fromStatus,
|
|
106
|
+
to: targetStatus,
|
|
107
|
+
file: sourcePath,
|
|
108
|
+
archivable: isArchivableStatus(astStatement.kind, targetStatus),
|
|
109
|
+
dod,
|
|
110
|
+
dryRun: options.dryRun === true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Acceptance criterion state machine.
|
|
2
|
+
//
|
|
3
|
+
// draft → approved → superseded
|
|
4
|
+
//
|
|
5
|
+
// ACs are always-live like requirements.
|
|
6
|
+
|
|
7
|
+
export const LEGAL_TRANSITIONS = {
|
|
8
|
+
draft: ["approved"],
|
|
9
|
+
approved: ["superseded", "draft"],
|
|
10
|
+
superseded: []
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TERMINAL_STATUSES = new Set(["superseded"]);
|
|
14
|
+
export const ARCHIVABLE_STATUSES = new Set();
|
|
15
|
+
|
|
16
|
+
export function validateTransition(from, to) {
|
|
17
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
18
|
+
if (!allowed) {
|
|
19
|
+
return { ok: false, error: `Unknown acceptance_criterion status '${from}'` };
|
|
20
|
+
}
|
|
21
|
+
if (!allowed.includes(to)) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: `Acceptance criterion cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ") || "(terminal)"}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { ok: true };
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Bug state machine.
|
|
2
|
+
//
|
|
3
|
+
// open → in-progress → fixed → verified
|
|
4
|
+
// └──────────┴────────┴────→ wont-fix
|
|
5
|
+
//
|
|
6
|
+
// `verified` and `wont-fix` are archive-eligible.
|
|
7
|
+
|
|
8
|
+
export const LEGAL_TRANSITIONS = {
|
|
9
|
+
open: ["in-progress", "wont-fix"],
|
|
10
|
+
"in-progress": ["fixed", "open", "wont-fix"],
|
|
11
|
+
fixed: ["verified", "in-progress", "wont-fix"],
|
|
12
|
+
verified: ["in-progress"],
|
|
13
|
+
"wont-fix": ["open"]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const TERMINAL_STATUSES = new Set(["verified", "wont-fix"]);
|
|
17
|
+
export const ARCHIVABLE_STATUSES = new Set(["verified", "wont-fix"]);
|
|
18
|
+
|
|
19
|
+
export function validateTransition(from, to) {
|
|
20
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
21
|
+
if (!allowed) {
|
|
22
|
+
return { ok: false, error: `Unknown bug status '${from}'` };
|
|
23
|
+
}
|
|
24
|
+
if (!allowed.includes(to)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: `Bug cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ")}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Document state machine (mirrors workspace-docs.DOC_STATUSES extension).
|
|
2
|
+
//
|
|
3
|
+
// draft → review → published → archived
|
|
4
|
+
// ↓
|
|
5
|
+
// review (when linked component changes — staleness signal)
|
|
6
|
+
|
|
7
|
+
export const LEGAL_TRANSITIONS = {
|
|
8
|
+
draft: ["review"],
|
|
9
|
+
review: ["published", "draft"],
|
|
10
|
+
published: ["review", "archived"],
|
|
11
|
+
archived: ["draft"]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const TERMINAL_STATUSES = new Set(["archived"]);
|
|
15
|
+
export const ARCHIVABLE_STATUSES = new Set(["archived"]);
|
|
16
|
+
|
|
17
|
+
export function validateTransition(from, to) {
|
|
18
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
19
|
+
if (!allowed) {
|
|
20
|
+
return { ok: false, error: `Unknown document status '${from}'` };
|
|
21
|
+
}
|
|
22
|
+
if (!allowed.includes(to)) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: `Document cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ")}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Per-kind dispatch for SDLC transitions.
|
|
2
|
+
|
|
3
|
+
import * as pitch from "./pitch.js";
|
|
4
|
+
import * as requirement from "./requirement.js";
|
|
5
|
+
import * as acceptanceCriterion from "./acceptance-criterion.js";
|
|
6
|
+
import * as task from "./task.js";
|
|
7
|
+
import * as bug from "./bug.js";
|
|
8
|
+
import * as document from "./document.js";
|
|
9
|
+
|
|
10
|
+
const MODULES = {
|
|
11
|
+
pitch,
|
|
12
|
+
requirement,
|
|
13
|
+
acceptance_criterion: acceptanceCriterion,
|
|
14
|
+
task,
|
|
15
|
+
bug,
|
|
16
|
+
document
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getTransitionModule(kind) {
|
|
20
|
+
return MODULES[kind] || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function legalTransitionsFor(kind, from) {
|
|
24
|
+
const mod = MODULES[kind];
|
|
25
|
+
if (!mod) return [];
|
|
26
|
+
return mod.LEGAL_TRANSITIONS[from] || [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isTerminalStatus(kind, status) {
|
|
30
|
+
const mod = MODULES[kind];
|
|
31
|
+
if (!mod) return false;
|
|
32
|
+
return mod.TERMINAL_STATUSES.has(status);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isArchivableStatus(kind, status) {
|
|
36
|
+
const mod = MODULES[kind];
|
|
37
|
+
if (!mod) return false;
|
|
38
|
+
return mod.ARCHIVABLE_STATUSES.has(status);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateTransition(kind, from, to) {
|
|
42
|
+
const mod = MODULES[kind];
|
|
43
|
+
if (!mod) {
|
|
44
|
+
return { ok: false, error: `No state machine for kind '${kind}'` };
|
|
45
|
+
}
|
|
46
|
+
return mod.validateTransition(from, to);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
pitch,
|
|
51
|
+
requirement,
|
|
52
|
+
acceptanceCriterion,
|
|
53
|
+
task,
|
|
54
|
+
bug,
|
|
55
|
+
document
|
|
56
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Pitch state machine.
|
|
2
|
+
//
|
|
3
|
+
// draft → shaped → submitted → approved
|
|
4
|
+
// ↓ ↓ ↓ ↓
|
|
5
|
+
// └────────┴─────────┴───→ rejected
|
|
6
|
+
//
|
|
7
|
+
// A pitch may skip `shaped` and go straight to `submitted` for small or
|
|
8
|
+
// obvious work (per the SDLC design). All terminal-status pitches stay in
|
|
9
|
+
// the active workspace except `rejected`, which is archive-eligible.
|
|
10
|
+
|
|
11
|
+
export const LEGAL_TRANSITIONS = {
|
|
12
|
+
draft: ["shaped", "submitted", "rejected"],
|
|
13
|
+
shaped: ["submitted", "rejected", "draft"],
|
|
14
|
+
submitted: ["approved", "rejected", "shaped"],
|
|
15
|
+
approved: ["rejected"],
|
|
16
|
+
rejected: ["draft"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const TERMINAL_STATUSES = new Set(["approved", "rejected"]);
|
|
20
|
+
export const ARCHIVABLE_STATUSES = new Set(["rejected"]);
|
|
21
|
+
|
|
22
|
+
export function validateTransition(from, to) {
|
|
23
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
24
|
+
if (!allowed) {
|
|
25
|
+
return { ok: false, error: `Unknown pitch status '${from}'` };
|
|
26
|
+
}
|
|
27
|
+
if (!allowed.includes(to)) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: `Pitch cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ")}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Requirement state machine.
|
|
2
|
+
//
|
|
3
|
+
// draft → in-review → approved → superseded
|
|
4
|
+
//
|
|
5
|
+
// Requirements are always-live: even `superseded` requirements stay in the
|
|
6
|
+
// active workspace because tasks may still reference them in the
|
|
7
|
+
// traceability matrix.
|
|
8
|
+
|
|
9
|
+
export const LEGAL_TRANSITIONS = {
|
|
10
|
+
draft: ["in-review"],
|
|
11
|
+
"in-review": ["approved", "draft"],
|
|
12
|
+
approved: ["superseded", "in-review"],
|
|
13
|
+
superseded: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const TERMINAL_STATUSES = new Set(["superseded"]);
|
|
17
|
+
export const ARCHIVABLE_STATUSES = new Set();
|
|
18
|
+
|
|
19
|
+
export function validateTransition(from, to) {
|
|
20
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
21
|
+
if (!allowed) {
|
|
22
|
+
return { ok: false, error: `Unknown requirement status '${from}'` };
|
|
23
|
+
}
|
|
24
|
+
if (!allowed.includes(to)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: `Requirement cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ") || "(terminal)"}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Task state machine.
|
|
2
|
+
//
|
|
3
|
+
// unclaimed → claimed → in-progress → done
|
|
4
|
+
// ↓ ↓
|
|
5
|
+
// └────→ blocked ←──┘
|
|
6
|
+
//
|
|
7
|
+
// `blocked` is exit-able back to whichever in-flight status the task came
|
|
8
|
+
// from; in practice agents return through `in-progress`. `done` is
|
|
9
|
+
// archive-eligible.
|
|
10
|
+
|
|
11
|
+
export const LEGAL_TRANSITIONS = {
|
|
12
|
+
unclaimed: ["claimed"],
|
|
13
|
+
claimed: ["in-progress", "unclaimed", "blocked"],
|
|
14
|
+
"in-progress": ["done", "blocked", "claimed"],
|
|
15
|
+
blocked: ["claimed", "in-progress"],
|
|
16
|
+
done: []
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const TERMINAL_STATUSES = new Set(["done"]);
|
|
20
|
+
export const ARCHIVABLE_STATUSES = new Set(["done"]);
|
|
21
|
+
|
|
22
|
+
export function validateTransition(from, to) {
|
|
23
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
24
|
+
if (!allowed) {
|
|
25
|
+
return { ok: false, error: `Unknown task status '${from}'` };
|
|
26
|
+
}
|
|
27
|
+
if (!allowed.includes(to)) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: `Task cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ") || "(terminal)"}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|