@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,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildVerificationSummary,
|
|
3
|
+
getDefaultEnvironmentProjections,
|
|
4
|
+
resolveRuntimeTopology,
|
|
5
|
+
runtimeUrls,
|
|
6
|
+
selectChecksByVerification
|
|
7
|
+
} from "./shared.js";
|
|
8
|
+
import { getExampleImplementation } from "../../example-implementation.js";
|
|
9
|
+
|
|
10
|
+
function buildRuntimeSmokePlan(graph, options = {}) {
|
|
11
|
+
const implementation = getExampleImplementation(graph, options);
|
|
12
|
+
const runtimeReference = implementation.runtime.reference;
|
|
13
|
+
const runtimeChecks = implementation.runtime.checks;
|
|
14
|
+
const topology = resolveRuntimeTopology(graph, options);
|
|
15
|
+
const { apiProjection, uiProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
16
|
+
const verification = buildVerificationSummary(graph, ["smoke", "journey"]);
|
|
17
|
+
const smokeSelection = selectChecksByVerification(graph, runtimeChecks.smokeChecks, ["smoke", "journey"], {
|
|
18
|
+
keepWebChecks: true
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
type: "runtime_smoke_plan",
|
|
22
|
+
name: runtimeReference.smoke.name,
|
|
23
|
+
projections: {
|
|
24
|
+
api: apiProjection.id,
|
|
25
|
+
ui: uiProjection.id
|
|
26
|
+
},
|
|
27
|
+
topology: {
|
|
28
|
+
components: topology.components.map((component) => ({
|
|
29
|
+
id: component.id,
|
|
30
|
+
type: component.type,
|
|
31
|
+
projection: component.projection.id,
|
|
32
|
+
generator: component.generator
|
|
33
|
+
}))
|
|
34
|
+
},
|
|
35
|
+
...(verification ? { verification } : {}),
|
|
36
|
+
...(smokeSelection.selection ? { selection: smokeSelection.selection } : {}),
|
|
37
|
+
env: {
|
|
38
|
+
apiBase: "TOPOGRAM_API_BASE_URL",
|
|
39
|
+
webBase: "TOPOGRAM_WEB_BASE_URL"
|
|
40
|
+
},
|
|
41
|
+
checks: smokeSelection.checks
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderRuntimeSmokeEnvExample(graph, options = {}) {
|
|
46
|
+
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
47
|
+
const urls = runtimeUrls(runtimeReference, resolveRuntimeTopology(graph, options));
|
|
48
|
+
return `TOPOGRAM_API_BASE_URL=${urls.api}
|
|
49
|
+
TOPOGRAM_WEB_BASE_URL=${urls.web}
|
|
50
|
+
${runtimeReference.environment.envExample || ""}
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderRuntimeSmokeReadme(graph, options = {}) {
|
|
55
|
+
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
56
|
+
const verification = buildVerificationSummary(graph, ["smoke", "journey"]);
|
|
57
|
+
const verificationLines = verification
|
|
58
|
+
? `\n## Canonical Verification\n\n- Sources: ${verification.sources.map((entry) => `\`${entry.id}\``).join(", ")}\n- Scenarios: ${verification.scenarios.map((entry) => entry.label).join(", ")}\n`
|
|
59
|
+
: "";
|
|
60
|
+
return `# ${runtimeReference.smoke.bundleTitle}
|
|
61
|
+
|
|
62
|
+
This bundle gives you lightweight runtime verification for the generated stack.
|
|
63
|
+
|
|
64
|
+
Use it when you want a fast, minimal confidence check that the generated stack is basically up and responding.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
1. Set \`TOPOGRAM_API_BASE_URL\`
|
|
69
|
+
2. Set \`TOPOGRAM_WEB_BASE_URL\`
|
|
70
|
+
3. Run \`bash ./scripts/smoke.sh\`
|
|
71
|
+
|
|
72
|
+
The smoke test will:
|
|
73
|
+
- confirm the web UI responds on \`${runtimeReference.smoke.webPath}\`
|
|
74
|
+
- assume the generated demo seed data has been applied
|
|
75
|
+
- create a primary resource through the API
|
|
76
|
+
- fetch the created primary resource
|
|
77
|
+
- confirm the list endpoint responds
|
|
78
|
+
|
|
79
|
+
If you want staged readiness checks, richer API verification, and a machine-readable report, use the runtime-check bundle instead.
|
|
80
|
+
${verificationLines}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderRuntimeSmokeScript() {
|
|
84
|
+
return `#!/usr/bin/env bash
|
|
85
|
+
set -euo pipefail
|
|
86
|
+
|
|
87
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
88
|
+
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
89
|
+
ENV_FILE="\${TOPOGRAM_ENV_FILE:-$ROOT_DIR/.env}"
|
|
90
|
+
|
|
91
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
92
|
+
set -a
|
|
93
|
+
. "$ENV_FILE"
|
|
94
|
+
set +a
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
node "$SCRIPT_DIR/smoke.mjs"
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderRuntimeSmokeModule(graph, options = {}) {
|
|
102
|
+
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
103
|
+
const containerField = runtimeReference.smoke.createPayload.containerField;
|
|
104
|
+
const payloadEntries = [
|
|
105
|
+
["title", runtimeReference.smoke.createPayload.title],
|
|
106
|
+
[containerField, "__DEMO_CONTAINER_ID__"],
|
|
107
|
+
...Object.entries(runtimeReference.smoke.createPayload.extraFields || {})
|
|
108
|
+
];
|
|
109
|
+
const payloadLines = payloadEntries.map(([key, value]) => {
|
|
110
|
+
if (value === "__DEMO_CONTAINER_ID__") {
|
|
111
|
+
return ` ${key}: demoContainerId`;
|
|
112
|
+
}
|
|
113
|
+
if (value === "__DEMO_USER_ID__") {
|
|
114
|
+
return ` ${key}: demoUserId`;
|
|
115
|
+
}
|
|
116
|
+
return ` ${key}: ${JSON.stringify(value)}`;
|
|
117
|
+
}).join(",\n");
|
|
118
|
+
return `function reportFatal(error) {
|
|
119
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.on("uncaughtException", reportFatal);
|
|
124
|
+
process.on("unhandledRejection", reportFatal);
|
|
125
|
+
|
|
126
|
+
const apiBase = process.env.TOPOGRAM_API_BASE_URL || "";
|
|
127
|
+
const webBase = process.env.TOPOGRAM_WEB_BASE_URL || "";
|
|
128
|
+
const demoContainerId = process.env.${runtimeReference.smoke.defaultContainerEnvVar} || "${runtimeReference.demoEnv.containerId}";
|
|
129
|
+
const demoUserId = process.env.TOPOGRAM_DEMO_USER_ID || "${runtimeReference.demoEnv.userId}";
|
|
130
|
+
const authToken = process.env.TOPOGRAM_AUTH_TOKEN || "";
|
|
131
|
+
|
|
132
|
+
if (!apiBase || !webBase) {
|
|
133
|
+
throw new Error("TOPOGRAM_API_BASE_URL and TOPOGRAM_WEB_BASE_URL are required");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stackStartHint() {
|
|
137
|
+
return "Start the generated stack with 'npm run dev' from the app bundle, or 'npm run app:dev' from the project root, then rerun this command.";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function describeFetchError(error) {
|
|
141
|
+
if (error?.cause?.code) {
|
|
142
|
+
return error.cause.code;
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(error?.cause?.errors) && error.cause.errors.length > 0) {
|
|
145
|
+
return [...new Set(error.cause.errors.map((entry) => entry.code).filter(Boolean))].join(", ");
|
|
146
|
+
}
|
|
147
|
+
return error instanceof Error ? error.message : String(error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fetchWithStackHint(url, init, label) {
|
|
151
|
+
try {
|
|
152
|
+
return await fetch(url, init);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new Error(\`\${label} is not reachable at \${url.toString()}. \${stackStartHint()} Original error: \${describeFetchError(error)}\`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function expectStatus(response, expected, label) {
|
|
159
|
+
if (response.status !== expected) {
|
|
160
|
+
const body = await response.text();
|
|
161
|
+
throw new Error(\`\${label} expected \${expected}, got \${response.status}: \${body}\`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const webResponse = await fetchWithStackHint(new URL("${runtimeReference.smoke.webPath}", webBase), undefined, "web app");
|
|
166
|
+
await expectStatus(webResponse, 200, "web page");
|
|
167
|
+
const webText = await webResponse.text();
|
|
168
|
+
if (!webText.includes("${runtimeReference.smoke.expectText}")) {
|
|
169
|
+
throw new Error("web page did not include expected page text");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const createResponse = await fetchWithStackHint(new URL("${runtimeReference.smoke.createPath}", apiBase), {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"content-type": "application/json",
|
|
176
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
177
|
+
...(authToken ? { Authorization: \`Bearer \${authToken}\` } : {})
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
${payloadLines}
|
|
181
|
+
})
|
|
182
|
+
}, "api service");
|
|
183
|
+
await expectStatus(createResponse, 201, "create resource");
|
|
184
|
+
const created = await createResponse.json();
|
|
185
|
+
if (!created.id) {
|
|
186
|
+
throw new Error("create resource response did not include id");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const getResponse = await fetchWithStackHint(new URL(\`${runtimeReference.smoke.getPathPrefix}\${created.id}\`, apiBase), {
|
|
190
|
+
headers: authToken ? { Authorization: \`Bearer \${authToken}\` } : undefined
|
|
191
|
+
}, "api service");
|
|
192
|
+
await expectStatus(getResponse, 200, "get resource");
|
|
193
|
+
|
|
194
|
+
const listResponse = await fetchWithStackHint(new URL("${runtimeReference.smoke.listPath}", apiBase), {
|
|
195
|
+
headers: authToken ? { Authorization: \`Bearer \${authToken}\` } : undefined
|
|
196
|
+
}, "api service");
|
|
197
|
+
await expectStatus(listResponse, 200, "list resources");
|
|
198
|
+
|
|
199
|
+
console.log(JSON.stringify({
|
|
200
|
+
ok: true,
|
|
201
|
+
createdPrimaryId: created.id
|
|
202
|
+
}, null, 2));
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function generateRuntimeSmokeBundle(graph, options = {}) {
|
|
207
|
+
const plan = buildRuntimeSmokePlan(graph, options);
|
|
208
|
+
return {
|
|
209
|
+
".env.example": renderRuntimeSmokeEnvExample(graph, options),
|
|
210
|
+
"README.md": renderRuntimeSmokeReadme(graph, options),
|
|
211
|
+
"runtime-smoke-plan.json": `${JSON.stringify(plan, null, 2)}\n`,
|
|
212
|
+
"scripts/smoke.sh": renderRuntimeSmokeScript(),
|
|
213
|
+
"scripts/smoke.mjs": renderRuntimeSmokeModule(graph, options)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function generateRuntimeSmokePlan(graph, options = {}) {
|
|
218
|
+
return buildRuntimeSmokePlan(graph, options);
|
|
219
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { fieldSignature, symbolList } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
4
|
+
|
|
5
|
+
function indexStatements(graph) {
|
|
6
|
+
const byId = new Map();
|
|
7
|
+
for (const statement of graph.statements) {
|
|
8
|
+
byId.set(statement.id, statement);
|
|
9
|
+
}
|
|
10
|
+
return byId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scalarSchema(typeName, byId) {
|
|
14
|
+
switch (typeName) {
|
|
15
|
+
case "string":
|
|
16
|
+
case "text":
|
|
17
|
+
return { type: "string" };
|
|
18
|
+
case "integer":
|
|
19
|
+
return { type: "integer" };
|
|
20
|
+
case "number":
|
|
21
|
+
return { type: "number" };
|
|
22
|
+
case "boolean":
|
|
23
|
+
return { type: "boolean" };
|
|
24
|
+
case "datetime":
|
|
25
|
+
return { type: "string", format: "date-time" };
|
|
26
|
+
case "uuid":
|
|
27
|
+
return { type: "string", format: "uuid" };
|
|
28
|
+
default: {
|
|
29
|
+
const target = byId.get(typeName);
|
|
30
|
+
if (target?.kind === "enum") {
|
|
31
|
+
return { type: "string", enum: target.values };
|
|
32
|
+
}
|
|
33
|
+
return { type: "string", "x-topogram-type": typeName };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function coerceDefaultValue(value, schema) {
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (schema.type === "integer") {
|
|
44
|
+
const parsed = Number.parseInt(value, 10);
|
|
45
|
+
return Number.isNaN(parsed) ? value : parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (schema.type === "number") {
|
|
49
|
+
const parsed = Number.parseFloat(value);
|
|
50
|
+
return Number.isNaN(parsed) ? value : parsed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schema.type === "boolean") {
|
|
54
|
+
if (value === "true") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (value === "false") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function schemaForField(field, byId) {
|
|
66
|
+
const schema = scalarSchema(field.fieldType, byId);
|
|
67
|
+
const defaultValue = coerceDefaultValue(field.defaultValue, schema);
|
|
68
|
+
return defaultValue === undefined ? schema : { ...schema, default: defaultValue };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateShapeJsonSchema(shape, byId) {
|
|
72
|
+
const fields = shape.projectedFields || shape.fields || [];
|
|
73
|
+
const properties = {};
|
|
74
|
+
const required = [];
|
|
75
|
+
|
|
76
|
+
for (const field of fields) {
|
|
77
|
+
properties[field.name] = schemaForField(field, byId);
|
|
78
|
+
if (field.requiredness === "required") {
|
|
79
|
+
required.push(field.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const schema = {
|
|
84
|
+
$schema: JSON_SCHEMA_DRAFT,
|
|
85
|
+
$id: `topogram:shape:${shape.id}`,
|
|
86
|
+
title: shape.name || shape.id,
|
|
87
|
+
description: shape.description || undefined,
|
|
88
|
+
type: "object",
|
|
89
|
+
properties,
|
|
90
|
+
additionalProperties: false
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (required.length > 0) {
|
|
94
|
+
schema.required = required;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return schema;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getShape(graph, shapeId) {
|
|
101
|
+
const byId = indexStatements(graph);
|
|
102
|
+
const shape = byId.get(shapeId);
|
|
103
|
+
if (!shape || shape.kind !== "shape") {
|
|
104
|
+
throw new Error(`No shape found with id '${shapeId}'`);
|
|
105
|
+
}
|
|
106
|
+
return shape;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function generateJsonSchema(graph, options = {}) {
|
|
110
|
+
const byId = indexStatements(graph);
|
|
111
|
+
const shapes = graph.byKind.shape || [];
|
|
112
|
+
|
|
113
|
+
if (options.shapeId) {
|
|
114
|
+
return generateShapeJsonSchema(getShape(graph, options.shapeId), byId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const output = {};
|
|
118
|
+
for (const shape of shapes) {
|
|
119
|
+
output[shape.id] = generateShapeJsonSchema(shape, byId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function generateShapeTransformGraph(graph, options = {}) {
|
|
126
|
+
const shapes = graph.byKind.shape || [];
|
|
127
|
+
|
|
128
|
+
if (options.shapeId) {
|
|
129
|
+
return getShape(graph, options.shapeId).transformGraph;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const output = {};
|
|
133
|
+
for (const shape of shapes) {
|
|
134
|
+
output[shape.id] = shape.transformGraph;
|
|
135
|
+
}
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function generateShapeTransformDebug(graph, options = {}) {
|
|
140
|
+
const shapes = options.shapeId ? [getShape(graph, options.shapeId)] : graph.byKind.shape || [];
|
|
141
|
+
const lines = [];
|
|
142
|
+
|
|
143
|
+
lines.push("# Shape Transform Debug");
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(`Generated from \`${graph.root}\``);
|
|
146
|
+
lines.push("");
|
|
147
|
+
|
|
148
|
+
for (const shape of shapes) {
|
|
149
|
+
lines.push(`## \`${shape.id}\` - ${shape.name || shape.id}`);
|
|
150
|
+
lines.push("");
|
|
151
|
+
if (shape.description) {
|
|
152
|
+
lines.push(shape.description);
|
|
153
|
+
lines.push("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push(`Selection mode: \`${shape.transformGraph.selection.mode}\``);
|
|
157
|
+
if (shape.transformGraph.selection.source?.id) {
|
|
158
|
+
lines.push(`Source: \`${shape.transformGraph.selection.source.id}\``);
|
|
159
|
+
}
|
|
160
|
+
lines.push(`Include: ${symbolList(shape.include)}`);
|
|
161
|
+
lines.push(`Exclude: ${symbolList(shape.exclude)}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
|
|
164
|
+
lines.push("Selected fields:");
|
|
165
|
+
for (const field of shape.transformGraph.selection.selectedFields) {
|
|
166
|
+
lines.push(`- ${fieldSignature(field)}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
|
|
170
|
+
lines.push("Transforms:");
|
|
171
|
+
if (shape.transformGraph.transforms.length === 0) {
|
|
172
|
+
lines.push("- _none_");
|
|
173
|
+
} else {
|
|
174
|
+
for (const transform of shape.transformGraph.transforms) {
|
|
175
|
+
if (transform.type === "rename_field") {
|
|
176
|
+
lines.push(`- rename \`${transform.from}\` -> \`${transform.to}\``);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (transform.type === "override_field") {
|
|
180
|
+
const changes = [];
|
|
181
|
+
if (transform.changes.requiredness) {
|
|
182
|
+
changes.push(transform.changes.requiredness);
|
|
183
|
+
}
|
|
184
|
+
if (transform.changes.fieldType) {
|
|
185
|
+
changes.push(`type \`${transform.changes.fieldType}\``);
|
|
186
|
+
}
|
|
187
|
+
if (transform.changes.defaultValue != null) {
|
|
188
|
+
changes.push(`default \`${transform.changes.defaultValue}\``);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`- override \`${transform.field}\`: ${changes.join(", ")}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
|
|
196
|
+
lines.push("Result fields:");
|
|
197
|
+
for (const field of shape.transformGraph.resultFields) {
|
|
198
|
+
lines.push(`- ${fieldSignature(field)}`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
204
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Kanban-style status board for SDLC artifacts.
|
|
2
|
+
//
|
|
3
|
+
// Output shape: rows = status columns, columns = artifact summaries.
|
|
4
|
+
// Filter to a single kind via `--kind pitch|task|bug`. Default lists all
|
|
5
|
+
// SDLC kinds in parallel. Archived entries are hidden by default (callers
|
|
6
|
+
// pass `includeArchived: true` to surface them).
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
summarizeBug,
|
|
10
|
+
summarizePitch,
|
|
11
|
+
summarizeRequirement,
|
|
12
|
+
summarizeTask
|
|
13
|
+
} from "../context/shared.js";
|
|
14
|
+
import {
|
|
15
|
+
defaultActiveStatuses,
|
|
16
|
+
filterStatements
|
|
17
|
+
} from "../../sdlc/status-filter.js";
|
|
18
|
+
|
|
19
|
+
const SUMMARIZERS = {
|
|
20
|
+
pitch: summarizePitch,
|
|
21
|
+
requirement: summarizeRequirement,
|
|
22
|
+
task: summarizeTask,
|
|
23
|
+
bug: summarizeBug
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SUPPORTED_KINDS = Object.keys(SUMMARIZERS);
|
|
27
|
+
|
|
28
|
+
function buildLanesForKind(graph, kind, options) {
|
|
29
|
+
const all = graph.byKind?.[kind] || [];
|
|
30
|
+
const visible = filterStatements(all, options);
|
|
31
|
+
const lanes = {};
|
|
32
|
+
const statuses = defaultActiveStatuses(kind);
|
|
33
|
+
if (statuses) {
|
|
34
|
+
for (const status of statuses) {
|
|
35
|
+
lanes[status] = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const summarize = SUMMARIZERS[kind];
|
|
39
|
+
for (const item of visible) {
|
|
40
|
+
if (!lanes[item.status]) lanes[item.status] = [];
|
|
41
|
+
lanes[item.status].push(summarize(item));
|
|
42
|
+
}
|
|
43
|
+
return lanes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function generateSdlcBoard(graph, options = {}) {
|
|
47
|
+
const wantedKinds = options.kind ? [options.kind] : SUPPORTED_KINDS;
|
|
48
|
+
const board = {};
|
|
49
|
+
const counts = {};
|
|
50
|
+
for (const kind of wantedKinds) {
|
|
51
|
+
if (!SUMMARIZERS[kind]) {
|
|
52
|
+
throw new Error(`Unsupported board kind '${kind}' (allowed: ${SUPPORTED_KINDS.join(", ")})`);
|
|
53
|
+
}
|
|
54
|
+
board[kind] = buildLanesForKind(graph, kind, options);
|
|
55
|
+
counts[kind] = Object.fromEntries(
|
|
56
|
+
Object.entries(board[kind]).map(([status, items]) => [status, items.length])
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: "sdlc_board",
|
|
61
|
+
version: 1,
|
|
62
|
+
kinds: wantedKinds,
|
|
63
|
+
counts,
|
|
64
|
+
board
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Render a single document as a published-page markdown artifact with a
|
|
2
|
+
// cross-reference sidebar (related entities, capabilities, projections,
|
|
3
|
+
// rules pulled from the doc's frontmatter back-links).
|
|
4
|
+
|
|
5
|
+
import { documentById } from "../context/shared.js";
|
|
6
|
+
|
|
7
|
+
function bulletList(label, ids) {
|
|
8
|
+
if (!ids || ids.length === 0) return "";
|
|
9
|
+
const sorted = [...ids].sort();
|
|
10
|
+
const items = sorted.map((id) => `- \`${id}\``).join("\n");
|
|
11
|
+
return `### ${label}\n${items}\n\n`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function generateSdlcDocPage(graph, options = {}) {
|
|
15
|
+
if (!options.documentId) {
|
|
16
|
+
throw new Error("sdlc-doc-page requires --document <id>");
|
|
17
|
+
}
|
|
18
|
+
const doc = documentById(graph, options.documentId);
|
|
19
|
+
if (!doc) {
|
|
20
|
+
throw new Error(`No document found with id '${options.documentId}'`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const headerLines = [
|
|
24
|
+
`# ${doc.title || doc.id}`,
|
|
25
|
+
"",
|
|
26
|
+
`**Status:** ${doc.status || "draft"} `,
|
|
27
|
+
doc.kind ? `**Kind:** ${doc.kind} ` : null,
|
|
28
|
+
doc.summary ? `**Summary:** ${doc.summary} ` : null,
|
|
29
|
+
doc.domain ? `**Domain:** \`${doc.domain}\` ` : null,
|
|
30
|
+
"",
|
|
31
|
+
"---",
|
|
32
|
+
""
|
|
33
|
+
].filter((l) => l !== null);
|
|
34
|
+
|
|
35
|
+
let sidebar = "";
|
|
36
|
+
sidebar += bulletList("Related entities", doc.relatedEntities);
|
|
37
|
+
sidebar += bulletList("Related capabilities", doc.relatedCapabilities);
|
|
38
|
+
sidebar += bulletList("Related projections", doc.relatedProjections);
|
|
39
|
+
sidebar += bulletList("Related rules", doc.relatedRules);
|
|
40
|
+
sidebar += bulletList("Related actors", doc.relatedActors);
|
|
41
|
+
sidebar += bulletList("Related roles", doc.relatedRoles);
|
|
42
|
+
|
|
43
|
+
const body = doc.body || "";
|
|
44
|
+
const markdown = headerLines.join("\n") + body + (sidebar ? "\n\n## Cross-references\n\n" + sidebar : "");
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
type: "sdlc_doc_page",
|
|
48
|
+
version: 1,
|
|
49
|
+
document_id: doc.id,
|
|
50
|
+
output_path: `topogram/docs-generated/sdlc/${doc.id}.md`,
|
|
51
|
+
markdown
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Dispatch for SDLC generator targets.
|
|
2
|
+
|
|
3
|
+
import { generateSdlcBoard } from "./board.js";
|
|
4
|
+
import { generateSdlcDocPage } from "./doc-page.js";
|
|
5
|
+
import { generateSdlcReleaseNotes } from "./release-notes.js";
|
|
6
|
+
import { generateSdlcTraceabilityMatrix } from "./traceability-matrix.js";
|
|
7
|
+
|
|
8
|
+
export function generateSdlcTarget(target, graph, options = {}) {
|
|
9
|
+
switch (target) {
|
|
10
|
+
case "sdlc-board":
|
|
11
|
+
return generateSdlcBoard(graph, options);
|
|
12
|
+
case "sdlc-doc-page":
|
|
13
|
+
return generateSdlcDocPage(graph, options);
|
|
14
|
+
case "sdlc-release-notes":
|
|
15
|
+
return generateSdlcReleaseNotes(graph, options);
|
|
16
|
+
case "sdlc-traceability-matrix":
|
|
17
|
+
return generateSdlcTraceabilityMatrix(graph, options);
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unsupported SDLC target '${target}'`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { generateSdlcBoard, generateSdlcDocPage, generateSdlcReleaseNotes, generateSdlcTraceabilityMatrix };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Release notes generator.
|
|
2
|
+
//
|
|
3
|
+
// Assembles the changes that landed in a release window from:
|
|
4
|
+
// - Approved pitches whose `affects` was touched
|
|
5
|
+
// - Tasks in `done` status (or archived in this window)
|
|
6
|
+
// - Bugs in `verified` or `wont-fix` (or archived in this window)
|
|
7
|
+
//
|
|
8
|
+
// Inputs:
|
|
9
|
+
// options.appVersion — required; release version label
|
|
10
|
+
// options.sinceTag — optional; ISO date or git-tag-like marker for window start
|
|
11
|
+
// options.includeArchived — optional; default true (release notes need archived data)
|
|
12
|
+
//
|
|
13
|
+
// Output: structured JSON the caller can render to markdown or a board.
|
|
14
|
+
|
|
15
|
+
function withinWindow(updated, sinceIso) {
|
|
16
|
+
if (!sinceIso) return true;
|
|
17
|
+
if (!updated) return false;
|
|
18
|
+
return updated >= sinceIso;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function summarizeMinimal(s) {
|
|
22
|
+
return {
|
|
23
|
+
id: s.id,
|
|
24
|
+
name: s.name,
|
|
25
|
+
status: s.status,
|
|
26
|
+
priority: s.priority || null,
|
|
27
|
+
severity: s.severity || null,
|
|
28
|
+
domain: s.resolvedDomain ? s.resolvedDomain.id : null,
|
|
29
|
+
updated: s.updated || null,
|
|
30
|
+
archived: !!s.archived
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function generateSdlcReleaseNotes(graph, options = {}) {
|
|
35
|
+
if (!options.appVersion) {
|
|
36
|
+
throw new Error("sdlc-release-notes requires --app-version <label>");
|
|
37
|
+
}
|
|
38
|
+
const sinceIso = options.sinceTag || options.since || null;
|
|
39
|
+
|
|
40
|
+
const pitches = (graph.byKind?.pitch || [])
|
|
41
|
+
.filter((p) => p.status === "approved" && withinWindow(p.updated, sinceIso))
|
|
42
|
+
.map(summarizeMinimal);
|
|
43
|
+
|
|
44
|
+
const tasks = (graph.byKind?.task || [])
|
|
45
|
+
.filter((t) => (t.status === "done" || t.archived) && withinWindow(t.updated, sinceIso))
|
|
46
|
+
.map(summarizeMinimal);
|
|
47
|
+
|
|
48
|
+
const bugs = (graph.byKind?.bug || [])
|
|
49
|
+
.filter((b) => (b.status === "verified" || b.status === "wont-fix" || b.archived) && withinWindow(b.updated, sinceIso))
|
|
50
|
+
.map(summarizeMinimal);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
type: "sdlc_release_notes",
|
|
54
|
+
version: 1,
|
|
55
|
+
app_version: options.appVersion,
|
|
56
|
+
since: sinceIso,
|
|
57
|
+
counts: { pitches: pitches.length, tasks: tasks.length, bugs: bugs.length },
|
|
58
|
+
pitches,
|
|
59
|
+
tasks,
|
|
60
|
+
bugs
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Pitch → Requirement → AC → (Task | Bug) → Verification table.
|
|
2
|
+
//
|
|
3
|
+
// Each row is one acceptance criterion; columns are the chain anchored on
|
|
4
|
+
// it. This is the canonical "did everything we shipped have a reason and a
|
|
5
|
+
// verification?" report.
|
|
6
|
+
|
|
7
|
+
export function generateSdlcTraceabilityMatrix(graph, options = {}) {
|
|
8
|
+
const acs = graph.byKind?.acceptance_criterion || [];
|
|
9
|
+
const requirementsById = new Map((graph.byKind?.requirement || []).map((r) => [r.id, r]));
|
|
10
|
+
const pitchesById = new Map((graph.byKind?.pitch || []).map((p) => [p.id, p]));
|
|
11
|
+
const tasksById = new Map((graph.byKind?.task || []).map((t) => [t.id, t]));
|
|
12
|
+
const bugsById = new Map((graph.byKind?.bug || []).map((b) => [b.id, b]));
|
|
13
|
+
const verificationsById = new Map((graph.byKind?.verification || []).map((v) => [v.id, v]));
|
|
14
|
+
|
|
15
|
+
const rows = [];
|
|
16
|
+
for (const ac of acs) {
|
|
17
|
+
const requirement = ac.requirement?.id ? requirementsById.get(ac.requirement.id) : null;
|
|
18
|
+
const pitchId = requirement?.pitch?.id || null;
|
|
19
|
+
const pitch = pitchId ? pitchesById.get(pitchId) : null;
|
|
20
|
+
|
|
21
|
+
const taskIds = (ac.tasks || []).slice().sort();
|
|
22
|
+
const verificationIds = (ac.verifications || []).slice().sort();
|
|
23
|
+
|
|
24
|
+
const linkedBugIds = [];
|
|
25
|
+
for (const bug of bugsById.values()) {
|
|
26
|
+
const linkedTask = (bug.fixedIn || []).some((ref) => taskIds.includes(typeof ref === "string" ? ref : ref?.id));
|
|
27
|
+
if (linkedTask) linkedBugIds.push(bug.id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rows.push({
|
|
31
|
+
pitch: pitch
|
|
32
|
+
? { id: pitch.id, status: pitch.status, name: pitch.name }
|
|
33
|
+
: null,
|
|
34
|
+
requirement: requirement
|
|
35
|
+
? { id: requirement.id, status: requirement.status, name: requirement.name }
|
|
36
|
+
: null,
|
|
37
|
+
acceptance_criterion: { id: ac.id, status: ac.status, name: ac.name },
|
|
38
|
+
tasks: taskIds.map((id) => {
|
|
39
|
+
const t = tasksById.get(id);
|
|
40
|
+
return t ? { id, status: t.status, work_type: t.workType } : { id, status: "missing" };
|
|
41
|
+
}),
|
|
42
|
+
bugs: linkedBugIds.sort().map((id) => {
|
|
43
|
+
const b = bugsById.get(id);
|
|
44
|
+
return { id, status: b?.status, severity: b?.severity };
|
|
45
|
+
}),
|
|
46
|
+
verifications: verificationIds.map((id) => {
|
|
47
|
+
const v = verificationsById.get(id);
|
|
48
|
+
return v ? { id, method: v.method, status: v.status } : { id, status: "missing" };
|
|
49
|
+
}),
|
|
50
|
+
gap: !verificationIds.length || !taskIds.length
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
rows.sort((a, b) => (a.acceptance_criterion.id || "").localeCompare(b.acceptance_criterion.id || ""));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
type: "sdlc_traceability_matrix",
|
|
58
|
+
version: 1,
|
|
59
|
+
counts: {
|
|
60
|
+
rows: rows.length,
|
|
61
|
+
gaps: rows.filter((r) => r.gap).length
|
|
62
|
+
},
|
|
63
|
+
rows
|
|
64
|
+
};
|
|
65
|
+
}
|