@topogram/cli 0.3.64 → 0.3.65
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/package.json +1 -1
- package/src/adoption/plan/index.js +703 -0
- package/src/adoption/plan.js +12 -703
- package/src/agent-ops/query-builders/auth.js +375 -0
- package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
- package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
- package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
- package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
- package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
- package/src/agent-ops/query-builders/change-risk.js +25 -0
- package/src/agent-ops/query-builders/common.js +149 -0
- package/src/agent-ops/query-builders/maintained-risk.js +539 -0
- package/src/agent-ops/query-builders/maintained-shared.js +120 -0
- package/src/agent-ops/query-builders/multi-agent.js +547 -0
- package/src/agent-ops/query-builders/projection-impacts.js +514 -0
- package/src/agent-ops/query-builders/work-packets.js +417 -0
- package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
- package/src/agent-ops/query-builders/workflow-context.js +398 -0
- package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -26
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +60 -0
- package/src/catalog/diagnostics.js +15 -0
- package/src/catalog/entries.js +42 -0
- package/src/catalog/files.js +67 -0
- package/src/catalog/provenance.js +122 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +2 -0
- package/src/catalog.js +18 -746
- package/src/cli/commands/catalog/check.js +31 -0
- package/src/cli/commands/catalog/copy.js +59 -0
- package/src/cli/commands/catalog/doctor.js +248 -0
- package/src/cli/commands/catalog/help.js +21 -0
- package/src/cli/commands/catalog/list.js +52 -0
- package/src/cli/commands/catalog/runner.js +92 -0
- package/src/cli/commands/catalog/shared.js +17 -0
- package/src/cli/commands/catalog/show.js +134 -0
- package/src/cli/commands/catalog.js +30 -615
- package/src/cli/commands/generator-policy/package-info.js +162 -0
- package/src/cli/commands/generator-policy/payloads.js +372 -0
- package/src/cli/commands/generator-policy/printers.js +159 -0
- package/src/cli/commands/generator-policy/runner.js +81 -0
- package/src/cli/commands/generator-policy/shared.js +39 -0
- package/src/cli/commands/generator-policy.js +15 -783
- package/src/cli/commands/import/adopt.js +170 -0
- package/src/cli/commands/import/check.js +91 -0
- package/src/cli/commands/import/diff.js +84 -0
- package/src/cli/commands/import/help.js +47 -0
- package/src/cli/commands/import/paths.js +277 -0
- package/src/cli/commands/import/plan.js +284 -0
- package/src/cli/commands/import/refresh.js +470 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +230 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/package/constants.js +17 -0
- package/src/cli/commands/package/doctor.js +240 -0
- package/src/cli/commands/package/help.js +27 -0
- package/src/cli/commands/package/lockfile.js +135 -0
- package/src/cli/commands/package/npm.js +97 -0
- package/src/cli/commands/package/reporting.js +35 -0
- package/src/cli/commands/package/runner.js +33 -0
- package/src/cli/commands/package/shared.js +9 -0
- package/src/cli/commands/package/update-cli.js +252 -0
- package/src/cli/commands/package/versions.js +35 -0
- package/src/cli/commands/package.js +29 -813
- package/src/cli/commands/query/change-plan.js +68 -0
- package/src/cli/commands/query/definitions.js +202 -0
- package/src/cli/commands/query/import-adopt.js +121 -0
- package/src/cli/commands/query/runner/artifacts.js +102 -0
- package/src/cli/commands/query/runner/boundaries.js +211 -0
- package/src/cli/commands/query/runner/change.js +182 -0
- package/src/cli/commands/query/runner/import-adopt.js +111 -0
- package/src/cli/commands/query/runner/index.js +31 -0
- package/src/cli/commands/query/runner/output.js +12 -0
- package/src/cli/commands/query/runner/workflow.js +241 -0
- package/src/cli/commands/query/runner.js +3 -0
- package/src/cli/commands/query/workflow-context.js +5 -0
- package/src/cli/commands/query/workspace.js +274 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +466 -0
- package/src/cli/commands/template/constants.js +8 -0
- package/src/cli/commands/template/diagnostics.js +26 -0
- package/src/cli/commands/template/help.js +28 -0
- package/src/cli/commands/template/lifecycle.js +404 -0
- package/src/cli/commands/template/list-show.js +287 -0
- package/src/cli/commands/template/policy.js +422 -0
- package/src/cli/commands/template/shared.js +127 -0
- package/src/cli/commands/template/updates.js +352 -0
- package/src/cli/commands/template.js +41 -2143
- package/src/generator/api/contracts.js +497 -0
- package/src/generator/api/metadata.js +221 -0
- package/src/generator/api/openapi.js +559 -0
- package/src/generator/api/schema.js +124 -0
- package/src/generator/api/types.d.ts +98 -0
- package/src/generator/api.js +3 -1195
- package/src/generator/context/shared/domain-sdlc.js +282 -0
- package/src/generator/context/shared/maintained-boundary.js +665 -0
- package/src/generator/context/shared/metrics.js +85 -0
- package/src/generator/context/shared/primitives.js +64 -0
- package/src/generator/context/shared/relationships.js +453 -0
- package/src/generator/context/shared/summaries.js +263 -0
- package/src/generator/context/shared/types.d.ts +207 -0
- package/src/generator/context/shared.d.ts +42 -0
- package/src/generator/context/shared.js +80 -1390
- package/src/generator/context/slice/core.js +397 -0
- package/src/generator/context/slice/sdlc.js +417 -0
- package/src/generator/context/slice/ui-packets.js +183 -0
- package/src/generator/context/slice.js +2 -859
- package/src/generator/registry/index.js +507 -0
- package/src/generator/registry.js +18 -504
- package/src/generator/runtime/environment/index.js +666 -0
- package/src/generator/runtime/environment.js +4 -666
- package/src/generator/runtime/runtime-check/index.js +554 -0
- package/src/generator/runtime/runtime-check.js +4 -554
- package/src/generator/runtime/shared/index.js +572 -0
- package/src/generator/runtime/shared.js +19 -570
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/widget-conformance/behavior-report.js +258 -0
- package/src/generator/widget-conformance/checks.js +371 -0
- package/src/generator/widget-conformance/projection-context.js +200 -0
- package/src/generator/widget-conformance/report.js +166 -0
- package/src/generator/widget-conformance/types.d.ts +121 -0
- package/src/generator/widget-conformance.js +3 -824
- package/src/import/core/context.d.ts +3 -0
- package/src/import/core/contracts.d.ts +1 -0
- package/src/import/core/registry.d.ts +4 -0
- package/src/import/core/runner/candidates.js +217 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +50 -0
- package/src/import/core/runner/run.js +79 -0
- package/src/import/core/runner/tracks.js +150 -0
- package/src/import/core/runner/ui-drafts.js +337 -0
- package/src/import/core/runner.js +3 -698
- package/src/import/core/shared/api-routes.js +221 -0
- package/src/import/core/shared/candidates.js +97 -0
- package/src/import/core/shared/files.js +177 -0
- package/src/import/core/shared/next-app.js +389 -0
- package/src/import/core/shared/types.d.ts +51 -0
- package/src/import/core/shared/ui-routes.js +230 -0
- package/src/import/core/shared.js +60 -861
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +83 -0
- package/src/new-project/json.js +28 -0
- package/src/new-project/metadata.js +96 -0
- package/src/new-project/package-spec.js +161 -0
- package/src/new-project/project-files.js +348 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +368 -0
- package/src/new-project/template-snapshots.js +430 -0
- package/src/new-project/template-updates.js +512 -0
- package/src/new-project/types.d.ts +83 -0
- package/src/new-project.js +6 -2277
- package/src/parser.d.ts +87 -1
- package/src/parser.js +118 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +564 -0
- package/src/project-config.js +19 -561
- package/src/resolver/enrich/acceptance-criterion.js +2 -0
- package/src/resolver/enrich/bug.js +2 -0
- package/src/resolver/enrich/pitch.js +2 -0
- package/src/resolver/enrich/requirement.js +2 -0
- package/src/resolver/enrich/task.js +2 -0
- package/src/resolver/index.js +19 -2089
- package/src/resolver/normalize.js +384 -1
- package/src/resolver/plans.js +168 -0
- package/src/resolver/projections-api.js +494 -0
- package/src/resolver/projections-db.js +133 -0
- package/src/resolver/projections-ui.js +317 -0
- package/src/resolver/shapes.js +251 -0
- package/src/resolver/shared.js +278 -0
- package/src/resolver/widgets.js +132 -0
- package/src/template-trust/constants.js +62 -0
- package/src/template-trust/content.js +258 -0
- package/src/template-trust/diff.js +92 -0
- package/src/template-trust/policy.js +61 -0
- package/src/template-trust/record.js +90 -0
- package/src/template-trust/status.js +182 -0
- package/src/template-trust.js +24 -687
- package/src/text-helpers.d.ts +1 -0
- package/src/topogram-types.d.ts +69 -0
- package/src/validator/common.js +488 -0
- package/src/validator/data-model.js +237 -0
- package/src/validator/docs.js +167 -0
- package/src/validator/expressions.js +146 -1
- package/src/validator/index.d.ts +23 -0
- package/src/validator/index.js +32 -3585
- package/src/validator/kinds.d.ts +41 -0
- package/src/validator/kinds.js +2 -0
- package/src/validator/model-helpers.js +46 -0
- package/src/validator/per-kind/acceptance-criterion.js +5 -0
- package/src/validator/per-kind/bug.js +6 -0
- package/src/validator/per-kind/domain.js +15 -2
- package/src/validator/per-kind/pitch.js +7 -0
- package/src/validator/per-kind/requirement.js +5 -0
- package/src/validator/per-kind/task.js +7 -0
- package/src/validator/per-kind/widget.js +14 -0
- package/src/validator/projections/api-http-async.js +410 -0
- package/src/validator/projections/api-http-authz.js +88 -0
- package/src/validator/projections/api-http-core.js +205 -0
- package/src/validator/projections/api-http-policies.js +339 -0
- package/src/validator/projections/api-http-responses.js +233 -0
- package/src/validator/projections/api-http.js +44 -0
- package/src/validator/projections/db.js +353 -0
- package/src/validator/projections/generator-defaults.js +45 -0
- package/src/validator/projections/helpers.js +87 -0
- package/src/validator/projections/ui-helpers.js +214 -0
- package/src/validator/projections/ui-navigation.js +344 -0
- package/src/validator/projections/ui-structure.js +364 -0
- package/src/validator/projections/ui-widgets.js +493 -0
- package/src/validator/projections/ui.js +46 -0
- package/src/validator/registry.js +48 -1
- package/src/validator/utils.d.ts +20 -0
- package/src/validator/utils.js +115 -12
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/import-app/api/collect.js +221 -0
- package/src/workflows/import-app/api/openapi.js +257 -0
- package/src/workflows/import-app/api/routes.js +327 -0
- package/src/workflows/import-app/api/sources.js +22 -0
- package/src/workflows/import-app/api.js +2 -797
- package/src/workflows/reconcile/adoption-plan/build.js +208 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
- package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
- package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
- package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
- package/src/workflows/reconcile/adoption-plan.js +30 -740
- package/src/workflows/reconcile/auth/closures.js +115 -0
- package/src/workflows/reconcile/auth/formatters.js +142 -0
- package/src/workflows/reconcile/auth/inference.js +330 -0
- package/src/workflows/reconcile/auth/roles.js +122 -0
- package/src/workflows/reconcile/auth.js +35 -690
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +12 -598
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
- package/src/workflows/reconcile/impacts/indexes.js +101 -0
- package/src/workflows/reconcile/impacts/patches.js +252 -0
- package/src/workflows/reconcile/impacts/reports.js +80 -0
- package/src/workflows/reconcile/impacts.js +14 -623
- package/src/workspace-docs.d.ts +29 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateRuntimeApiContracts,
|
|
3
|
+
buildVerificationSummary,
|
|
4
|
+
getDefaultEnvironmentProjections,
|
|
5
|
+
resolveRuntimeTopology,
|
|
6
|
+
runtimeDemoUserId,
|
|
7
|
+
selectChecksByVerification,
|
|
8
|
+
runtimePorts,
|
|
9
|
+
runtimeUrls
|
|
10
|
+
} from "../shared.js";
|
|
11
|
+
import { getExampleImplementation } from "../../../example-implementation.js";
|
|
12
|
+
import { renderNodeScriptRunner } from "../bundle-shared.js";
|
|
13
|
+
|
|
14
|
+
function buildRuntimeCheckPlan(graph, options = {}) {
|
|
15
|
+
const implementation = getExampleImplementation(graph, options);
|
|
16
|
+
const runtimeReference = implementation.runtime.reference;
|
|
17
|
+
const runtimeChecks = implementation.runtime.checks;
|
|
18
|
+
const topology = resolveRuntimeTopology(graph, options);
|
|
19
|
+
const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
20
|
+
const verification = buildVerificationSummary(graph, ["runtime", "contract", "journey"]);
|
|
21
|
+
const apiSelection = selectChecksByVerification(graph, runtimeChecks.apiStage.checks, ["runtime", "contract", "journey"], {
|
|
22
|
+
keepLookupChecks: true
|
|
23
|
+
});
|
|
24
|
+
const resolveLookupCheck = (check) => {
|
|
25
|
+
if (!check.lookupKey) {
|
|
26
|
+
return check;
|
|
27
|
+
}
|
|
28
|
+
const { lookupKey, ...rest } = check;
|
|
29
|
+
const path = runtimeReference.runtimeCheck.lookupPaths[check.lookupKey];
|
|
30
|
+
return path ? { ...rest, path } : rest;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
type: "runtime_check_plan",
|
|
34
|
+
name: runtimeReference.runtimeCheck.name,
|
|
35
|
+
projections: {
|
|
36
|
+
api: apiProjection.id,
|
|
37
|
+
ui: uiProjection.id,
|
|
38
|
+
db: dbProjection.id
|
|
39
|
+
},
|
|
40
|
+
topology: {
|
|
41
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
42
|
+
id: runtime.id,
|
|
43
|
+
kind: runtime.kind,
|
|
44
|
+
projection: runtime.projection.id,
|
|
45
|
+
generator: runtime.generator
|
|
46
|
+
}))
|
|
47
|
+
},
|
|
48
|
+
...(verification ? { verification } : {}),
|
|
49
|
+
...(apiSelection.selection ? { selection: apiSelection.selection } : {}),
|
|
50
|
+
env: {
|
|
51
|
+
required: [
|
|
52
|
+
...runtimeReference.runtimeCheck.requiredEnv
|
|
53
|
+
],
|
|
54
|
+
apiBase: "TOPOGRAM_API_BASE_URL",
|
|
55
|
+
webBase: "TOPOGRAM_WEB_BASE_URL",
|
|
56
|
+
demoContainerId: runtimeReference.runtimeCheck.demoContainerEnvVar,
|
|
57
|
+
demoPrimaryId: runtimeReference.runtimeCheck.demoPrimaryEnvVar,
|
|
58
|
+
demoUserId: "TOPOGRAM_DEMO_USER_ID"
|
|
59
|
+
},
|
|
60
|
+
report: {
|
|
61
|
+
outputPath: "state/runtime-check-report.json"
|
|
62
|
+
},
|
|
63
|
+
stages: [
|
|
64
|
+
{
|
|
65
|
+
...runtimeChecks.environmentStage,
|
|
66
|
+
checks: runtimeChecks.environmentStage.checks.map(resolveLookupCheck)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
...runtimeChecks.apiStage,
|
|
70
|
+
checks: apiSelection.checks.map(resolveLookupCheck)
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderRuntimeCheckEnvExample(graph, options = {}) {
|
|
77
|
+
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
78
|
+
const urls = runtimeUrls(runtimeReference, resolveRuntimeTopology(graph, options));
|
|
79
|
+
return `TOPOGRAM_API_BASE_URL=${urls.api}
|
|
80
|
+
TOPOGRAM_WEB_BASE_URL=${urls.web}
|
|
81
|
+
TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
82
|
+
${runtimeReference.environment.envExample || ""}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderRuntimeCheckReadme(graph, options = {}) {
|
|
87
|
+
const runtimeReference = getExampleImplementation(graph, options).runtime.reference;
|
|
88
|
+
const verification = buildVerificationSummary(graph, ["runtime", "contract", "journey"]);
|
|
89
|
+
const stageNames = (runtimeReference.runtimeCheck.stageNotes && runtimeReference.runtimeCheck.stageNotes.length > 0)
|
|
90
|
+
? runtimeReference.runtimeCheck.stageNotes
|
|
91
|
+
: [
|
|
92
|
+
{
|
|
93
|
+
id: "environment",
|
|
94
|
+
summary: "required env, web readiness, API health, API readiness, and seeded data checks"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "api",
|
|
98
|
+
summary: "core API happy paths, lookup endpoints, and important negative cases"
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
const notes = (runtimeReference.runtimeCheck.notes && runtimeReference.runtimeCheck.notes.length > 0)
|
|
102
|
+
? runtimeReference.runtimeCheck.notes
|
|
103
|
+
: [
|
|
104
|
+
"Mutating checks exercise the example's create/update/terminal flows.",
|
|
105
|
+
"Lookup checks verify the generated lookup endpoints configured for the example.",
|
|
106
|
+
"Later stages are skipped if environment readiness fails.",
|
|
107
|
+
"The generated server exposes both `/health` and `/ready`.",
|
|
108
|
+
"Use the smoke bundle for a faster minimal confidence check.",
|
|
109
|
+
"Use this runtime-check bundle for richer staged verification and JSON reporting."
|
|
110
|
+
];
|
|
111
|
+
return `# ${runtimeReference.runtimeCheck.bundleTitle}
|
|
112
|
+
|
|
113
|
+
This bundle gives you richer staged runtime verification for the generated stack.
|
|
114
|
+
|
|
115
|
+
Use it when you want more than a quick smoke test. It goes beyond the lightweight smoke bundle by checking environment readiness, API health, DB-backed seeded data, and deeper API behavior.
|
|
116
|
+
|
|
117
|
+
## Stages
|
|
118
|
+
|
|
119
|
+
${stageNames.map((stage) => `- \`${stage.id}\`: ${stage.summary}`).join("\n")}
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
1. Copy \`.env.example\` to \`.env\` if needed
|
|
124
|
+
2. Run \`bash ./scripts/check.sh\`
|
|
125
|
+
3. Inspect \`state/runtime-check-report.json\`
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
${notes.map((note) => `- ${note}`).join("\n")}
|
|
130
|
+
${verification ? `\n## Canonical Verification\n\n- Sources: ${verification.sources.map((entry) => `\`${entry.id}\``).join(", ")}\n- Scenarios: ${verification.scenarios.map((entry) => entry.label).join(", ")}` : ""}
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderRuntimeCheckScript() {
|
|
135
|
+
return renderNodeScriptRunner("check.mjs", { searchParentEnv: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderRuntimeCheckModule(graph, options = {}) {
|
|
139
|
+
const implementation = getExampleImplementation(graph, options);
|
|
140
|
+
const runtimeReference = implementation.runtime.reference;
|
|
141
|
+
const runtimeCheckRenderers = implementation.runtime.checkRenderers;
|
|
142
|
+
const ports = runtimePorts(runtimeReference, resolveRuntimeTopology(graph, options));
|
|
143
|
+
return `import fs from "node:fs/promises";
|
|
144
|
+
import { execFile } from "node:child_process";
|
|
145
|
+
import path from "node:path";
|
|
146
|
+
import { promisify } from "node:util";
|
|
147
|
+
import { fileURLToPath } from "node:url";
|
|
148
|
+
|
|
149
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
150
|
+
const __dirname = path.dirname(__filename);
|
|
151
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
152
|
+
const plan = JSON.parse(await fs.readFile(path.join(rootDir, "runtime-check-plan.json"), "utf8"));
|
|
153
|
+
const apiContracts = JSON.parse(await fs.readFile(path.join(rootDir, "api-contracts.json"), "utf8"));
|
|
154
|
+
const reportPath = path.join(rootDir, plan.report.outputPath);
|
|
155
|
+
const execFileAsync = promisify(execFile);
|
|
156
|
+
|
|
157
|
+
const envVars = Object.fromEntries(Object.entries(plan.env).filter(([, value]) => typeof value === "string"));
|
|
158
|
+
${runtimeCheckRenderers.renderRuntimeCheckState()}
|
|
159
|
+
|
|
160
|
+
function envValue(name) {
|
|
161
|
+
const direct = process.env[name];
|
|
162
|
+
if (direct) {
|
|
163
|
+
return direct;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fallbackMap = {
|
|
167
|
+
TOPOGRAM_API_BASE_URL: process.env.PUBLIC_TOPOGRAM_API_BASE_URL || "http://localhost:${ports.server}",
|
|
168
|
+
TOPOGRAM_WEB_BASE_URL: process.env.PUBLIC_TOPOGRAM_WEB_BASE_URL || \`http://localhost:\${process.env.WEB_PORT || "${ports.web}"}\`,
|
|
169
|
+
${runtimeReference.runtimeCheck.demoContainerEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_CONTAINER_ID || "",
|
|
170
|
+
${runtimeReference.runtimeCheck.demoPrimaryEnvVar}: process.env.PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID || "",
|
|
171
|
+
TOPOGRAM_DEMO_USER_ID: process.env.PUBLIC_TOPOGRAM_AUTH_USER_ID || process.env.PUBLIC_TOPOGRAM_DEMO_USER_ID || "",
|
|
172
|
+
TOPOGRAM_AUTH_USER_ID: process.env.TOPOGRAM_DEMO_USER_ID || ""
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return fallbackMap[name] || "";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function apiBase() {
|
|
179
|
+
return envValue(plan.env.apiBase);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function webBase() {
|
|
183
|
+
return envValue(plan.env.webBase);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stackStartHint() {
|
|
187
|
+
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.";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function describeFetchError(error) {
|
|
191
|
+
if (error?.cause?.code) {
|
|
192
|
+
return error.cause.code;
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(error?.cause?.errors) && error.cause.errors.length > 0) {
|
|
195
|
+
return [...new Set(error.cause.errors.map((entry) => entry.code).filter(Boolean))].join(", ");
|
|
196
|
+
}
|
|
197
|
+
return error instanceof Error ? error.message : String(error);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function fetchWithStackHint(url, init, label) {
|
|
201
|
+
try {
|
|
202
|
+
return await fetch(url, init);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new Error(\`\${label} is not reachable at \${url.toString()}. \${stackStartHint()} Original error: \${describeFetchError(error)}\`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveCheckPath(pathTemplate) {
|
|
209
|
+
return String(pathTemplate || "")
|
|
210
|
+
.replace(/\\$env:([A-Z0-9_]+)/g, (_, name) => encodeURIComponent(envValue(name)))
|
|
211
|
+
.replace(/\\$state:primary_id/g, encodeURIComponent(String(state.latestPrimary?.id || state.createdPrimary?.id || "")));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function authToken() {
|
|
215
|
+
return envValue("TOPOGRAM_AUTH_TOKEN");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pathParamValue(token) {
|
|
219
|
+
if (typeof token !== "string") {
|
|
220
|
+
return token;
|
|
221
|
+
}
|
|
222
|
+
if (token.startsWith("$env:")) {
|
|
223
|
+
return envValue(token.slice(5));
|
|
224
|
+
}
|
|
225
|
+
if (token === "$state:primary_id") {
|
|
226
|
+
return state.latestPrimary?.id || state.createdPrimary?.id || "";
|
|
227
|
+
}
|
|
228
|
+
return token;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function contractFor(capabilityId) {
|
|
232
|
+
const contract = apiContracts[capabilityId];
|
|
233
|
+
if (!contract) {
|
|
234
|
+
throw new Error(\`Missing API contract for \${capabilityId}\`);
|
|
235
|
+
}
|
|
236
|
+
return contract;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildUrl(base, endpointPath, pathParams = {}) {
|
|
240
|
+
let resolvedPath = endpointPath;
|
|
241
|
+
for (const [name, rawValue] of Object.entries(pathParams)) {
|
|
242
|
+
const value = pathParamValue(rawValue);
|
|
243
|
+
resolvedPath = resolvedPath.replace(\`:\${name}\`, encodeURIComponent(String(value)));
|
|
244
|
+
}
|
|
245
|
+
return new URL(resolvedPath, base);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function inferPathParams(contract, overrides = {}) {
|
|
249
|
+
const params = {};
|
|
250
|
+
for (const field of contract.requestContract?.transport?.path || []) {
|
|
251
|
+
if (Object.prototype.hasOwnProperty.call(overrides, field.name)) {
|
|
252
|
+
params[field.transport.wireName] = overrides[field.name];
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
field.name.endsWith("_id") &&
|
|
257
|
+
field.transport?.wireName === "id" &&
|
|
258
|
+
field.name !== "job_id"
|
|
259
|
+
) {
|
|
260
|
+
params[field.transport.wireName] = "$state:primary_id";
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (field.name === "job_id") {
|
|
264
|
+
throw new Error("Runtime checks do not support job_id paths in v1");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return params;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
${runtimeCheckRenderers.renderRuntimeCheckCreatePayload()}
|
|
271
|
+
|
|
272
|
+
function assertCondition(condition, message) {
|
|
273
|
+
if (!condition) {
|
|
274
|
+
throw new Error(message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function assertErrorResponse(responseBody, expectedCode, label) {
|
|
279
|
+
assertCondition(responseBody && typeof responseBody === "object", \`\${label} did not return a JSON object body\`);
|
|
280
|
+
assertCondition(responseBody.error && typeof responseBody.error === "object", \`\${label} did not include an error object\`);
|
|
281
|
+
assertCondition(typeof responseBody.error.code === "string" && responseBody.error.code.length > 0, \`\${label} did not include an error code\`);
|
|
282
|
+
assertCondition(typeof responseBody.error.message === "string" && responseBody.error.message.length > 0, \`\${label} did not include an error message\`);
|
|
283
|
+
if (expectedCode) {
|
|
284
|
+
assertCondition(responseBody.error.code === expectedCode, \`\${label} expected error code \${expectedCode}, got \${responseBody.error.code}\`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sleep(ms) {
|
|
289
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function appleScriptString(value) {
|
|
293
|
+
return JSON.stringify(String(value ?? ""));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function runSafariBrowserCheck(definition) {
|
|
297
|
+
if (process.platform !== "darwin") {
|
|
298
|
+
throw new Error("web_browser_contract is only supported on macOS local stacks");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const targetUrl = new URL(resolveCheckPath(definition.path), webBase()).toString();
|
|
302
|
+
const waitMs = Number(process.env.TOPOGRAM_BROWSER_WAIT_MS || "8000");
|
|
303
|
+
const pollMs = Number(process.env.TOPOGRAM_BROWSER_POLL_MS || "250");
|
|
304
|
+
const pollLimit = Math.max(1, Math.ceil(waitMs / pollMs));
|
|
305
|
+
const pollDelaySeconds = pollMs / 1000;
|
|
306
|
+
const browserApp = process.env.TOPOGRAM_BROWSER_APP || "Safari";
|
|
307
|
+
const script = [
|
|
308
|
+
\`set targetUrl to \${appleScriptString(targetUrl)}\`,
|
|
309
|
+
\`set expectedText to \${appleScriptString(definition.expectText || "")}\`,
|
|
310
|
+
\`set forbiddenText to \${appleScriptString(definition.expectNotText || "")}\`,
|
|
311
|
+
\`set pollLimit to \${appleScriptString(pollLimit)} as integer\`,
|
|
312
|
+
\`set pollDelay to \${appleScriptString(pollDelaySeconds)} as real\`,
|
|
313
|
+
\`tell application \${appleScriptString(browserApp)}\`,
|
|
314
|
+
"activate",
|
|
315
|
+
"set runtimeDoc to make new document with properties {URL:targetUrl}",
|
|
316
|
+
"repeat with attempt from 1 to pollLimit",
|
|
317
|
+
"delay pollDelay",
|
|
318
|
+
"try",
|
|
319
|
+
"set bodyText to do JavaScript \\"document.body ? document.body.innerText : ''\\" in runtimeDoc",
|
|
320
|
+
"if (expectedText is \\"\\" or bodyText contains expectedText) and (forbiddenText is \\"\\" or bodyText does not contain forbiddenText) then exit repeat",
|
|
321
|
+
"end try",
|
|
322
|
+
"end repeat",
|
|
323
|
+
"set finalText to do JavaScript \\"document.body ? document.body.innerText : ''\\" in runtimeDoc",
|
|
324
|
+
"close runtimeDoc",
|
|
325
|
+
"return finalText",
|
|
326
|
+
"end tell"
|
|
327
|
+
];
|
|
328
|
+
const args = script.flatMap((line) => ["-e", line]);
|
|
329
|
+
const { stdout, stderr } = await execFileAsync("osascript", args);
|
|
330
|
+
if (stderr && stderr.trim()) {
|
|
331
|
+
throw new Error(\`browser check failed: \${stderr.trim()}\`);
|
|
332
|
+
}
|
|
333
|
+
return stdout.trim();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
${runtimeCheckRenderers.renderRuntimeCheckHelpers()}
|
|
337
|
+
|
|
338
|
+
async function parseResponseBody(response) {
|
|
339
|
+
const text = await response.text();
|
|
340
|
+
if (!text) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const contentType = response.headers.get("content-type") || "";
|
|
344
|
+
if (contentType.includes("application/json")) {
|
|
345
|
+
return JSON.parse(text);
|
|
346
|
+
}
|
|
347
|
+
return text;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function requestContract(capabilityId, { payload = null, pathParams = null, headers = {} } = {}) {
|
|
351
|
+
const contract = contractFor(capabilityId);
|
|
352
|
+
const resolvedPathParams = pathParams || inferPathParams(contract);
|
|
353
|
+
const url = buildUrl(apiBase(), contract.endpoint.path, resolvedPathParams);
|
|
354
|
+
const requestHeaders = new Headers(headers);
|
|
355
|
+
if ((contract.endpoint.authz || []).length > 0 && authToken() && !requestHeaders.has("Authorization")) {
|
|
356
|
+
requestHeaders.set("Authorization", \`Bearer \${authToken()}\`);
|
|
357
|
+
}
|
|
358
|
+
let body;
|
|
359
|
+
if (payload && (contract.requestContract?.transport?.body || []).length > 0) {
|
|
360
|
+
requestHeaders.set("content-type", "application/json");
|
|
361
|
+
body = JSON.stringify(payload);
|
|
362
|
+
}
|
|
363
|
+
const response = await fetchWithStackHint(url, {
|
|
364
|
+
method: contract.endpoint.method,
|
|
365
|
+
headers: requestHeaders,
|
|
366
|
+
body
|
|
367
|
+
}, "api service");
|
|
368
|
+
const responseBody = await parseResponseBody(response);
|
|
369
|
+
return { contract, response, responseBody, url: url.toString() };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function preconditionStatusFor(contract) {
|
|
373
|
+
return contract.endpoint.preconditions?.[0]?.error || 412;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function visibleTextFromHtml(html) {
|
|
377
|
+
return String(html || "")
|
|
378
|
+
.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, " ")
|
|
379
|
+
.replace(/<style\\b[^>]*>[\\s\\S]*?<\\/style>/gi, " ")
|
|
380
|
+
.replace(/<[^>]+>/g, " ")
|
|
381
|
+
.replace(/ /g, " ")
|
|
382
|
+
.replace(/&/g, "&")
|
|
383
|
+
.replace(/</g, "<")
|
|
384
|
+
.replace(/>/g, ">")
|
|
385
|
+
.replace(/"/g, '"')
|
|
386
|
+
.replace(/'/g, "'")
|
|
387
|
+
.replace(/\\s+/g, " ")
|
|
388
|
+
.trim();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function checkResultTemplate(definition) {
|
|
392
|
+
return {
|
|
393
|
+
id: definition.id,
|
|
394
|
+
kind: definition.kind,
|
|
395
|
+
mandatory: definition.mandatory !== false,
|
|
396
|
+
mutating: Boolean(definition.mutating),
|
|
397
|
+
ok: null,
|
|
398
|
+
skipped: false,
|
|
399
|
+
durationMs: 0,
|
|
400
|
+
error: null,
|
|
401
|
+
resources: {}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function runCheck(definition) {
|
|
406
|
+
const startedAt = Date.now();
|
|
407
|
+
const result = checkResultTemplate(definition);
|
|
408
|
+
try {
|
|
409
|
+
if (definition.id === "required_env") {
|
|
410
|
+
const missing = plan.env.required.filter((name) => !envValue(name));
|
|
411
|
+
assertCondition(missing.length === 0, \`Missing required environment variables: \${missing.join(", ")}\`);
|
|
412
|
+
} else if (definition.kind === "web_contract") {
|
|
413
|
+
const response = await fetchWithStackHint(new URL(resolveCheckPath(definition.path), webBase()), undefined, "web app");
|
|
414
|
+
const body = await response.text();
|
|
415
|
+
const bodyText = visibleTextFromHtml(body);
|
|
416
|
+
assertCondition(response.status === definition.expectStatus, \`web readiness expected \${definition.expectStatus}, got \${response.status}\`);
|
|
417
|
+
assertCondition(bodyText.includes(definition.expectText), \`web readiness did not include expected text: \${definition.expectText}\`);
|
|
418
|
+
if (definition.expectNotText) {
|
|
419
|
+
assertCondition(!bodyText.includes(definition.expectNotText), \`web readiness unexpectedly included text: \${definition.expectNotText}\`);
|
|
420
|
+
}
|
|
421
|
+
} else if (definition.kind === "web_browser_contract") {
|
|
422
|
+
const bodyText = await runSafariBrowserCheck(definition);
|
|
423
|
+
if (definition.expectText) {
|
|
424
|
+
assertCondition(bodyText.includes(definition.expectText), \`browser check did not include expected text: \${definition.expectText}\`);
|
|
425
|
+
}
|
|
426
|
+
if (definition.expectNotText) {
|
|
427
|
+
assertCondition(!bodyText.includes(definition.expectNotText), \`browser check unexpectedly included text: \${definition.expectNotText}\`);
|
|
428
|
+
}
|
|
429
|
+
} else if (definition.kind === "api_health") {
|
|
430
|
+
const response = await fetchWithStackHint(new URL(definition.path, apiBase()), undefined, "api service");
|
|
431
|
+
const responseBody = await parseResponseBody(response);
|
|
432
|
+
assertCondition(response.status === definition.expectStatus, \`api health expected \${definition.expectStatus}, got \${response.status}\`);
|
|
433
|
+
assertCondition(responseBody?.ok === definition.expectOk, "api health did not report ok");
|
|
434
|
+
} else if (definition.kind === "api_ready") {
|
|
435
|
+
const response = await fetchWithStackHint(new URL(definition.path, apiBase()), undefined, "api service");
|
|
436
|
+
const responseBody = await parseResponseBody(response);
|
|
437
|
+
assertCondition(response.status === definition.expectStatus, \`api readiness expected \${definition.expectStatus}, got \${response.status}\`);
|
|
438
|
+
assertCondition(responseBody?.ready === definition.expectReady, "api readiness did not report ready");
|
|
439
|
+
${runtimeCheckRenderers.renderRuntimeCheckCases()}
|
|
440
|
+
} else {
|
|
441
|
+
throw new Error(\`Unsupported runtime check id: \${definition.id}\`);
|
|
442
|
+
}
|
|
443
|
+
result.ok = true;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
result.ok = false;
|
|
446
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
447
|
+
} finally {
|
|
448
|
+
result.durationMs = Date.now() - startedAt;
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function stageSummary(stageDefinition, checks) {
|
|
454
|
+
const failedMandatory = checks.some((check) => check.ok === false && check.mandatory);
|
|
455
|
+
const skipped = checks.every((check) => check.skipped);
|
|
456
|
+
return {
|
|
457
|
+
id: stageDefinition.id,
|
|
458
|
+
name: stageDefinition.name,
|
|
459
|
+
ok: skipped ? null : !failedMandatory,
|
|
460
|
+
skipped,
|
|
461
|
+
checks
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function skippedChecksForStage(stageDefinition, reason) {
|
|
466
|
+
return stageDefinition.checks.map((definition) => ({
|
|
467
|
+
...checkResultTemplate(definition),
|
|
468
|
+
skipped: true,
|
|
469
|
+
error: reason
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function logCheck(stageId, result) {
|
|
474
|
+
const icon = result.skipped ? "-" : result.ok ? "[ok]" : "[fail]";
|
|
475
|
+
const suffix = result.error ? \` - \${result.error}\` : "";
|
|
476
|
+
console.log(\`\${icon} \${stageId}:\${result.id} (\${result.durationMs}ms)\${suffix}\`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function writeReport(report) {
|
|
480
|
+
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
|
481
|
+
await fs.writeFile(reportPath, \`\${JSON.stringify(report, null, 2)}\\n\`, "utf8");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const report = {
|
|
485
|
+
ok: true,
|
|
486
|
+
name: plan.name,
|
|
487
|
+
reportPath: plan.report.outputPath,
|
|
488
|
+
stages: [],
|
|
489
|
+
generatedAt: new Date().toISOString()
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
let stopFurtherStages = false;
|
|
493
|
+
|
|
494
|
+
for (const stageDefinition of plan.stages) {
|
|
495
|
+
if (stopFurtherStages) {
|
|
496
|
+
const skippedChecks = skippedChecksForStage(stageDefinition, "Skipped because a prior fail-fast stage failed");
|
|
497
|
+
for (const check of skippedChecks) {
|
|
498
|
+
logCheck(stageDefinition.id, check);
|
|
499
|
+
}
|
|
500
|
+
report.stages.push(stageSummary(stageDefinition, skippedChecks));
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const checks = [];
|
|
505
|
+
for (const checkDefinition of stageDefinition.checks) {
|
|
506
|
+
const result = await runCheck(checkDefinition);
|
|
507
|
+
checks.push(result);
|
|
508
|
+
logCheck(stageDefinition.id, result);
|
|
509
|
+
if (stageDefinition.failFast && result.ok === false && result.mandatory) {
|
|
510
|
+
stopFurtherStages = true;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (stopFurtherStages && checks.length < stageDefinition.checks.length) {
|
|
516
|
+
const remaining = stageDefinition.checks.slice(checks.length).map((definition) => ({
|
|
517
|
+
...checkResultTemplate(definition),
|
|
518
|
+
skipped: true,
|
|
519
|
+
error: "Skipped because this stage failed in fail-fast mode"
|
|
520
|
+
}));
|
|
521
|
+
for (const check of remaining) {
|
|
522
|
+
logCheck(stageDefinition.id, check);
|
|
523
|
+
}
|
|
524
|
+
checks.push(...remaining);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
report.stages.push(stageSummary(stageDefinition, checks));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
report.ok = report.stages.every((stage) => stage.ok !== false);
|
|
531
|
+
await writeReport(report);
|
|
532
|
+
console.log(JSON.stringify(report, null, 2));
|
|
533
|
+
|
|
534
|
+
if (!report.ok) {
|
|
535
|
+
process.exitCode = 1;
|
|
536
|
+
}
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function generateRuntimeCheckBundle(graph, options = {}) {
|
|
541
|
+
const plan = buildRuntimeCheckPlan(graph, options);
|
|
542
|
+
return {
|
|
543
|
+
".env.example": renderRuntimeCheckEnvExample(graph, options),
|
|
544
|
+
"README.md": renderRuntimeCheckReadme(graph, options),
|
|
545
|
+
"runtime-check-plan.json": `${JSON.stringify(plan, null, 2)}\n`,
|
|
546
|
+
"api-contracts.json": `${JSON.stringify(generateRuntimeApiContracts(graph), null, 2)}\n`,
|
|
547
|
+
"scripts/check.sh": renderRuntimeCheckScript(),
|
|
548
|
+
"scripts/check.mjs": renderRuntimeCheckModule(graph, options)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function generateRuntimeCheckPlan(graph, options = {}) {
|
|
553
|
+
return buildRuntimeCheckPlan(graph, options);
|
|
554
|
+
}
|