@sdd-method/author-cli 0.0.0-stage → 0.1.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 +201 -0
- package/NOTICE +17 -0
- package/README.md +6 -2
- package/dist/author-bin.js +8 -0
- package/dist/author-program.js +99 -0
- package/dist/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +37 -0
- package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
- package/dist/lib/api-surface/analyzers/library.js +95 -0
- package/dist/lib/api-surface/analyzers/serverless.js +336 -0
- package/dist/lib/api-surface/analyzers/spring.js +237 -0
- package/dist/lib/api-surface/analyzers/terraform.js +109 -0
- package/dist/lib/api-surface/detect.js +25 -0
- package/dist/lib/api-surface/emit.js +109 -0
- package/dist/lib/api-surface/file-set.js +43 -0
- package/dist/lib/api-surface/index.js +134 -0
- package/dist/lib/api-surface/types.js +17 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +132 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
- package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
- package/dist/lib/catalogue/archetype/resolver.js +103 -0
- package/dist/lib/catalogue/archetype/wireframe.js +418 -0
- package/dist/lib/catalogue/baseline-check.js +151 -0
- package/dist/lib/catalogue/build.js +1502 -0
- package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
- package/dist/lib/catalogue/builders/adrs.js +222 -0
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
- package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
- package/dist/lib/catalogue/builders/c4-components.js +229 -0
- package/dist/lib/catalogue/builders/c4-containers.js +240 -0
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
- package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
- package/dist/lib/catalogue/builders/c4-systems.js +98 -0
- package/dist/lib/catalogue/builders/capabilities.js +488 -0
- package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
- package/dist/lib/catalogue/builders/capability-edges.js +256 -0
- package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
- package/dist/lib/catalogue/builders/capability-spec.js +348 -0
- package/dist/lib/catalogue/builders/container-dependency.js +183 -0
- package/dist/lib/catalogue/builders/contracts.js +402 -0
- package/dist/lib/catalogue/builders/data-items.js +344 -0
- package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
- package/dist/lib/catalogue/builders/domain-models.js +175 -0
- package/dist/lib/catalogue/builders/domains.js +119 -0
- package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
- package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
- package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
- package/dist/lib/catalogue/builders/entity-slug.js +47 -0
- package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
- package/dist/lib/catalogue/builders/event-edges.js +327 -0
- package/dist/lib/catalogue/builders/events.js +168 -0
- package/dist/lib/catalogue/builders/evidence-class.js +43 -0
- package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
- package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
- package/dist/lib/catalogue/builders/feature-contract.js +77 -0
- package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
- package/dist/lib/catalogue/builders/feature-groups.js +63 -0
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
- package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
- package/dist/lib/catalogue/builders/features.js +300 -0
- package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
- package/dist/lib/catalogue/builders/glossary.js +204 -0
- package/dist/lib/catalogue/builders/implementations.js +214 -0
- package/dist/lib/catalogue/builders/index.js +64 -0
- package/dist/lib/catalogue/builders/integration-components.js +344 -0
- package/dist/lib/catalogue/builders/integrations.js +181 -0
- package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
- package/dist/lib/catalogue/builders/persona-container.js +133 -0
- package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
- package/dist/lib/catalogue/builders/personas.js +152 -0
- package/dist/lib/catalogue/builders/product-edges.js +106 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
- package/dist/lib/catalogue/builders/products.js +162 -0
- package/dist/lib/catalogue/builders/quality-scores.js +635 -0
- package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-edges.js +340 -0
- package/dist/lib/catalogue/builders/repositories.js +458 -0
- package/dist/lib/catalogue/builders/scaffolds.js +197 -0
- package/dist/lib/catalogue/builders/screen-tree.js +222 -0
- package/dist/lib/catalogue/builders/screens.js +396 -0
- package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
- package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
- package/dist/lib/catalogue/builders/service-contract.js +74 -0
- package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
- package/dist/lib/catalogue/builders/services.js +232 -0
- package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
- package/dist/lib/catalogue/builders/user-journeys.js +253 -0
- package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/contract-id.js +49 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +552 -0
- package/dist/lib/catalogue/coverage-run.js +115 -0
- package/dist/lib/catalogue/csv-reader.js +97 -0
- package/dist/lib/catalogue/csv-writer.js +96 -0
- package/dist/lib/catalogue/data-plane-coherence.js +77 -0
- package/dist/lib/catalogue/ddl-generator.js +233 -0
- package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
- package/dist/lib/catalogue/engine-validator.js +239 -0
- package/dist/lib/catalogue/ext-id.js +76 -0
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
- package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
- package/dist/lib/catalogue/finding-identity.js +183 -0
- package/dist/lib/catalogue/findings.js +46 -0
- package/dist/lib/catalogue/fk-validator.js +136 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +435 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/population-checks.js +87 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/reverse-product-quality.js +99 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
- package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
- package/dist/lib/catalogue/scaffold-delta.js +261 -0
- package/dist/lib/catalogue/schema-compat.js +52 -0
- package/dist/lib/catalogue/schema-loader.js +87 -0
- package/dist/lib/catalogue/scores-coverage.js +573 -0
- package/dist/lib/catalogue/screen-validation-loader.js +89 -0
- package/dist/lib/catalogue/service-architecture-parser.js +193 -0
- package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/traceability-loader.js +350 -0
- package/dist/lib/catalogue/validate.js +200 -0
- package/dist/lib/catalogue/version.js +18 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/conformance/finding.js +69 -0
- package/dist/lib/conformance/index.js +578 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/contracts-availability/index.js +267 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/list/discover.js +100 -0
- package/dist/lib/list/format.js +84 -0
- package/dist/lib/list/index.js +16 -0
- package/dist/lib/lock-file.js +437 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/schema.js +121 -0
- package/dist/lib/pack/seed.js +182 -0
- package/dist/lib/pack/validate.js +49 -0
- package/dist/lib/package-version.js +92 -0
- package/dist/lib/plan/index.js +69 -0
- package/dist/lib/plan/read-sdd.js +102 -0
- package/dist/lib/plan/render-template.js +84 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scaffold/domain.js +234 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/sdd-index/repo-config.js +245 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/snapshot/advance.js +454 -0
- package/dist/lib/snapshot/diff.js +190 -0
- package/dist/lib/snapshot/fold.js +208 -0
- package/dist/lib/snapshot/land.js +178 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/snapshot/targets.js +169 -0
- package/dist/lib/snapshot/verify.js +91 -0
- package/dist/lib/sync/briefing-merge.js +106 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/yaml-block-merge.js +120 -0
- package/dist/lib/validate/aggregate.js +51 -0
- package/dist/lib/validate/bundle-coherence.js +74 -0
- package/dist/lib/validate/dispatch.js +243 -0
- package/dist/lib/validate/index.js +110 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/api-surface.js +71 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/conformance.js +34 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/contracts.js +34 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/plan.js +42 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/scaffold.js +83 -0
- package/dist/verbs/snapshot.js +134 -0
- package/dist/verbs/validate.js +48 -0
- package/dist/verify-program.js +122 -0
- package/dist/verify-version.json +4 -0
- package/package.json +32 -4
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- package/scripts/validate-integration-docs.sh +505 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Walker for `docs/domains/<domain>/contracts/events/event-catalog.yaml` —
|
|
2
|
+
// the reverse-method canonical source for events per ADR 0156.
|
|
3
|
+
//
|
|
4
|
+
// One file per domain. Each `events[]` entry encodes the event's name,
|
|
5
|
+
// publisher_service_id, transport, schema_path, version, and consumers[].
|
|
6
|
+
// The walker emits rows for both `events.csv` and `service_event.csv`
|
|
7
|
+
// (publisher → publishes role; consumers[] → consumes role).
|
|
8
|
+
//
|
|
9
|
+
// Merges with the avsc-sourced forward path in events.ts: YAML wins on
|
|
10
|
+
// name/domain/publisher/transport/consumers; avsc augments schema_path/
|
|
11
|
+
// version when the YAML row leaves them null. See ADR 0156 §4 and the
|
|
12
|
+
// metamodel's §"Event sources" subsection for the precedence rules.
|
|
13
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { parse as parseYaml } from "yaml";
|
|
16
|
+
const RECOMMENDED_TRANSPORTS = new Set([
|
|
17
|
+
"kafka",
|
|
18
|
+
"sns",
|
|
19
|
+
"sqs",
|
|
20
|
+
"kinesis",
|
|
21
|
+
"eventbridge",
|
|
22
|
+
"mongodb-change-stream",
|
|
23
|
+
"nats",
|
|
24
|
+
"activemq",
|
|
25
|
+
"rabbitmq",
|
|
26
|
+
"jms",
|
|
27
|
+
"other",
|
|
28
|
+
]);
|
|
29
|
+
export async function walkEventCatalogs(inputs) {
|
|
30
|
+
const domainsDir = join(inputs.sddPath, "docs", "domains");
|
|
31
|
+
const byId = new Map();
|
|
32
|
+
let domainEntries;
|
|
33
|
+
try {
|
|
34
|
+
domainEntries = await readdir(domainsDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const code = err.code;
|
|
38
|
+
if (code === "ENOENT")
|
|
39
|
+
return { byId };
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of domainEntries) {
|
|
43
|
+
if (!entry.isDirectory())
|
|
44
|
+
continue;
|
|
45
|
+
const domain = entry.name;
|
|
46
|
+
const manifestPath = join(domainsDir, domain, "contracts", "events", "event-catalog.yaml");
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = await readFile(manifestPath, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const code = err.code;
|
|
53
|
+
if (code === "ENOENT")
|
|
54
|
+
continue;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = parseYaml(text);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
inputs.findings.add("convention", `docs/domains/${domain}/contracts/events/event-catalog.yaml: failed to parse YAML — ${err.message}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!raw || typeof raw !== "object")
|
|
66
|
+
continue;
|
|
67
|
+
const rawObj = raw;
|
|
68
|
+
const events = rawObj["events"];
|
|
69
|
+
if (!Array.isArray(events))
|
|
70
|
+
continue;
|
|
71
|
+
// File-level evidence-class fallback for events that don't declare
|
|
72
|
+
// their own (ADR 0175). Both key spellings accepted.
|
|
73
|
+
const fileEvidenceClass = String(rawObj["evidence_class"] ?? rawObj["evidence-class"] ?? "").trim();
|
|
74
|
+
// Track slugs derived within this domain so we can flag duplicates.
|
|
75
|
+
const slugsInDomain = new Set();
|
|
76
|
+
for (const event of events) {
|
|
77
|
+
if (!event || typeof event !== "object")
|
|
78
|
+
continue;
|
|
79
|
+
const e = event;
|
|
80
|
+
const explicitId = String(e["id"] ?? "").trim();
|
|
81
|
+
const name = String(e["name"] ?? "").trim();
|
|
82
|
+
// Derive id: explicit id wins when present (allows future override);
|
|
83
|
+
// otherwise slug from name. Original ADR 0156 coordination doc spec:
|
|
84
|
+
// "derive `id` from `name` (slugged)". Some adopters' authoring uses
|
|
85
|
+
// `name:` only (no `id:` field).
|
|
86
|
+
let id;
|
|
87
|
+
if (explicitId !== "") {
|
|
88
|
+
id = explicitId;
|
|
89
|
+
}
|
|
90
|
+
else if (name !== "") {
|
|
91
|
+
id = slugify(name);
|
|
92
|
+
if (slugsInDomain.has(id)) {
|
|
93
|
+
inputs.findings.add("divergence", `docs/domains/${domain}/contracts/events/event-catalog.yaml: duplicate slug-derived id "${id}" in domain ${domain} (from name "${name}") — first-seen kept`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
slugsInDomain.add(id);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
inputs.findings.add("convention", `docs/domains/${domain}/contracts/events/event-catalog.yaml: event entry missing both 'id' and 'name' — skipped`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Display name: explicit name wins; fall back to id when name absent.
|
|
103
|
+
const displayName = name !== "" ? name : id;
|
|
104
|
+
const publisher = String(e["publisher_service_id"] ?? e["producer"] ?? "").trim();
|
|
105
|
+
const transport = String(e["transport"] ?? "").trim();
|
|
106
|
+
const schemaPath = String(e["schema_path"] ?? "").trim();
|
|
107
|
+
const version = String(e["version"] ?? "").trim();
|
|
108
|
+
const ownEvidenceClass = String(e["evidence_class"] ?? e["evidence-class"] ?? "").trim();
|
|
109
|
+
const evidenceClass = ownEvidenceClass !== "" ? ownEvidenceClass : fileEvidenceClass;
|
|
110
|
+
const consumersRaw = e["consumers"];
|
|
111
|
+
const consumers = [];
|
|
112
|
+
if (Array.isArray(consumersRaw)) {
|
|
113
|
+
for (const c of consumersRaw) {
|
|
114
|
+
const cstr = String(c ?? "").trim();
|
|
115
|
+
if (cstr !== "")
|
|
116
|
+
consumers.push(cstr);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (transport !== "" && !RECOMMENDED_TRANSPORTS.has(transport)) {
|
|
120
|
+
inputs.findings.add("convention", `docs/domains/${domain}/contracts/events/event-catalog.yaml: event ${id} transport "${transport}" outside recommended set (per ADR 0156)`);
|
|
121
|
+
}
|
|
122
|
+
if (byId.has(id)) {
|
|
123
|
+
inputs.findings.add("divergence", `event ${id} declared in multiple event-catalog.yaml files — first-seen kept`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
byId.set(id, {
|
|
127
|
+
id,
|
|
128
|
+
name: displayName,
|
|
129
|
+
domain,
|
|
130
|
+
publisher,
|
|
131
|
+
transport,
|
|
132
|
+
schemaPath,
|
|
133
|
+
version,
|
|
134
|
+
consumers,
|
|
135
|
+
evidenceClass,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { byId };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Slugify an event name into an id.
|
|
143
|
+
*
|
|
144
|
+
* - Inserts a hyphen at camelCase boundaries (`AssetTwin` → `Asset-Twin`).
|
|
145
|
+
* - Lowercases.
|
|
146
|
+
* - Replaces any run of non-alphanumeric characters with a single hyphen.
|
|
147
|
+
* - Trims leading/trailing hyphens.
|
|
148
|
+
*
|
|
149
|
+
* Mirrors the slug derivation used elsewhere in the builder for consistency
|
|
150
|
+
* (capability ids, feature ids).
|
|
151
|
+
*/
|
|
152
|
+
function slugify(s) {
|
|
153
|
+
return s
|
|
154
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
157
|
+
.replace(/^-+|-+$/g, "");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Merge a YAML-parsed event with the equivalent avsc-derived row. YAML wins
|
|
161
|
+
* on name/domain/publisher/transport per ADR 0156 §4.1; avsc augments
|
|
162
|
+
* schema_path/version only when the YAML row leaves them empty.
|
|
163
|
+
*
|
|
164
|
+
* Returns a fresh CatalogueRow ready to write into events.csv.
|
|
165
|
+
*/
|
|
166
|
+
export function mergeYamlAndAvsc(yaml, avsc) {
|
|
167
|
+
const schemaPath = yaml.schemaPath !== ""
|
|
168
|
+
? yaml.schemaPath
|
|
169
|
+
: String(avsc?.["schema_path"] ?? "");
|
|
170
|
+
const version = yaml.version !== ""
|
|
171
|
+
? yaml.version
|
|
172
|
+
: String(avsc?.["version"] ?? "");
|
|
173
|
+
return {
|
|
174
|
+
id: yaml.id,
|
|
175
|
+
name: yaml.name,
|
|
176
|
+
publisher_service_id: yaml.publisher,
|
|
177
|
+
domain_id: yaml.domain,
|
|
178
|
+
transport: yaml.transport,
|
|
179
|
+
schema_path: schemaPath,
|
|
180
|
+
version,
|
|
181
|
+
// YAML-only field — avsc has no evidence slot, so YAML wins outright.
|
|
182
|
+
evidence_class: yaml.evidenceClass,
|
|
183
|
+
source_sdd_id: "",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=event-catalog-walker.js.map
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// service_event + capability_event edge builders.
|
|
2
|
+
//
|
|
3
|
+
// service_event sources (per ADR 0156):
|
|
4
|
+
// - Forward-method canonical: repo-config `events.produces[].schema`
|
|
5
|
+
// (role=publishes) and `events.consumes[].schema` (role=consumes).
|
|
6
|
+
// Each schema is resolved against the contracts.avro index → event id.
|
|
7
|
+
// - Reverse-method canonical: event-catalog.yaml `publisher_service_id`
|
|
8
|
+
// (role=publishes) and `consumers[]` (role=consumes).
|
|
9
|
+
// - Both paths emit; rows merge by (service, event, role) deduplication.
|
|
10
|
+
//
|
|
11
|
+
// `contracts.avro` is NOT a fallback for publishes — it's a build-time list
|
|
12
|
+
// (for code-gen / validation) that consumers commonly declare too. Only
|
|
13
|
+
// the events block + event-catalog.yaml are authoritative for edges.
|
|
14
|
+
//
|
|
15
|
+
// capability_event sources (per ADR 0160, schema 2.14.0):
|
|
16
|
+
// - Forward shape A — `traceability.events[{id, role}]` (legacy
|
|
17
|
+
// aspirational shape; unauthored across observed adopters, kept for
|
|
18
|
+
// compatibility). Emits with source=authored.
|
|
19
|
+
// - Forward shape B — `traceability.contracts.published_events[]` /
|
|
20
|
+
// `consumed_events[]` with `{event, status, notes}`. Slug the `event`
|
|
21
|
+
// field via the same normaliser the events walker uses; check against
|
|
22
|
+
// `knownEventIds`. Pre-lock sketches (events not yet in events.csv)
|
|
23
|
+
// fire `[finding] orphan`. Emits with source=authored.
|
|
24
|
+
// - Derived — `capability_service ⋈ service_event` join on service_id.
|
|
25
|
+
// Zero-authoring associative path; emits role from service_event.
|
|
26
|
+
// Per ADR 0160, the over-attribution fan-out is intrinsic to services
|
|
27
|
+
// that own multiple primary capabilities, so a role filter has no
|
|
28
|
+
// useful effect (verified on a real corpus: 144 → 144 with primary
|
|
29
|
+
// filter). The `source=derived` column distinguishes these from
|
|
30
|
+
// authored edges for governance queries.
|
|
31
|
+
// - Merge by PK `{capability_id}__{event_id}__{role}`: authored wins;
|
|
32
|
+
// conflicting authored sources fire `[finding] divergence`.
|
|
33
|
+
//
|
|
34
|
+
// feature_event sources (per ADR 0161, schema 2.15.0):
|
|
35
|
+
// - Derived only — `feature_service ⋈ service_event` join on service_id.
|
|
36
|
+
// Completes the capability/feature/service trio of materialised event
|
|
37
|
+
// edges. There is no authored shape; the authored signal lives at the
|
|
38
|
+
// service level (`service_event`) and the feature inherits it through
|
|
39
|
+
// `feature_service`. Every row carries `source=derived` for symmetry
|
|
40
|
+
// with `capability_event.source`.
|
|
41
|
+
// - PK is `{feature_id}__{event_id}__{role}` so a feature can carry both
|
|
42
|
+
// publishes + consumes for the same event without collision.
|
|
43
|
+
import { sep } from "node:path";
|
|
44
|
+
export function buildServiceEvent(inputs) {
|
|
45
|
+
const rows = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
// Interim guard (M-ADR 0197 stopgap, ahead of the Phase B attribution
|
|
48
|
+
// mechanism): this check pre-dates M-ADR 0197 and always dropped a
|
|
49
|
+
// non-resolving serviceId silently, on the assumption that knownServiceIds
|
|
50
|
+
// was always a superset of every repo id — true until a repo could declare
|
|
51
|
+
// a `services:` block that drops its own default id. The drop stays (a
|
|
52
|
+
// dangling FK would fail the whole build for what is an authoring
|
|
53
|
+
// situation), but it must never be silent again: any serviceId absent from
|
|
54
|
+
// knownServiceIds now names the loss instead of swallowing it. `context`
|
|
55
|
+
// points at the authoring source so the finding is actionable.
|
|
56
|
+
const emit = (serviceId, eventId, role, context) => {
|
|
57
|
+
if (!inputs.knownServiceIds.has(serviceId)) {
|
|
58
|
+
inputs.findings.add("orphan", `${context}: service_event ${role} edge for service "${serviceId}" dropped — "${serviceId}" does not resolve in services.csv. Check the id matches a repository or a services: block entry (M-ADR 0197); this can also mean the authoring source names something other than a catalogued service.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!inputs.knownEventIds.has(eventId)) {
|
|
62
|
+
// Same shape as the guard above, unrelated cause: a resolved schema
|
|
63
|
+
// path or YAML event id that doesn't land a row in events.csv. Not an
|
|
64
|
+
// M-ADR 0197 case — flagged separately in the same sweep that found
|
|
65
|
+
// the serviceId guard, fixed here rather than left as the one asymmetry
|
|
66
|
+
// inside a function just touched for the identical reason.
|
|
67
|
+
inputs.findings.add("orphan", `${context}: service_event ${role} edge for event "${eventId}" dropped — "${eventId}" does not resolve in events.csv`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const id = `${serviceId}__${eventId}__${role}`;
|
|
71
|
+
if (seen.has(id))
|
|
72
|
+
return;
|
|
73
|
+
seen.add(id);
|
|
74
|
+
rows.push({ id, service_id: serviceId, event_id: eventId, role });
|
|
75
|
+
};
|
|
76
|
+
// Forward-method path: repo-config events.produces/consumes blocks.
|
|
77
|
+
for (const cfg of inputs.configs) {
|
|
78
|
+
const events = cfg.data.events;
|
|
79
|
+
if (!events)
|
|
80
|
+
continue;
|
|
81
|
+
for (const entry of events.produces ?? []) {
|
|
82
|
+
if (!entry.schema)
|
|
83
|
+
continue;
|
|
84
|
+
const eid = resolveAvroPath(entry.schema, inputs.contractIdByPath);
|
|
85
|
+
if (eid !== undefined)
|
|
86
|
+
emit(cfg.repoId, eid, "publishes", cfg.relPath);
|
|
87
|
+
}
|
|
88
|
+
for (const entry of events.consumes ?? []) {
|
|
89
|
+
if (!entry.schema)
|
|
90
|
+
continue;
|
|
91
|
+
const eid = resolveAvroPath(entry.schema, inputs.contractIdByPath);
|
|
92
|
+
if (eid !== undefined)
|
|
93
|
+
emit(cfg.repoId, eid, "consumes", cfg.relPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Reverse-method path: event-catalog.yaml publisher + consumers[].
|
|
97
|
+
// YAML rows already validated publisher/consumers against knownServiceIds
|
|
98
|
+
// during events.ts merge, but emit() re-checks defensively.
|
|
99
|
+
for (const yaml of (inputs.yamlEvents?.values() ?? [])) {
|
|
100
|
+
if (yaml.publisher !== "") {
|
|
101
|
+
emit(yaml.publisher, yaml.id, "publishes", `event ${yaml.id}`);
|
|
102
|
+
}
|
|
103
|
+
for (const consumer of yaml.consumers) {
|
|
104
|
+
if (!inputs.knownServiceIds.has(consumer)) {
|
|
105
|
+
inputs.findings.add("orphan", `event ${yaml.id}: consumer "${consumer}" does not resolve to a known service — service_event edge omitted`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
emit(consumer, yaml.id, "consumes", `event ${yaml.id}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
export function buildCapabilityEvent(inputs) {
|
|
114
|
+
// Authored rows from shapes A + B accumulate first; derived rows are
|
|
115
|
+
// appended only when no authored row owns the same PK.
|
|
116
|
+
const authored = new Map();
|
|
117
|
+
// ---- Shape A: traceability.events[{id, role}] (legacy) -------------
|
|
118
|
+
for (const m of inputs.manifests) {
|
|
119
|
+
const capId = m.capability.id;
|
|
120
|
+
if (!inputs.knownCapabilityIds.has(capId))
|
|
121
|
+
continue;
|
|
122
|
+
const events = m.data
|
|
123
|
+
?.traceability?.events;
|
|
124
|
+
if (!Array.isArray(events))
|
|
125
|
+
continue;
|
|
126
|
+
for (const entry of events) {
|
|
127
|
+
if (!entry || typeof entry !== "object")
|
|
128
|
+
continue;
|
|
129
|
+
const o = entry;
|
|
130
|
+
const eid = String(o["id"] ?? "").trim();
|
|
131
|
+
const role = String(o["role"] ?? "").trim();
|
|
132
|
+
if (eid === "" || (role !== "publishes" && role !== "consumes"))
|
|
133
|
+
continue;
|
|
134
|
+
if (!inputs.knownEventIds.has(eid)) {
|
|
135
|
+
inputs.findings.add("orphan", `${m.relPath}: capability_event references "${eid}" but no such event exists`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
tryEmitAuthored(authored, capId, eid, role, m.relPath, inputs.findings);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---- Shape B: traceability.contracts.{published,consumed}_events[] -
|
|
142
|
+
// The observed forward-method shape. `event` is a name (often dotted);
|
|
143
|
+
// slug-normalise via the same convention the events walker uses before
|
|
144
|
+
// checking against knownEventIds. Pre-lock sketches that don't resolve
|
|
145
|
+
// are surfaced as `orphan` findings (not silent drops).
|
|
146
|
+
for (const m of inputs.manifests) {
|
|
147
|
+
const capId = m.capability.id;
|
|
148
|
+
if (!inputs.knownCapabilityIds.has(capId))
|
|
149
|
+
continue;
|
|
150
|
+
const contracts = m.data?.traceability?.contracts;
|
|
151
|
+
if (!contracts || typeof contracts !== "object")
|
|
152
|
+
continue;
|
|
153
|
+
const c = contracts;
|
|
154
|
+
for (const [key, role] of [
|
|
155
|
+
["published_events", "publishes"],
|
|
156
|
+
["consumed_events", "consumes"],
|
|
157
|
+
]) {
|
|
158
|
+
const items = c[key];
|
|
159
|
+
if (!Array.isArray(items))
|
|
160
|
+
continue;
|
|
161
|
+
for (const entry of items) {
|
|
162
|
+
if (!entry || typeof entry !== "object")
|
|
163
|
+
continue;
|
|
164
|
+
const o = entry;
|
|
165
|
+
const eventName = String(o["event"] ?? "").trim();
|
|
166
|
+
if (eventName === "")
|
|
167
|
+
continue;
|
|
168
|
+
const eid = slugifyEventName(eventName);
|
|
169
|
+
if (eid === "")
|
|
170
|
+
continue;
|
|
171
|
+
if (!inputs.knownEventIds.has(eid)) {
|
|
172
|
+
inputs.findings.add("orphan", `${m.relPath}: ${key} entry "${eventName}" doesn't resolve to a known event id "${eid}" (likely a pre-lock sketch — events.csv has no matching row)`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
tryEmitAuthored(authored, capId, eid, role, m.relPath, inputs.findings);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ---- Derived: capability_service ⋈ service_event --------------------
|
|
180
|
+
// Pure associative coupling. Per ADR 0160, no role filter — Phase Pre
|
|
181
|
+
// verified that role=primary filtering gives no reduction in practice
|
|
182
|
+
// (services that emit events legitimately own multiple primary caps).
|
|
183
|
+
// Provenance distinguishes derived from authored for governance queries.
|
|
184
|
+
const derived = [];
|
|
185
|
+
const derivedSeen = new Set();
|
|
186
|
+
if (inputs.capabilityServiceRows &&
|
|
187
|
+
inputs.serviceEventRows &&
|
|
188
|
+
inputs.serviceEventRows.length > 0) {
|
|
189
|
+
const capsByService = new Map();
|
|
190
|
+
for (const cs of inputs.capabilityServiceRows) {
|
|
191
|
+
const sid = String(cs["service_id"] ?? "");
|
|
192
|
+
const cid = String(cs["capability_id"] ?? "");
|
|
193
|
+
if (sid === "" || cid === "")
|
|
194
|
+
continue;
|
|
195
|
+
if (!inputs.knownCapabilityIds.has(cid))
|
|
196
|
+
continue;
|
|
197
|
+
let bucket = capsByService.get(sid);
|
|
198
|
+
if (bucket === undefined) {
|
|
199
|
+
bucket = new Set();
|
|
200
|
+
capsByService.set(sid, bucket);
|
|
201
|
+
}
|
|
202
|
+
bucket.add(cid);
|
|
203
|
+
}
|
|
204
|
+
for (const se of inputs.serviceEventRows) {
|
|
205
|
+
const sid = String(se["service_id"] ?? "");
|
|
206
|
+
const eid = String(se["event_id"] ?? "");
|
|
207
|
+
const role = String(se["role"] ?? "");
|
|
208
|
+
if (sid === "" || eid === "" || (role !== "publishes" && role !== "consumes"))
|
|
209
|
+
continue;
|
|
210
|
+
const caps = capsByService.get(sid);
|
|
211
|
+
if (caps === undefined)
|
|
212
|
+
continue;
|
|
213
|
+
for (const cid of caps) {
|
|
214
|
+
const pk = `${cid}__${eid}__${role}`;
|
|
215
|
+
if (authored.has(pk))
|
|
216
|
+
continue; // authored wins
|
|
217
|
+
if (derivedSeen.has(pk))
|
|
218
|
+
continue;
|
|
219
|
+
derivedSeen.add(pk);
|
|
220
|
+
derived.push({
|
|
221
|
+
id: pk,
|
|
222
|
+
capability_id: cid,
|
|
223
|
+
event_id: eid,
|
|
224
|
+
role,
|
|
225
|
+
source: "derived",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return [...authored.values(), ...derived];
|
|
231
|
+
}
|
|
232
|
+
export function buildFeatureEvent(inputs) {
|
|
233
|
+
// Derived-only edge per ADR 0161. Mirrors capability_event's derived path
|
|
234
|
+
// one rung lower in the hierarchy. PK uses three components so the same
|
|
235
|
+
// feature can hold both publishes + consumes rows for the same event.
|
|
236
|
+
const rows = [];
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
if (inputs.serviceEventRows.length === 0)
|
|
239
|
+
return rows;
|
|
240
|
+
// Index: service_id -> features served by that service.
|
|
241
|
+
const featuresByService = new Map();
|
|
242
|
+
for (const fs of inputs.featureServiceRows) {
|
|
243
|
+
const sid = String(fs["service_id"] ?? "");
|
|
244
|
+
const fid = String(fs["feature_id"] ?? "");
|
|
245
|
+
if (sid === "" || fid === "")
|
|
246
|
+
continue;
|
|
247
|
+
if (!inputs.knownFeatureIds.has(fid))
|
|
248
|
+
continue;
|
|
249
|
+
let bucket = featuresByService.get(sid);
|
|
250
|
+
if (bucket === undefined) {
|
|
251
|
+
bucket = new Set();
|
|
252
|
+
featuresByService.set(sid, bucket);
|
|
253
|
+
}
|
|
254
|
+
bucket.add(fid);
|
|
255
|
+
}
|
|
256
|
+
for (const se of inputs.serviceEventRows) {
|
|
257
|
+
const sid = String(se["service_id"] ?? "");
|
|
258
|
+
const eid = String(se["event_id"] ?? "");
|
|
259
|
+
const role = String(se["role"] ?? "");
|
|
260
|
+
if (sid === "" || eid === "" || (role !== "publishes" && role !== "consumes"))
|
|
261
|
+
continue;
|
|
262
|
+
if (!inputs.knownEventIds.has(eid))
|
|
263
|
+
continue;
|
|
264
|
+
const features = featuresByService.get(sid);
|
|
265
|
+
if (features === undefined)
|
|
266
|
+
continue;
|
|
267
|
+
for (const fid of features) {
|
|
268
|
+
const pk = `${fid}__${eid}__${role}`;
|
|
269
|
+
if (seen.has(pk))
|
|
270
|
+
continue;
|
|
271
|
+
seen.add(pk);
|
|
272
|
+
rows.push({
|
|
273
|
+
id: pk,
|
|
274
|
+
feature_id: fid,
|
|
275
|
+
event_id: eid,
|
|
276
|
+
role,
|
|
277
|
+
source: "derived",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return rows;
|
|
282
|
+
}
|
|
283
|
+
function tryEmitAuthored(authored, capId, eid, role, relPath, findings) {
|
|
284
|
+
const pk = `${capId}__${eid}__${role}`;
|
|
285
|
+
if (authored.has(pk)) {
|
|
286
|
+
// Already authored from another source/shape. We don't compare
|
|
287
|
+
// attributes here (the only fields are PK components); silent dedup
|
|
288
|
+
// is the right behaviour. Cross-shape divergence at the field level
|
|
289
|
+
// would only matter if shapes carried attributes beyond the PK.
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
authored.set(pk, {
|
|
293
|
+
id: pk,
|
|
294
|
+
capability_id: capId,
|
|
295
|
+
event_id: eid,
|
|
296
|
+
role,
|
|
297
|
+
source: "authored",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function slugifyEventName(s) {
|
|
301
|
+
return s
|
|
302
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
303
|
+
.toLowerCase()
|
|
304
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
305
|
+
.replace(/^-+|-+$/g, "");
|
|
306
|
+
}
|
|
307
|
+
function resolveAvroPath(schema, index) {
|
|
308
|
+
const cleaned = schema.replace(/^\/+/, "");
|
|
309
|
+
const full = `contracts/avro/${cleaned}`;
|
|
310
|
+
if (index.has(full))
|
|
311
|
+
return index.get(full);
|
|
312
|
+
if (index.has(cleaned))
|
|
313
|
+
return index.get(cleaned);
|
|
314
|
+
for (const [path, id] of index) {
|
|
315
|
+
const parts = path.split(sep);
|
|
316
|
+
if (parts.length >= 2 && parts.slice(-2).join("/") === cleaned)
|
|
317
|
+
return id;
|
|
318
|
+
}
|
|
319
|
+
const base = cleaned.split("/").slice(-1)[0] ?? cleaned;
|
|
320
|
+
for (const [path, id] of index) {
|
|
321
|
+
const parts = path.split(sep);
|
|
322
|
+
if ((parts[parts.length - 1] ?? "") === base)
|
|
323
|
+
return id;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=event-edges.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Events discoverer — emits `events.csv` from two canonical sources per
|
|
2
|
+
// ADR 0156 (multi-source ingestion):
|
|
3
|
+
//
|
|
4
|
+
// Forward-method canonical: `contracts/avro/*.avsc` files (existing path).
|
|
5
|
+
// The avsc id becomes the event id; publisher_service_id reads from
|
|
6
|
+
// `repo-config.events.produces[].schema` resolved against the avsc index.
|
|
7
|
+
// Reverse-method canonical: `docs/domains/<domain>/contracts/events/
|
|
8
|
+
// event-catalog.yaml` (new path, walked by event-catalog-walker.ts).
|
|
9
|
+
// One file per domain; each entry carries name, publisher_service_id,
|
|
10
|
+
// transport, schema_path, version, consumers[].
|
|
11
|
+
//
|
|
12
|
+
// Merge precedence when both sources declare an event (matched by id slug):
|
|
13
|
+
// - name, domain_id, publisher_service_id, transport → YAML wins
|
|
14
|
+
// - schema_path, version → YAML wins when set; avsc augments when null
|
|
15
|
+
// - Events declared only in avsc → forward-only row (existing behaviour)
|
|
16
|
+
// - Events declared only in YAML → reverse-only row (new)
|
|
17
|
+
//
|
|
18
|
+
// Transport: open string per ADR 0156 (no hardcoded default). When the
|
|
19
|
+
// source is avsc-only with no `repo-config.events` declaration, transport
|
|
20
|
+
// may be empty.
|
|
21
|
+
import { sep } from "node:path";
|
|
22
|
+
import { mergeYamlAndAvsc, walkEventCatalogs, } from "./event-catalog-walker.js";
|
|
23
|
+
export async function buildEvents(inputs) {
|
|
24
|
+
// 1. Walk the reverse-method canonical source (event-catalog.yaml files).
|
|
25
|
+
const yamlResult = await walkEventCatalogs({
|
|
26
|
+
sddPath: inputs.sddPath,
|
|
27
|
+
findings: inputs.findings,
|
|
28
|
+
});
|
|
29
|
+
// 2. Build the forward-method publisher index from repo-config events.produces.
|
|
30
|
+
// Earlier-encountered service wins on ties (deterministic per sorted
|
|
31
|
+
// config order from the repositories builder).
|
|
32
|
+
const publisherByContractId = new Map();
|
|
33
|
+
for (const cfg of inputs.configs) {
|
|
34
|
+
if (!inputs.knownServiceIds.has(cfg.repoId))
|
|
35
|
+
continue;
|
|
36
|
+
const events = cfg.data.events;
|
|
37
|
+
const produces = events?.produces ?? [];
|
|
38
|
+
for (const entry of produces) {
|
|
39
|
+
const schema = entry.schema;
|
|
40
|
+
if (!schema)
|
|
41
|
+
continue;
|
|
42
|
+
const id = resolveContractByPath(schema, "avro", inputs.contractIdByPath);
|
|
43
|
+
if (id === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
if (!publisherByContractId.has(id)) {
|
|
46
|
+
publisherByContractId.set(id, cfg.repoId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 3. Build avsc-derived event rows (forward path), indexed by id for merge.
|
|
51
|
+
const avscRows = new Map();
|
|
52
|
+
for (const c of inputs.avroContracts) {
|
|
53
|
+
const id = String(c["id"]);
|
|
54
|
+
const specPath = String(c["spec_path"] ?? "");
|
|
55
|
+
const rawDomain = inferDomainFromAvroPath(specPath);
|
|
56
|
+
let domain = rawDomain;
|
|
57
|
+
if (rawDomain.startsWith("_")) {
|
|
58
|
+
// `_common`, `_shared`, etc. — cross-domain envelope schemas.
|
|
59
|
+
domain = "";
|
|
60
|
+
}
|
|
61
|
+
else if (rawDomain !== "" && !inputs.knownDomainIds.has(rawDomain)) {
|
|
62
|
+
inputs.findings.add("orphan", `event ${id}: schema is at ${specPath} but no docs/domains/${rawDomain}/ exists — emitting with empty domain_id`);
|
|
63
|
+
domain = "";
|
|
64
|
+
}
|
|
65
|
+
avscRows.set(id, {
|
|
66
|
+
id,
|
|
67
|
+
name: String(c["name"] ?? id),
|
|
68
|
+
publisher_service_id: publisherByContractId.get(id) ?? "",
|
|
69
|
+
domain_id: domain,
|
|
70
|
+
transport: "",
|
|
71
|
+
schema_path: specPath,
|
|
72
|
+
version: String(c["version"] ?? ""),
|
|
73
|
+
// avsc has no evidence slot — avsc-only events stay empty (ADR 0175).
|
|
74
|
+
evidence_class: "",
|
|
75
|
+
source_sdd_id: "",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// 4. Merge YAML + avsc. YAML rows win on overlapping fields.
|
|
79
|
+
const merged = new Map();
|
|
80
|
+
// An event declared in the catalogue YAML and present as an .avsc is ONE event, whatever
|
|
81
|
+
// the two sources call it. Matching by id alone missed every pair whose ids differ (the
|
|
82
|
+
// avsc-derived id carries the domain prefix and an `-event` suffix), so one schema
|
|
83
|
+
// yielded two event rows — inflating an estate's count by 12. The schema path is the
|
|
84
|
+
// stronger key: the YAML's id wins and the avsc row it describes is consumed.
|
|
85
|
+
const avscIdBySchema = new Map();
|
|
86
|
+
for (const [aid, row] of avscRows) {
|
|
87
|
+
const sp = normaliseSchemaPath(String(row["schema_path"] ?? ""));
|
|
88
|
+
if (sp !== "")
|
|
89
|
+
avscIdBySchema.set(sp, aid);
|
|
90
|
+
}
|
|
91
|
+
const consumedAvsc = new Set();
|
|
92
|
+
for (const [id, yaml] of yamlResult.byId) {
|
|
93
|
+
let avsc = avscRows.get(id);
|
|
94
|
+
if (avsc !== undefined) {
|
|
95
|
+
consumedAvsc.add(id);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const aid = avscIdBySchema.get(normaliseSchemaPath(yaml.schemaPath));
|
|
99
|
+
if (aid !== undefined && !yamlResult.byId.has(aid)) {
|
|
100
|
+
avsc = avscRows.get(aid);
|
|
101
|
+
consumedAvsc.add(aid);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const baseRow = mergeYamlAndAvsc(yaml, avsc);
|
|
105
|
+
// Resolve domain_id against known domains; empty if unknown.
|
|
106
|
+
let domainId = String(baseRow["domain_id"] ?? "");
|
|
107
|
+
if (domainId !== "" && !inputs.knownDomainIds.has(domainId)) {
|
|
108
|
+
inputs.findings.add("orphan", `event ${id}: event-catalog.yaml declares domain "${domainId}" but no docs/domains/${domainId}/ exists — emitting with empty domain_id`);
|
|
109
|
+
domainId = "";
|
|
110
|
+
}
|
|
111
|
+
// Resolve publisher against known services.
|
|
112
|
+
let publisher = String(baseRow["publisher_service_id"] ?? "");
|
|
113
|
+
if (publisher !== "" && !inputs.knownServiceIds.has(publisher)) {
|
|
114
|
+
inputs.findings.add("orphan", `event ${id}: publisher_service_id "${publisher}" does not resolve to a known service — emitting with empty publisher_service_id`);
|
|
115
|
+
publisher = "";
|
|
116
|
+
}
|
|
117
|
+
merged.set(id, {
|
|
118
|
+
...baseRow,
|
|
119
|
+
domain_id: domainId,
|
|
120
|
+
publisher_service_id: publisher,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Avsc-only events (no YAML row): keep the existing avsc-derived row as-is.
|
|
124
|
+
for (const [id, row] of avscRows) {
|
|
125
|
+
if (!merged.has(id) && !consumedAvsc.has(id)) {
|
|
126
|
+
merged.set(id, row);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 5. Deterministic order: sort by id.
|
|
130
|
+
const rows = [...merged.values()].sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
131
|
+
return {
|
|
132
|
+
rows,
|
|
133
|
+
publisherIndex: { publisherByContractId },
|
|
134
|
+
yamlEvents: yamlResult.byId,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function normaliseSchemaPath(p) {
|
|
138
|
+
return p.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
139
|
+
}
|
|
140
|
+
function inferDomainFromAvroPath(relPath) {
|
|
141
|
+
// Layout: contracts/avro/<domain>/<file>.avsc
|
|
142
|
+
const parts = relPath.split(sep);
|
|
143
|
+
if (parts.length < 4)
|
|
144
|
+
return "";
|
|
145
|
+
return parts[2] ?? "";
|
|
146
|
+
}
|
|
147
|
+
function resolveContractByPath(schema, kind, index) {
|
|
148
|
+
const cleaned = schema.replace(/^\/+/, "");
|
|
149
|
+
const fullCandidate = `contracts/${kind}/${cleaned}`;
|
|
150
|
+
if (index.has(fullCandidate))
|
|
151
|
+
return index.get(fullCandidate);
|
|
152
|
+
if (index.has(cleaned))
|
|
153
|
+
return index.get(cleaned);
|
|
154
|
+
for (const [path, id] of index) {
|
|
155
|
+
const parts = path.split(sep);
|
|
156
|
+
const last2 = parts.length >= 2 ? parts.slice(-2).join("/") : "";
|
|
157
|
+
if (last2 === cleaned)
|
|
158
|
+
return id;
|
|
159
|
+
}
|
|
160
|
+
const base = cleaned.split("/").slice(-1)[0] ?? cleaned;
|
|
161
|
+
for (const [path, id] of index) {
|
|
162
|
+
const parts = path.split(sep);
|
|
163
|
+
if ((parts[parts.length - 1] ?? "") === base)
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=events.js.map
|