@dev.fast/whiteboard 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import { isObjectValue, jsonValueSchema } from "@dev.fast/review-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const softwareElementTypeSchema = z.enum([
|
|
5
|
+
"person",
|
|
6
|
+
"softwareSystem",
|
|
7
|
+
"container",
|
|
8
|
+
"dataStore",
|
|
9
|
+
"component",
|
|
10
|
+
"codeElement",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export type SoftwareElementType = z.infer<typeof softwareElementTypeSchema>;
|
|
14
|
+
|
|
15
|
+
const softwareChangeStatusSchema = z.enum([
|
|
16
|
+
"added",
|
|
17
|
+
"removed",
|
|
18
|
+
"modified",
|
|
19
|
+
"unchanged",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export type SoftwareChangeStatus = z.infer<typeof softwareChangeStatusSchema>;
|
|
23
|
+
|
|
24
|
+
const softwareDataStoreKindSchema = z.enum([
|
|
25
|
+
"database",
|
|
26
|
+
"objectStore",
|
|
27
|
+
"bucket",
|
|
28
|
+
"artifactStore",
|
|
29
|
+
"fileStore",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export type SoftwareDataStoreKind = z.infer<typeof softwareDataStoreKindSchema>;
|
|
33
|
+
|
|
34
|
+
export const softwareLineRangeSchema = z.strictObject({
|
|
35
|
+
fromLine: z.int().positive(),
|
|
36
|
+
toLine: z.int().positive(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type SoftwareLineRange = z.infer<typeof softwareLineRangeSchema>;
|
|
40
|
+
|
|
41
|
+
export const softwareSourceRangeSchema = softwareLineRangeSchema.extend({
|
|
42
|
+
file: z.string(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type SoftwareSourceRange = z.infer<typeof softwareSourceRangeSchema>;
|
|
46
|
+
|
|
47
|
+
export interface SoftwareCoverageFileInput {
|
|
48
|
+
path: string;
|
|
49
|
+
ranges?: SoftwareLineRange[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SoftwareCoverageInput {
|
|
53
|
+
files?: Array<string | SoftwareCoverageFileInput>;
|
|
54
|
+
globs?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SoftwareRelationshipBaseInput {
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
label?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SoftwareCallRelationshipInput extends SoftwareRelationshipBaseInput {
|
|
65
|
+
kind: "call";
|
|
66
|
+
nthCallSite?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SoftwareSemanticRelationshipInput extends SoftwareRelationshipBaseInput {
|
|
70
|
+
kind: "semantic";
|
|
71
|
+
semanticKind?: string;
|
|
72
|
+
sourceRanges?: SoftwareLineRange[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type SoftwareRelationshipInput =
|
|
76
|
+
| SoftwareCallRelationshipInput
|
|
77
|
+
| SoftwareSemanticRelationshipInput;
|
|
78
|
+
|
|
79
|
+
export interface SoftwareElementBaseInput {
|
|
80
|
+
id?: string;
|
|
81
|
+
label?: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
changeStatus?: SoftwareChangeStatus;
|
|
84
|
+
coverage?: SoftwareCoverageInput;
|
|
85
|
+
relationships?: SoftwareRelationshipInput[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PersonInput extends SoftwareElementBaseInput {}
|
|
89
|
+
|
|
90
|
+
export interface SoftwareSystemInput extends SoftwareElementBaseInput {
|
|
91
|
+
external?: boolean;
|
|
92
|
+
containers?: SoftwareElementCollection<ContainerInput>;
|
|
93
|
+
dataStores?: SoftwareElementCollection<DataStoreInput>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ContainerInput extends SoftwareElementBaseInput {
|
|
97
|
+
components?: SoftwareElementCollection<ComponentInput>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface DataStoreInput extends SoftwareElementBaseInput {
|
|
101
|
+
kind?: SoftwareDataStoreKind;
|
|
102
|
+
tables?: Record<string, SoftwareDataStoreCollectionInput>;
|
|
103
|
+
documents?: Record<string, SoftwareDataStoreCollectionInput>;
|
|
104
|
+
components?: SoftwareElementCollection<ComponentInput>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SoftwareDataStoreFieldLeaf {
|
|
108
|
+
type: string;
|
|
109
|
+
example?: unknown;
|
|
110
|
+
pk?: boolean;
|
|
111
|
+
fk?: SoftwareDataStoreForeignKeyRef;
|
|
112
|
+
schema?: SoftwareDataStoreFieldSchema;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type SoftwareDataStoreFieldSchema = {
|
|
116
|
+
[field: string]: SoftwareDataStoreFieldLeaf | SoftwareDataStoreFieldSchema;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export interface SoftwareDataStoreCollectionInput {
|
|
120
|
+
label?: string;
|
|
121
|
+
key?: string;
|
|
122
|
+
schema: SoftwareDataStoreFieldSchema;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ComponentInput extends SoftwareElementBaseInput {
|
|
126
|
+
codeElements?: SoftwareElementCollection<CodeElementInput>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CodeElementInput extends SoftwareElementBaseInput {
|
|
130
|
+
sourceRanges?: SoftwareSourceRange[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type SoftwareElementCollection<T extends SoftwareElementBaseInput> =
|
|
134
|
+
| Record<string, T>
|
|
135
|
+
| T[];
|
|
136
|
+
|
|
137
|
+
export interface SoftwareModelInput {
|
|
138
|
+
people?: SoftwareElementCollection<PersonInput>;
|
|
139
|
+
systems?: SoftwareElementCollection<SoftwareSystemInput>;
|
|
140
|
+
relationships?: SoftwareRelationshipInput[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const softwareDataStoreForeignKeyRefDataSchema = z.union([
|
|
144
|
+
z.string(),
|
|
145
|
+
z.strictObject({
|
|
146
|
+
store: z.string().optional(),
|
|
147
|
+
table: z.string(),
|
|
148
|
+
field: z.string(),
|
|
149
|
+
label: z.string().optional(),
|
|
150
|
+
cardinality: z.enum(["one-to-one", "many-to-one"]).optional(),
|
|
151
|
+
onDelete: z.string().optional(),
|
|
152
|
+
onUpdate: z.string().optional(),
|
|
153
|
+
}),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
export type SoftwareDataStoreForeignKeyRef = z.infer<
|
|
157
|
+
typeof softwareDataStoreForeignKeyRefDataSchema
|
|
158
|
+
>;
|
|
159
|
+
|
|
160
|
+
// The recursive field schema keeps its hand-written type: zod cannot infer a
|
|
161
|
+
// mutually recursive record. The data form pins `example` to JSON so a
|
|
162
|
+
// published map is parseable, while the authored input type stays `unknown`.
|
|
163
|
+
const softwareDataStoreFieldDataSchema: z.ZodType<SoftwareDataStoreFieldSchema> =
|
|
164
|
+
z.lazy(() =>
|
|
165
|
+
z.record(
|
|
166
|
+
z.string(),
|
|
167
|
+
z.union([
|
|
168
|
+
z.strictObject({
|
|
169
|
+
type: z.string(),
|
|
170
|
+
example: jsonValueSchema.optional(),
|
|
171
|
+
pk: z.boolean().optional(),
|
|
172
|
+
fk: softwareDataStoreForeignKeyRefDataSchema.optional(),
|
|
173
|
+
schema: softwareDataStoreFieldDataSchema.optional(),
|
|
174
|
+
}),
|
|
175
|
+
softwareDataStoreFieldDataSchema,
|
|
176
|
+
]),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
export const normalizedSoftwareDataStoreCollectionSchema = z.strictObject({
|
|
181
|
+
id: z.string(),
|
|
182
|
+
label: z.string(),
|
|
183
|
+
key: z.string().optional(),
|
|
184
|
+
schema: softwareDataStoreFieldDataSchema,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export type NormalizedSoftwareDataStoreCollection = z.infer<
|
|
188
|
+
typeof normalizedSoftwareDataStoreCollectionSchema
|
|
189
|
+
>;
|
|
190
|
+
|
|
191
|
+
export const normalizedSoftwareDataStoreSchemaSchema = z.strictObject({
|
|
192
|
+
tables: z.record(z.string(), normalizedSoftwareDataStoreCollectionSchema),
|
|
193
|
+
documents: z.record(z.string(), normalizedSoftwareDataStoreCollectionSchema),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export type NormalizedSoftwareDataStoreSchema = z.infer<
|
|
197
|
+
typeof normalizedSoftwareDataStoreSchemaSchema
|
|
198
|
+
>;
|
|
199
|
+
|
|
200
|
+
export const normalizedSoftwareCoverageFileSchema = z.strictObject({
|
|
201
|
+
path: z.string(),
|
|
202
|
+
ranges: z.array(softwareLineRangeSchema),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export type NormalizedSoftwareCoverageFile = z.infer<
|
|
206
|
+
typeof normalizedSoftwareCoverageFileSchema
|
|
207
|
+
>;
|
|
208
|
+
|
|
209
|
+
export const normalizedSoftwareCoverageSchema = z.strictObject({
|
|
210
|
+
files: z.array(normalizedSoftwareCoverageFileSchema),
|
|
211
|
+
globs: z.array(z.string()),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export type NormalizedSoftwareCoverage = z.infer<
|
|
215
|
+
typeof normalizedSoftwareCoverageSchema
|
|
216
|
+
>;
|
|
217
|
+
|
|
218
|
+
export const normalizedSoftwareElementSchema = z.strictObject({
|
|
219
|
+
type: softwareElementTypeSchema,
|
|
220
|
+
id: z.string(),
|
|
221
|
+
path: z.string(),
|
|
222
|
+
parentPath: z.string().optional(),
|
|
223
|
+
label: z.string(),
|
|
224
|
+
description: z.string().optional(),
|
|
225
|
+
changeStatus: softwareChangeStatusSchema.optional(),
|
|
226
|
+
coverage: normalizedSoftwareCoverageSchema.optional(),
|
|
227
|
+
external: z.boolean().optional(),
|
|
228
|
+
dataStoreKind: softwareDataStoreKindSchema.optional(),
|
|
229
|
+
dataStoreSchema: normalizedSoftwareDataStoreSchemaSchema.optional(),
|
|
230
|
+
sourceRanges: z.array(softwareSourceRangeSchema).optional(),
|
|
231
|
+
children: z.array(z.string()),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export type NormalizedSoftwareElement = z.infer<
|
|
235
|
+
typeof normalizedSoftwareElementSchema
|
|
236
|
+
>;
|
|
237
|
+
|
|
238
|
+
const normalizedRelationshipFields = {
|
|
239
|
+
id: z.string(),
|
|
240
|
+
from: z.string(),
|
|
241
|
+
to: z.string(),
|
|
242
|
+
scopePath: z.string().optional(),
|
|
243
|
+
label: z.string().optional(),
|
|
244
|
+
description: z.string().optional(),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const normalizedSoftwareRelationshipSchema = z.discriminatedUnion(
|
|
248
|
+
"kind",
|
|
249
|
+
[
|
|
250
|
+
z.strictObject({
|
|
251
|
+
...normalizedRelationshipFields,
|
|
252
|
+
kind: z.literal("call"),
|
|
253
|
+
nthCallSite: z.int().nonnegative(),
|
|
254
|
+
}),
|
|
255
|
+
z.strictObject({
|
|
256
|
+
...normalizedRelationshipFields,
|
|
257
|
+
kind: z.literal("semantic"),
|
|
258
|
+
semanticKind: z.string().optional(),
|
|
259
|
+
sourceRanges: z.array(softwareLineRangeSchema).optional(),
|
|
260
|
+
}),
|
|
261
|
+
],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
export type NormalizedSoftwareRelationship = z.infer<
|
|
265
|
+
typeof normalizedSoftwareRelationshipSchema
|
|
266
|
+
>;
|
|
267
|
+
|
|
268
|
+
export type NormalizedCallRelationship = Extract<
|
|
269
|
+
NormalizedSoftwareRelationship,
|
|
270
|
+
{ kind: "call" }
|
|
271
|
+
>;
|
|
272
|
+
|
|
273
|
+
export type NormalizedSemanticRelationship = Extract<
|
|
274
|
+
NormalizedSoftwareRelationship,
|
|
275
|
+
{ kind: "semantic" }
|
|
276
|
+
>;
|
|
277
|
+
|
|
278
|
+
export interface NormalizedSoftwareModel {
|
|
279
|
+
elements: NormalizedSoftwareElement[];
|
|
280
|
+
elementsByPath: ReadonlyMap<string, NormalizedSoftwareElement>;
|
|
281
|
+
relationships: NormalizedSoftwareRelationship[];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface PendingRelationship {
|
|
285
|
+
input: SoftwareRelationshipInput;
|
|
286
|
+
scopePath?: string;
|
|
287
|
+
index: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface ElementDraft {
|
|
291
|
+
type: SoftwareElementType;
|
|
292
|
+
id: string;
|
|
293
|
+
path: string;
|
|
294
|
+
parentPath?: string;
|
|
295
|
+
label: string;
|
|
296
|
+
description?: string;
|
|
297
|
+
changeStatus?: SoftwareChangeStatus;
|
|
298
|
+
coverage?: NormalizedSoftwareCoverage;
|
|
299
|
+
external?: boolean;
|
|
300
|
+
dataStoreKind?: SoftwareDataStoreKind;
|
|
301
|
+
dataStoreSchema?: NormalizedSoftwareDataStoreSchema;
|
|
302
|
+
sourceRanges?: SoftwareSourceRange[];
|
|
303
|
+
children: string[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export class SoftwareModelValidationError extends Error {
|
|
307
|
+
errors: string[];
|
|
308
|
+
|
|
309
|
+
constructor(errors: string[]) {
|
|
310
|
+
super(`Invalid software model:\n- ${errors.join("\n- ")}`);
|
|
311
|
+
this.name = "SoftwareModelValidationError";
|
|
312
|
+
this.errors = errors;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function defineSoftwareMap(
|
|
317
|
+
input: SoftwareModelInput,
|
|
318
|
+
): NormalizedSoftwareModel {
|
|
319
|
+
const errors: string[] = [];
|
|
320
|
+
const elements: ElementDraft[] = [];
|
|
321
|
+
const elementsByPath = new Map<string, ElementDraft>();
|
|
322
|
+
const pendingRelationships: PendingRelationship[] = [];
|
|
323
|
+
|
|
324
|
+
if ("views" in input) {
|
|
325
|
+
errors.push(
|
|
326
|
+
"Software model must not author views; SoftwareMap derives inline C4 projection from elements, relationships, and expansion state.",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
flattenElements({
|
|
331
|
+
collection: input.people,
|
|
332
|
+
type: "person",
|
|
333
|
+
parentPath: undefined,
|
|
334
|
+
elements,
|
|
335
|
+
elementsByPath,
|
|
336
|
+
pendingRelationships,
|
|
337
|
+
errors,
|
|
338
|
+
});
|
|
339
|
+
flattenElements({
|
|
340
|
+
collection: input.systems,
|
|
341
|
+
type: "softwareSystem",
|
|
342
|
+
parentPath: undefined,
|
|
343
|
+
elements,
|
|
344
|
+
elementsByPath,
|
|
345
|
+
pendingRelationships,
|
|
346
|
+
errors,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
for (const [index, relationship] of (input.relationships ?? []).entries()) {
|
|
350
|
+
pendingRelationships.push({ input: relationship, index });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const relationships = normalizeRelationships(
|
|
354
|
+
pendingRelationships,
|
|
355
|
+
elementsByPath,
|
|
356
|
+
errors,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (errors.length > 0) {
|
|
360
|
+
throw new SoftwareModelValidationError(errors);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const normalizedElements = elements.map((element) => Object.freeze(element));
|
|
364
|
+
|
|
365
|
+
return Object.freeze({
|
|
366
|
+
elements: normalizedElements,
|
|
367
|
+
elementsByPath: new Map(
|
|
368
|
+
normalizedElements.map((element) => [element.path, element]),
|
|
369
|
+
),
|
|
370
|
+
relationships,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function flattenElements({
|
|
375
|
+
collection,
|
|
376
|
+
type,
|
|
377
|
+
parentPath,
|
|
378
|
+
elements,
|
|
379
|
+
elementsByPath,
|
|
380
|
+
pendingRelationships,
|
|
381
|
+
errors,
|
|
382
|
+
}: {
|
|
383
|
+
collection: SoftwareElementCollection<SoftwareElementBaseInput> | undefined;
|
|
384
|
+
type: SoftwareElementType;
|
|
385
|
+
parentPath: string | undefined;
|
|
386
|
+
elements: ElementDraft[];
|
|
387
|
+
elementsByPath: Map<string, ElementDraft>;
|
|
388
|
+
pendingRelationships: PendingRelationship[];
|
|
389
|
+
errors: string[];
|
|
390
|
+
}) {
|
|
391
|
+
if (!collection) return;
|
|
392
|
+
|
|
393
|
+
const siblings = entriesForCollection(collection);
|
|
394
|
+
const siblingIds = new Set<string>();
|
|
395
|
+
|
|
396
|
+
for (const [collectionKey, input] of siblings) {
|
|
397
|
+
const id = input.id ?? collectionKey;
|
|
398
|
+
const path = parentPath ? `${parentPath}.${id}` : id;
|
|
399
|
+
|
|
400
|
+
if (!id) {
|
|
401
|
+
errors.push(`Software ${type} under ${parentPath ?? "model"} has no id.`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (siblingIds.has(id)) {
|
|
406
|
+
errors.push(
|
|
407
|
+
`Duplicate ${type} id "${id}" under ${parentPath ?? "model"}.`,
|
|
408
|
+
);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
siblingIds.add(id);
|
|
413
|
+
|
|
414
|
+
if (elementsByPath.has(path)) {
|
|
415
|
+
errors.push(`Duplicate element path "${path}".`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
validateElementFields(type, path, input, errors);
|
|
420
|
+
|
|
421
|
+
const draft: ElementDraft = {
|
|
422
|
+
type,
|
|
423
|
+
id,
|
|
424
|
+
path,
|
|
425
|
+
parentPath,
|
|
426
|
+
label: input.label ?? id,
|
|
427
|
+
description: input.description,
|
|
428
|
+
changeStatus: input.changeStatus,
|
|
429
|
+
coverage: normalizeCoverage(type, path, input.coverage, errors),
|
|
430
|
+
children: [],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (parentPath) {
|
|
434
|
+
elementsByPath.get(parentPath)?.children.push(path);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (type === "softwareSystem") {
|
|
438
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
439
|
+
draft.external = (input as SoftwareSystemInput).external;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (type === "dataStore") {
|
|
443
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
444
|
+
const dataStore = input as DataStoreInput;
|
|
445
|
+
draft.dataStoreKind = dataStore.kind ?? "database";
|
|
446
|
+
draft.dataStoreSchema = normalizeDataStoreSchema(path, dataStore, errors);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (type === "codeElement") {
|
|
450
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
451
|
+
const codeElement = input as CodeElementInput;
|
|
452
|
+
draft.sourceRanges = codeElement.sourceRanges;
|
|
453
|
+
validateSourceRanges(
|
|
454
|
+
codeElement.sourceRanges,
|
|
455
|
+
`Code element "${path}" sourceRanges`,
|
|
456
|
+
errors,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
elements.push(draft);
|
|
461
|
+
elementsByPath.set(path, draft);
|
|
462
|
+
|
|
463
|
+
for (const [index, relationship] of (input.relationships ?? []).entries()) {
|
|
464
|
+
pendingRelationships.push({
|
|
465
|
+
input: relationship,
|
|
466
|
+
scopePath: path,
|
|
467
|
+
index,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (type === "softwareSystem") {
|
|
472
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
473
|
+
const system = input as SoftwareSystemInput;
|
|
474
|
+
flattenElements({
|
|
475
|
+
collection: system.containers,
|
|
476
|
+
type: "container",
|
|
477
|
+
parentPath: path,
|
|
478
|
+
elements,
|
|
479
|
+
elementsByPath,
|
|
480
|
+
pendingRelationships,
|
|
481
|
+
errors,
|
|
482
|
+
});
|
|
483
|
+
flattenElements({
|
|
484
|
+
collection: system.dataStores,
|
|
485
|
+
type: "dataStore",
|
|
486
|
+
parentPath: path,
|
|
487
|
+
elements,
|
|
488
|
+
elementsByPath,
|
|
489
|
+
pendingRelationships,
|
|
490
|
+
errors,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (type === "container" || type === "dataStore") {
|
|
495
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
496
|
+
flattenElements({
|
|
497
|
+
collection: (input as ContainerInput | DataStoreInput).components,
|
|
498
|
+
type: "component",
|
|
499
|
+
parentPath: path,
|
|
500
|
+
elements,
|
|
501
|
+
elementsByPath,
|
|
502
|
+
pendingRelationships,
|
|
503
|
+
errors,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (type === "component") {
|
|
508
|
+
// SAFETY: `type` names the collection `input` came from.
|
|
509
|
+
flattenElements({
|
|
510
|
+
collection: (input as ComponentInput).codeElements,
|
|
511
|
+
type: "codeElement",
|
|
512
|
+
parentPath: path,
|
|
513
|
+
elements,
|
|
514
|
+
elementsByPath,
|
|
515
|
+
pendingRelationships,
|
|
516
|
+
errors,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function entriesForCollection<T extends SoftwareElementBaseInput>(
|
|
523
|
+
collection: SoftwareElementCollection<T>,
|
|
524
|
+
): Array<[string, T]> {
|
|
525
|
+
if (Array.isArray(collection)) {
|
|
526
|
+
return collection.map((item, index) => [item.id ?? String(index), item]);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return Object.entries(collection);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function validateElementFields(
|
|
533
|
+
type: SoftwareElementType,
|
|
534
|
+
path: string,
|
|
535
|
+
input: SoftwareElementBaseInput,
|
|
536
|
+
errors: string[],
|
|
537
|
+
) {
|
|
538
|
+
if ("additions" in input || "deletions" in input) {
|
|
539
|
+
errors.push(
|
|
540
|
+
`Element "${path}" must not author additions or deletions; diff counts are computed automatically.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (
|
|
545
|
+
"changeStatus" in input &&
|
|
546
|
+
input.changeStatus !== undefined &&
|
|
547
|
+
input.changeStatus !== "added" &&
|
|
548
|
+
input.changeStatus !== "removed" &&
|
|
549
|
+
input.changeStatus !== "modified" &&
|
|
550
|
+
input.changeStatus !== "unchanged"
|
|
551
|
+
) {
|
|
552
|
+
errors.push(
|
|
553
|
+
`Element "${path}" changeStatus must be one of "added", "removed", "modified", or "unchanged".`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (
|
|
558
|
+
type === "dataStore" &&
|
|
559
|
+
"kind" in input &&
|
|
560
|
+
input.kind !== undefined &&
|
|
561
|
+
input.kind !== "database" &&
|
|
562
|
+
input.kind !== "objectStore" &&
|
|
563
|
+
input.kind !== "bucket" &&
|
|
564
|
+
input.kind !== "artifactStore" &&
|
|
565
|
+
input.kind !== "fileStore"
|
|
566
|
+
) {
|
|
567
|
+
errors.push(
|
|
568
|
+
`Data store "${path}" kind must be one of "database", "objectStore", "bucket", "artifactStore", or "fileStore".`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (type !== "dataStore" && "kind" in input) {
|
|
573
|
+
errors.push(`Only data stores may define kind: "${path}".`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (type !== "dataStore" && ("tables" in input || "documents" in input)) {
|
|
577
|
+
errors.push(`Only data stores may define tables or documents: "${path}".`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (type === "codeElement") {
|
|
581
|
+
if ("codeElements" in input) {
|
|
582
|
+
errors.push(`Code element "${path}" cannot contain code elements.`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (
|
|
587
|
+
"coverage" in input &&
|
|
588
|
+
input.coverage !== undefined &&
|
|
589
|
+
type !== "softwareSystem" &&
|
|
590
|
+
type !== "container" &&
|
|
591
|
+
type !== "dataStore" &&
|
|
592
|
+
type !== "component"
|
|
593
|
+
) {
|
|
594
|
+
errors.push(
|
|
595
|
+
`Element "${path}" coverage may only be authored on systems, containers, data stores, or components.`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function normalizeDataStoreSchema(
|
|
601
|
+
path: string,
|
|
602
|
+
dataStore: DataStoreInput,
|
|
603
|
+
errors: string[],
|
|
604
|
+
): NormalizedSoftwareDataStoreSchema | undefined {
|
|
605
|
+
validateDataStoreCollectionRecord(path, "tables", dataStore.tables, errors);
|
|
606
|
+
validateDataStoreCollectionRecord(
|
|
607
|
+
path,
|
|
608
|
+
"documents",
|
|
609
|
+
dataStore.documents,
|
|
610
|
+
errors,
|
|
611
|
+
);
|
|
612
|
+
const tables = normalizeDataStoreCollections(dataStore.tables);
|
|
613
|
+
const documents = normalizeDataStoreCollections(dataStore.documents);
|
|
614
|
+
|
|
615
|
+
if (Object.keys(tables).length === 0 && Object.keys(documents).length === 0) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return Object.freeze({
|
|
620
|
+
tables: Object.freeze(tables),
|
|
621
|
+
documents: Object.freeze(documents),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function validateDataStoreCollectionRecord(
|
|
626
|
+
path: string,
|
|
627
|
+
property: "tables" | "documents",
|
|
628
|
+
collections: Record<string, SoftwareDataStoreCollectionInput> | undefined,
|
|
629
|
+
errors: string[],
|
|
630
|
+
) {
|
|
631
|
+
if (collections === undefined) return;
|
|
632
|
+
|
|
633
|
+
if (!isAuthoredObject(collections)) {
|
|
634
|
+
errors.push(`Data store "${path}" ${property} must be an object.`);
|
|
635
|
+
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
for (const [collectionId, collection] of Object.entries(collections)) {
|
|
640
|
+
if (!collectionId.trim()) {
|
|
641
|
+
errors.push(`Data store "${path}" ${property} contains an empty id.`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!isAuthoredObject(collection)) {
|
|
646
|
+
errors.push(
|
|
647
|
+
`Data store "${path}" ${property}.${collectionId} must be an object.`,
|
|
648
|
+
);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
validateDataStoreFieldSchema(
|
|
653
|
+
collection.schema,
|
|
654
|
+
`Data store "${path}" ${property}.${collectionId}.schema`,
|
|
655
|
+
errors,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function normalizeDataStoreCollections(
|
|
661
|
+
collections: Record<string, SoftwareDataStoreCollectionInput> | undefined,
|
|
662
|
+
): Record<string, NormalizedSoftwareDataStoreCollection> {
|
|
663
|
+
return Object.fromEntries(
|
|
664
|
+
Object.entries(collections ?? {}).map(([id, collection]) => [
|
|
665
|
+
id,
|
|
666
|
+
Object.freeze({
|
|
667
|
+
id,
|
|
668
|
+
label: collection.label ?? id,
|
|
669
|
+
key: collection.key,
|
|
670
|
+
schema: collection.schema,
|
|
671
|
+
}),
|
|
672
|
+
]),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function validateDataStoreFieldSchema(
|
|
677
|
+
schema: SoftwareDataStoreFieldSchema | undefined,
|
|
678
|
+
path: string,
|
|
679
|
+
errors: string[],
|
|
680
|
+
) {
|
|
681
|
+
if (!isAuthoredObject(schema)) {
|
|
682
|
+
errors.push(`${path} must be an object.`);
|
|
683
|
+
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
for (const [field, value] of Object.entries(schema)) {
|
|
688
|
+
if (!field.trim()) {
|
|
689
|
+
errors.push(`${path} contains an empty field name.`);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const fieldPath = `${path}.${field}`;
|
|
694
|
+
|
|
695
|
+
if (!isAuthoredObject(value)) {
|
|
696
|
+
errors.push(`${fieldPath} must be a field object or nested schema.`);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (isDataStoreFieldLeaf(value)) {
|
|
701
|
+
if (!value.type.trim()) {
|
|
702
|
+
errors.push(`${fieldPath}.type must not be empty.`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (value.schema) {
|
|
706
|
+
validateDataStoreFieldSchema(
|
|
707
|
+
value.schema,
|
|
708
|
+
`${fieldPath}.schema`,
|
|
709
|
+
errors,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
validateDataStoreFieldSchema(value, fieldPath, errors);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const DataStoreFieldLeafSchema = z.object({ type: z.string() });
|
|
721
|
+
|
|
722
|
+
function isDataStoreFieldLeaf(
|
|
723
|
+
value: SoftwareDataStoreFieldSchema[string],
|
|
724
|
+
): value is SoftwareDataStoreFieldLeaf {
|
|
725
|
+
return DataStoreFieldLeafSchema.safeParse(value).success;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizeCoverage(
|
|
729
|
+
type: SoftwareElementType,
|
|
730
|
+
path: string,
|
|
731
|
+
coverage: SoftwareCoverageInput | undefined,
|
|
732
|
+
errors: string[],
|
|
733
|
+
): NormalizedSoftwareCoverage | undefined {
|
|
734
|
+
if (!coverage) return undefined;
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
type !== "softwareSystem" &&
|
|
738
|
+
type !== "container" &&
|
|
739
|
+
type !== "dataStore" &&
|
|
740
|
+
type !== "component"
|
|
741
|
+
) {
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const files: NormalizedSoftwareCoverageFile[] = [];
|
|
746
|
+
const globs: string[] = [];
|
|
747
|
+
|
|
748
|
+
if (coverage.files !== undefined && !Array.isArray(coverage.files)) {
|
|
749
|
+
errors.push(`Element "${path}" coverage.files must be an array.`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const [index, file] of (coverage.files ?? []).entries()) {
|
|
753
|
+
if (isAuthoredString(file)) {
|
|
754
|
+
if (!file.trim()) {
|
|
755
|
+
errors.push(`Element "${path}" coverage.files[${index}] is empty.`);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
files.push({ path: file, ranges: [] });
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!isAuthoredObject(file) || !file.path) {
|
|
764
|
+
errors.push(
|
|
765
|
+
`Element "${path}" coverage.files[${index}] must be a file path or object with path.`,
|
|
766
|
+
);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
files.push({
|
|
771
|
+
path: file.path,
|
|
772
|
+
ranges: file.ranges ?? [],
|
|
773
|
+
});
|
|
774
|
+
validateLineRanges(
|
|
775
|
+
file.ranges,
|
|
776
|
+
`Element "${path}" coverage.files[${index}].ranges`,
|
|
777
|
+
errors,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (coverage.globs !== undefined && !Array.isArray(coverage.globs)) {
|
|
782
|
+
errors.push(`Element "${path}" coverage.globs must be an array.`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
for (const [index, glob] of (coverage.globs ?? []).entries()) {
|
|
786
|
+
if (!isAuthoredString(glob) || !glob.trim()) {
|
|
787
|
+
errors.push(`Element "${path}" coverage.globs[${index}] is empty.`);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
globs.push(glob);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (files.length === 0 && globs.length === 0) return undefined;
|
|
795
|
+
|
|
796
|
+
return Object.freeze({
|
|
797
|
+
files: files.map((file) => Object.freeze(file)),
|
|
798
|
+
globs,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function normalizeRelationships(
|
|
803
|
+
pendingRelationships: PendingRelationship[],
|
|
804
|
+
elementsByPath: Map<string, ElementDraft>,
|
|
805
|
+
errors: string[],
|
|
806
|
+
): NormalizedSoftwareRelationship[] {
|
|
807
|
+
const relationships: NormalizedSoftwareRelationship[] = [];
|
|
808
|
+
|
|
809
|
+
for (const pending of pendingRelationships) {
|
|
810
|
+
const from = resolveEndpoint(
|
|
811
|
+
pending.input.from,
|
|
812
|
+
pending.scopePath,
|
|
813
|
+
elementsByPath,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const to = resolveEndpoint(
|
|
817
|
+
pending.input.to,
|
|
818
|
+
pending.scopePath,
|
|
819
|
+
elementsByPath,
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const relationshipLabel = `relationships[${pending.index}] at ${pending.scopePath ?? "model"}`;
|
|
823
|
+
|
|
824
|
+
if (!from) {
|
|
825
|
+
errors.push(
|
|
826
|
+
`${relationshipLabel}.from: "${pending.input.from}" does not match an element path or data store schema path. Use an existing full path or a name relative to this scope or its parent.`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!to) {
|
|
831
|
+
errors.push(
|
|
832
|
+
`${relationshipLabel}.to: "${pending.input.to}" does not match an element path or data store schema path. Use an existing full path or a name relative to this scope or its parent.`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
validateRelationshipFields(pending, errors);
|
|
837
|
+
|
|
838
|
+
if (!from || !to) continue;
|
|
839
|
+
|
|
840
|
+
const base = {
|
|
841
|
+
id: relationshipIdForPending(pending),
|
|
842
|
+
from,
|
|
843
|
+
to,
|
|
844
|
+
scopePath: pending.scopePath,
|
|
845
|
+
label: pending.input.label,
|
|
846
|
+
description: pending.input.description,
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
if (pending.input.kind === "call") {
|
|
850
|
+
relationships.push(
|
|
851
|
+
Object.freeze({
|
|
852
|
+
...base,
|
|
853
|
+
kind: "call",
|
|
854
|
+
nthCallSite: pending.input.nthCallSite ?? 0,
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
} else {
|
|
858
|
+
relationships.push(
|
|
859
|
+
Object.freeze({
|
|
860
|
+
...base,
|
|
861
|
+
kind: "semantic",
|
|
862
|
+
semanticKind: pending.input.semanticKind,
|
|
863
|
+
sourceRanges: pending.input.sourceRanges,
|
|
864
|
+
}),
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return relationships;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function validateRelationshipFields(
|
|
873
|
+
pending: PendingRelationship,
|
|
874
|
+
errors: string[],
|
|
875
|
+
) {
|
|
876
|
+
if (pending.input.kind === "call") {
|
|
877
|
+
const nthCallSite = pending.input.nthCallSite ?? 0;
|
|
878
|
+
|
|
879
|
+
if (!Number.isInteger(nthCallSite) || nthCallSite < 0) {
|
|
880
|
+
errors.push(
|
|
881
|
+
`Call relationship "${relationshipIdForPending(
|
|
882
|
+
pending,
|
|
883
|
+
)}" must use a non-negative integer nthCallSite.`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
validateLineRanges(
|
|
891
|
+
pending.input.sourceRanges,
|
|
892
|
+
`Semantic relationship "${relationshipIdForPending(pending)}" sourceRanges`,
|
|
893
|
+
errors,
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function resolveEndpoint(
|
|
898
|
+
endpoint: string,
|
|
899
|
+
scopePath: string | undefined,
|
|
900
|
+
elementsByPath: Map<string, ElementDraft>,
|
|
901
|
+
): string | undefined {
|
|
902
|
+
const candidates: string[] = [];
|
|
903
|
+
|
|
904
|
+
if (scopePath && endpoint === ".") {
|
|
905
|
+
candidates.push(scopePath);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (scopePath && endpoint !== ".") {
|
|
909
|
+
candidates.push(`${scopePath}.${endpoint}`);
|
|
910
|
+
const parentPath = parentPathFor(scopePath);
|
|
911
|
+
|
|
912
|
+
if (parentPath) candidates.push(`${parentPath}.${endpoint}`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
candidates.push(endpoint);
|
|
916
|
+
|
|
917
|
+
for (const candidate of candidates) {
|
|
918
|
+
if (elementsByPath.has(candidate)) return candidate;
|
|
919
|
+
|
|
920
|
+
const schemaEndpoint = resolveDataStoreSchemaEndpoint(
|
|
921
|
+
candidate,
|
|
922
|
+
elementsByPath,
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
if (schemaEndpoint) return schemaEndpoint;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export interface ResolvedDataStoreSchemaEndpoint {
|
|
932
|
+
dataStorePath: string;
|
|
933
|
+
collectionKind: "tables" | "documents";
|
|
934
|
+
collectionId: string;
|
|
935
|
+
fieldPath: string[];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export function parseDataStoreSchemaEndpoint(
|
|
939
|
+
endpoint: string,
|
|
940
|
+
elementsByPath: ReadonlyMap<
|
|
941
|
+
string,
|
|
942
|
+
Pick<NormalizedSoftwareElement, "type" | "dataStoreSchema">
|
|
943
|
+
>,
|
|
944
|
+
): ResolvedDataStoreSchemaEndpoint | undefined {
|
|
945
|
+
for (const [dataStorePath, element] of elementsByPath) {
|
|
946
|
+
if (element.type !== "dataStore" || !element.dataStoreSchema) continue;
|
|
947
|
+
|
|
948
|
+
const parsed = parseDataStoreSchemaEndpointForStore(
|
|
949
|
+
endpoint,
|
|
950
|
+
dataStorePath,
|
|
951
|
+
element.dataStoreSchema,
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
if (parsed) return parsed;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function resolveDataStoreSchemaEndpoint(
|
|
961
|
+
endpoint: string,
|
|
962
|
+
elementsByPath: ReadonlyMap<string, ElementDraft>,
|
|
963
|
+
): string | undefined {
|
|
964
|
+
return parseDataStoreSchemaEndpoint(endpoint, elementsByPath)
|
|
965
|
+
? endpoint
|
|
966
|
+
: undefined;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function parseDataStoreSchemaEndpointForStore(
|
|
970
|
+
endpoint: string,
|
|
971
|
+
dataStorePath: string,
|
|
972
|
+
schema: NormalizedSoftwareDataStoreSchema,
|
|
973
|
+
): ResolvedDataStoreSchemaEndpoint | undefined {
|
|
974
|
+
for (const collectionKind of ["tables", "documents"] as const) {
|
|
975
|
+
const prefix = `${dataStorePath}.${collectionKind}.`;
|
|
976
|
+
|
|
977
|
+
if (!endpoint.startsWith(prefix)) continue;
|
|
978
|
+
const parts = endpoint.slice(prefix.length).split(".").filter(Boolean);
|
|
979
|
+
|
|
980
|
+
if (parts.length === 0) continue;
|
|
981
|
+
const [collectionId, ...fieldPath] = parts;
|
|
982
|
+
const collection = schema[collectionKind][collectionId];
|
|
983
|
+
|
|
984
|
+
if (!collection) continue;
|
|
985
|
+
|
|
986
|
+
if (
|
|
987
|
+
fieldPath.length > 0 &&
|
|
988
|
+
!dataStoreFieldPathExists(collection.schema, fieldPath)
|
|
989
|
+
) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
dataStorePath,
|
|
995
|
+
collectionKind,
|
|
996
|
+
collectionId,
|
|
997
|
+
fieldPath,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function dataStoreFieldPathExists(
|
|
1005
|
+
schema: SoftwareDataStoreFieldSchema,
|
|
1006
|
+
fieldPath: readonly string[],
|
|
1007
|
+
): boolean {
|
|
1008
|
+
let current: SoftwareDataStoreFieldSchema = schema;
|
|
1009
|
+
|
|
1010
|
+
for (const [index, part] of fieldPath.entries()) {
|
|
1011
|
+
const next: SoftwareDataStoreFieldSchema | SoftwareDataStoreFieldLeaf =
|
|
1012
|
+
current[part];
|
|
1013
|
+
|
|
1014
|
+
if (!next) return false;
|
|
1015
|
+
|
|
1016
|
+
if (index === fieldPath.length - 1) return true;
|
|
1017
|
+
|
|
1018
|
+
if (isDataStoreFieldLeaf(next)) {
|
|
1019
|
+
if (!next.schema) return false;
|
|
1020
|
+
current = next.schema;
|
|
1021
|
+
} else {
|
|
1022
|
+
current = next;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return fieldPath.length === 0;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function validateSourceRanges(
|
|
1030
|
+
sourceRanges: SoftwareSourceRange[] | undefined,
|
|
1031
|
+
label: string,
|
|
1032
|
+
errors: string[],
|
|
1033
|
+
) {
|
|
1034
|
+
for (const [index, range] of (sourceRanges ?? []).entries()) {
|
|
1035
|
+
if (!isAuthoredString(range.file) || range.file.trim().length === 0) {
|
|
1036
|
+
errors.push(`${label}[${index}].file must be a non-empty string.`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (
|
|
1040
|
+
!Number.isInteger(range.fromLine) ||
|
|
1041
|
+
!Number.isInteger(range.toLine) ||
|
|
1042
|
+
range.fromLine < 1 ||
|
|
1043
|
+
range.toLine < range.fromLine
|
|
1044
|
+
) {
|
|
1045
|
+
errors.push(
|
|
1046
|
+
`${label}[${index}] must use positive inclusive line numbers with fromLine <= toLine.`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function validateLineRanges(
|
|
1053
|
+
ranges: SoftwareLineRange[] | undefined,
|
|
1054
|
+
label: string,
|
|
1055
|
+
errors: string[],
|
|
1056
|
+
) {
|
|
1057
|
+
for (const [index, range] of (ranges ?? []).entries()) {
|
|
1058
|
+
if (
|
|
1059
|
+
!Number.isInteger(range.fromLine) ||
|
|
1060
|
+
!Number.isInteger(range.toLine) ||
|
|
1061
|
+
range.fromLine < 1 ||
|
|
1062
|
+
range.toLine < range.fromLine
|
|
1063
|
+
) {
|
|
1064
|
+
errors.push(
|
|
1065
|
+
`${label}[${index}] must use positive inclusive line numbers with fromLine <= toLine.`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function relationshipIdForPending(pending: PendingRelationship) {
|
|
1072
|
+
return `${pending.scopePath ?? "model"}.relationship.${pending.index}`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function parentPathFor(path: string) {
|
|
1076
|
+
const lastDot = path.lastIndexOf(".");
|
|
1077
|
+
|
|
1078
|
+
if (lastDot === -1) return undefined;
|
|
1079
|
+
|
|
1080
|
+
return path.slice(0, lastDot);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// software-map.ts is imported as a plain module, so authored values reach the
|
|
1084
|
+
// normalizer without type checking. These decode a value's representation once
|
|
1085
|
+
// before the normalizer reads its fields.
|
|
1086
|
+
const AuthoredObjectSchema = z.object({});
|
|
1087
|
+
|
|
1088
|
+
const AuthoredStringSchema = z.string();
|
|
1089
|
+
|
|
1090
|
+
function isAuthoredObject<T>(value: T | undefined): value is T {
|
|
1091
|
+
return AuthoredObjectSchema.safeParse(value).success;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function isAuthoredString(
|
|
1095
|
+
value: string | SoftwareCoverageFileInput,
|
|
1096
|
+
): value is string {
|
|
1097
|
+
return AuthoredStringSchema.safeParse(value).success;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** A normalized model as a software-map module exports it (holds a Map, so not JSON). */
|
|
1101
|
+
export function isNormalizedSoftwareModel(
|
|
1102
|
+
value: unknown,
|
|
1103
|
+
): value is NormalizedSoftwareModel {
|
|
1104
|
+
return (
|
|
1105
|
+
isObjectValue(value) &&
|
|
1106
|
+
"elements" in value &&
|
|
1107
|
+
Array.isArray(value.elements) &&
|
|
1108
|
+
"elementsByPath" in value &&
|
|
1109
|
+
value.elementsByPath instanceof Map &&
|
|
1110
|
+
"relationships" in value &&
|
|
1111
|
+
Array.isArray(value.relationships)
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export const softwareModelDataSchema = z.strictObject({
|
|
1116
|
+
elements: z.array(normalizedSoftwareElementSchema),
|
|
1117
|
+
relationships: z.array(normalizedSoftwareRelationshipSchema),
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
export const SOFTWARE_MAP_DATA_FORMAT = "software-map/1";
|
|
1121
|
+
|
|
1122
|
+
/** The on-disk envelope, shared by publishers, readers and the canvas. */
|
|
1123
|
+
export const softwareMapDataFileSchema = softwareModelDataSchema.extend({
|
|
1124
|
+
format: z.literal(SOFTWARE_MAP_DATA_FORMAT),
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
/** JSON projection of a normalized model. `elementsByPath` is derived and is
|
|
1128
|
+
rebuilt on load by `hydrateSoftwareModel`. */
|
|
1129
|
+
export type SoftwareModelData = z.infer<typeof softwareModelDataSchema>;
|
|
1130
|
+
|
|
1131
|
+
export function softwareModelData(
|
|
1132
|
+
model: NormalizedSoftwareModel,
|
|
1133
|
+
): SoftwareModelData {
|
|
1134
|
+
return { elements: model.elements, relationships: model.relationships };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export function hydrateSoftwareModel(
|
|
1138
|
+
data: SoftwareModelData,
|
|
1139
|
+
): NormalizedSoftwareModel {
|
|
1140
|
+
return {
|
|
1141
|
+
elements: data.elements,
|
|
1142
|
+
relationships: data.relationships,
|
|
1143
|
+
elementsByPath: new Map(
|
|
1144
|
+
data.elements.map((element) => [element.path, element]),
|
|
1145
|
+
),
|
|
1146
|
+
};
|
|
1147
|
+
}
|