@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,181 @@
|
|
|
1
|
+
// Integrations discoverer — emits `integrations.csv` rows from an
|
|
2
|
+
// integration-product SDD's `integration-manifest.yaml`.
|
|
3
|
+
//
|
|
4
|
+
// Per ADR 0134 federation: each integration-provider SDD owns one row
|
|
5
|
+
// in the granular integrations.csv. The aggregator merges siblings.
|
|
6
|
+
//
|
|
7
|
+
// Per ADR 0144 (schema 2.6.0): if the manifest declares an optional
|
|
8
|
+
// `sub_integrations:` array (multi-provider monolith pattern), emit
|
|
9
|
+
// one additional row per entry with parent_product_id set to the
|
|
10
|
+
// SDD's integrationId. The umbrella SDD row stays unchanged.
|
|
11
|
+
//
|
|
12
|
+
// Profile gating: only integration-product profile SDDs emit. Other
|
|
13
|
+
// profiles are no-ops.
|
|
14
|
+
//
|
|
15
|
+
// Umbrella row:
|
|
16
|
+
// id = integrationId from the manifest (or sdd_id as fallback).
|
|
17
|
+
// name = providerName (or title-cased id).
|
|
18
|
+
// category = profileType (open enum per schema description; ecosystems
|
|
19
|
+
// extend with values like saas-platform, oem, device, ...).
|
|
20
|
+
// parent_product_id = parentIntegrationProductId (typically empty).
|
|
21
|
+
// doc_path = "." (SDD root).
|
|
22
|
+
// repo_count = supplied by the orchestrator from discovered repos.
|
|
23
|
+
//
|
|
24
|
+
// Sub-integration rows (one per entry in sub_integrations:):
|
|
25
|
+
// id = entry.id (required; unique within SDD)
|
|
26
|
+
// name = entry.providerName (required)
|
|
27
|
+
// category = entry.category (required)
|
|
28
|
+
// parent_product_id = the SDD's integrationId (internal FK)
|
|
29
|
+
// doc_path = SDD's doc_path
|
|
30
|
+
// repo_count = 0 (repo ownership stays with the umbrella)
|
|
31
|
+
//
|
|
32
|
+
// domain_id is always empty per ADR 0136.
|
|
33
|
+
import { readFile } from "node:fs/promises";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { parse as parseYaml } from "yaml";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
const REL_PATH = "integration-manifest.yaml";
|
|
38
|
+
const SubIntegrationSchema = z.object({
|
|
39
|
+
id: z.string().optional(),
|
|
40
|
+
providerName: z.string().optional(),
|
|
41
|
+
category: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
const ManifestSchema = z.object({
|
|
44
|
+
integrationId: z.string().optional(),
|
|
45
|
+
providerName: z.string().optional(),
|
|
46
|
+
profileType: z.string().optional(),
|
|
47
|
+
ingestionPattern: z.string().optional(),
|
|
48
|
+
invocationPattern: z.string().optional(),
|
|
49
|
+
patternQualifier: z.string().optional(),
|
|
50
|
+
parentIntegrationProductId: z.string().optional(),
|
|
51
|
+
sub_integrations: z.array(SubIntegrationSchema).optional(),
|
|
52
|
+
});
|
|
53
|
+
export async function buildIntegrations(inputs) {
|
|
54
|
+
if (inputs.profile !== "integration-product")
|
|
55
|
+
return [];
|
|
56
|
+
const path = join(inputs.sddPath, REL_PATH);
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = await readFile(path, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code !== "ENOENT")
|
|
63
|
+
throw err;
|
|
64
|
+
inputs.findings.add("coverage", `${REL_PATH} not found — emitting empty integrations.csv (integration-product SDD without a manifest)`);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = parseYaml(text);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
inputs.findings.add("convention", `${REL_PATH}: failed to parse YAML — ${err.message}`);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const parsed = ManifestSchema.safeParse(raw);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
inputs.findings.add("convention", `${REL_PATH}: shape violation — ${parsed.error.issues.map((i) => i.path.join(".") + ": " + i.message).join("; ")}`);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const id = (parsed.data.integrationId ?? "").trim() || inputs.sddId;
|
|
81
|
+
const name = (parsed.data.providerName ?? "").trim() || titleCase(id);
|
|
82
|
+
const category = (parsed.data.profileType ?? "").trim();
|
|
83
|
+
const parentProductId = (parsed.data.parentIntegrationProductId ?? "").trim();
|
|
84
|
+
if (category === "") {
|
|
85
|
+
inputs.findings.add("convention", `${REL_PATH}: missing required \`profileType\` (used as integrations.category)`);
|
|
86
|
+
}
|
|
87
|
+
// Reference-exemplar trait (catalogue 2.46.0), derived from the SDD manifest —
|
|
88
|
+
// see the note on `IntegrationsBuildInputs.exemplar`. Empty is the unmarked
|
|
89
|
+
// norm; no finding on absence.
|
|
90
|
+
const exemplar = inputs.exemplar === true ? "true" : "";
|
|
91
|
+
const sharedLibrary = inputs.sharedLibrary === true ? "true" : "";
|
|
92
|
+
const rows = [
|
|
93
|
+
{
|
|
94
|
+
id,
|
|
95
|
+
name,
|
|
96
|
+
domain_id: "",
|
|
97
|
+
parent_product_id: parentProductId,
|
|
98
|
+
category,
|
|
99
|
+
doc_path: ".",
|
|
100
|
+
repo_count: inputs.repoCount,
|
|
101
|
+
// Interaction-pattern axis (M-ADR 0171 §4.2/§4.3), catalogued from 2.45.0.
|
|
102
|
+
// Authorable and gate-validated since the axis landed, but catalogued
|
|
103
|
+
// nowhere until now — so a declared pattern was invisible to rollups.
|
|
104
|
+
// SCALAR: §10.5 withdrew §9.3's multi-valued allowance, which is what lets
|
|
105
|
+
// these be plain columns rather than a join table.
|
|
106
|
+
ingestion_pattern: (parsed.data.ingestionPattern ?? "").trim(),
|
|
107
|
+
invocation_pattern: (parsed.data.invocationPattern ?? "").trim(),
|
|
108
|
+
pattern_qualifier: (parsed.data.patternQualifier ?? "").trim(),
|
|
109
|
+
exemplar,
|
|
110
|
+
shared_library: sharedLibrary,
|
|
111
|
+
source_sdd_id: "",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
// Per ADR 0144 (schema 2.6.0): emit one row per sub_integration with
|
|
115
|
+
// parent_product_id = the umbrella SDD's id.
|
|
116
|
+
const subs = parsed.data.sub_integrations ?? [];
|
|
117
|
+
const seenSubIds = new Set([id]); // umbrella id is reserved
|
|
118
|
+
for (let i = 0; i < subs.length; i++) {
|
|
119
|
+
const sub = subs[i];
|
|
120
|
+
const subId = (sub.id ?? "").trim();
|
|
121
|
+
const subName = (sub.providerName ?? "").trim();
|
|
122
|
+
const subCategory = (sub.category ?? "").trim();
|
|
123
|
+
const missing = [];
|
|
124
|
+
if (subId === "")
|
|
125
|
+
missing.push("id");
|
|
126
|
+
if (subName === "")
|
|
127
|
+
missing.push("providerName");
|
|
128
|
+
if (subCategory === "")
|
|
129
|
+
missing.push("category");
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
inputs.findings.add("convention", `${REL_PATH}: sub_integrations[${i}] missing required field(s): ${missing.join(", ")} — entry skipped`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (seenSubIds.has(subId)) {
|
|
135
|
+
inputs.findings.add("convention", `${REL_PATH}: sub_integrations[${i}] id "${subId}" duplicates an existing id within the SDD — entry skipped (first occurrence wins)`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
seenSubIds.add(subId);
|
|
139
|
+
rows.push({
|
|
140
|
+
id: subId,
|
|
141
|
+
name: subName,
|
|
142
|
+
domain_id: "",
|
|
143
|
+
parent_product_id: id,
|
|
144
|
+
category: subCategory,
|
|
145
|
+
doc_path: ".",
|
|
146
|
+
repo_count: 0,
|
|
147
|
+
// Deliberately empty on a sub_integrations row: the patterns are declared
|
|
148
|
+
// once at SDD level, and copying the umbrella's onto each sub-row would
|
|
149
|
+
// assert a per-provider mechanism the manifest never stated. Per-component
|
|
150
|
+
// patterns live in `integration_component`.
|
|
151
|
+
ingestion_pattern: "",
|
|
152
|
+
invocation_pattern: "",
|
|
153
|
+
pattern_qualifier: "",
|
|
154
|
+
// INHERITED, and deliberately the opposite treatment to the three pattern
|
|
155
|
+
// columns directly above. Do not "fix" this to match them. A pattern is a
|
|
156
|
+
// MECHANISM — how this integration acquires data — which is genuinely one
|
|
157
|
+
// per SDD. `exemplar` is a REALITY STATUS, and it propagates: if the SDD is
|
|
158
|
+
// a demonstration then every provider relationship it declares is a
|
|
159
|
+
// demonstration. For a multi-provider monolith the sub rows ARE the
|
|
160
|
+
// integrations a consumer counts (the umbrella row is the SDD container), so
|
|
161
|
+
// leaving them empty would flag the container while counting its
|
|
162
|
+
// demonstration providers as real — the exact rollup overcount this column
|
|
163
|
+
// exists to fix, reproduced one level down.
|
|
164
|
+
// Accepted trade: a real SDD with one demonstration sub-provider cannot be
|
|
165
|
+
// expressed. That state should not exist — it is a modelling error, not a
|
|
166
|
+
// legitimate shape — so per-entry declaration would buy expressiveness
|
|
167
|
+
// nobody needs.
|
|
168
|
+
exemplar,
|
|
169
|
+
shared_library: sharedLibrary,
|
|
170
|
+
source_sdd_id: "",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
function titleCase(slug) {
|
|
176
|
+
return slug
|
|
177
|
+
.split("-")
|
|
178
|
+
.map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
179
|
+
.join(" ");
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=integrations.js.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// persona_c4system edge builder — emits one row per `Person(...)` actor on
|
|
2
|
+
// the L1 context diagram that maps to a known persona (per ADR 0155). This
|
|
3
|
+
// captures the C4 "Person uses System" relationship; the C4 Person
|
|
4
|
+
// abstraction reuses the existing Persona entity rather than a parallel one.
|
|
5
|
+
//
|
|
6
|
+
// Resolution: `Person(id, "name", "desc")` actors are matched to personas
|
|
7
|
+
// rows by name (case-insensitive) then by name-slug, using the maps the
|
|
8
|
+
// personas builder produces. Unmatched actors log a `coverage` finding and
|
|
9
|
+
// are skipped (no dangling FK). The optional interaction label comes from a
|
|
10
|
+
// `Rel(person, system, "label")` line.
|
|
11
|
+
//
|
|
12
|
+
// `Person_Ext(...)` is discriminated by EVIDENCE, not notation (F10; F9's
|
|
13
|
+
// first fix keyed on notation and broke the external-human case): the
|
|
14
|
+
// notation is idiomatic both for a non-human external actor (a device) and
|
|
15
|
+
// for a human legitimately outside the boundary. Whether a persona row
|
|
16
|
+
// resolves is the declaration of humanity — a Person_Ext that resolves
|
|
17
|
+
// emits its edge; one that doesn't is silent (a device is not an authoring
|
|
18
|
+
// gap, and a typo'd human still surfaces through F8's reverse finding on
|
|
19
|
+
// the edgeless persona row).
|
|
20
|
+
//
|
|
21
|
+
// `Person(...)` asserts a human, so an unresolved one stays a forward
|
|
22
|
+
// coverage finding.
|
|
23
|
+
//
|
|
24
|
+
// Both directions report (F8, M-ADR 0157 parity + M-ADR 0133 §4.2c
|
|
25
|
+
// no-silent-drop): a Person with no persona row is a coverage finding
|
|
26
|
+
// (forward), and a persona with no edge is a coverage finding stating WHY —
|
|
27
|
+
// no system row, no identity-sourcing L1, no Person actors, or no actor
|
|
28
|
+
// naming it (reverse). Every persona ends the build with an edge or a
|
|
29
|
+
// finding.
|
|
30
|
+
import { readFile } from "node:fs/promises";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { resolveIdentityL1 } from "./c4-l1-candidates.js";
|
|
33
|
+
// `Person(id, "name", "desc")` / `Person_Ext(id, "name", "desc")` — the
|
|
34
|
+
// _Ext capture drives the evidence discriminator (F10; see header).
|
|
35
|
+
const PERSON_RE = /Person(_Ext)?\s*\(\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*\)/;
|
|
36
|
+
const REL_RE = /^(?:Bi)?Rel(?:_[A-Za-z]+)?\s*\(\s*(\w+)\s*,\s*(\w+)\s*(?:,\s*"([^"]*)")?/;
|
|
37
|
+
export async function buildPersonaC4System(inputs) {
|
|
38
|
+
const allPersonaIds = new Set([
|
|
39
|
+
...inputs.personaIdByName.values(),
|
|
40
|
+
...inputs.personaIdBySlug.values(),
|
|
41
|
+
]);
|
|
42
|
+
const none = new Set();
|
|
43
|
+
const systemId = inputs.manifest.c4SystemId ?? `${inputs.manifest.sddId}-system`;
|
|
44
|
+
if (!inputs.knownSystemIds.has(systemId)) {
|
|
45
|
+
reportEdgeless(allPersonaIds, none, "the SDD emits no c4_systems row", inputs.findings);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const content = await readL1(inputs.sddPath, inputs.manifest.category === "platform-root");
|
|
49
|
+
if (content === "") {
|
|
50
|
+
reportEdgeless(allPersonaIds, none, "no identity-sourcing L1 diagram (M-ADR 0189 §4.5)", inputs.findings);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const persons = parsePersons(content);
|
|
54
|
+
if (persons.length === 0) {
|
|
55
|
+
reportEdgeless(allPersonaIds, none, "the L1 declares no Person actors", inputs.findings);
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const interactionByEntryId = relInteractionsTo(content, systemId);
|
|
59
|
+
const rows = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const linked = new Set();
|
|
62
|
+
for (const person of persons) {
|
|
63
|
+
const personaId = inputs.personaIdByName.get(person.name.toLowerCase()) ??
|
|
64
|
+
inputs.personaIdBySlug.get(slugify(person.name));
|
|
65
|
+
if (personaId === undefined) {
|
|
66
|
+
// Person_Ext with no persona row: a device or other non-human external
|
|
67
|
+
// actor — silence is correct, and a typo'd human surfaces through the
|
|
68
|
+
// reverse finding on its edgeless persona row (F10).
|
|
69
|
+
if (person.external)
|
|
70
|
+
continue;
|
|
71
|
+
inputs.findings.add("coverage", `persona_c4system: Person "${person.name}" on the L1 diagram has no matching persona row — edge skipped`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const id = `${personaId}__${systemId}`;
|
|
75
|
+
linked.add(personaId);
|
|
76
|
+
if (seen.has(id))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(id);
|
|
79
|
+
rows.push({
|
|
80
|
+
id,
|
|
81
|
+
persona_id: personaId,
|
|
82
|
+
c4_system_id: systemId,
|
|
83
|
+
interaction: interactionByEntryId.get(person.entryId) ?? "",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
reportEdgeless(allPersonaIds, linked, "no Person actor on the L1 names it", inputs.findings);
|
|
87
|
+
rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
88
|
+
return rows;
|
|
89
|
+
}
|
|
90
|
+
// F8 — the reverse direction (M-ADR 0157 parity): every persona that ends
|
|
91
|
+
// the build without an edge is reported, with the reason. A persona with
|
|
92
|
+
// neither an edge nor a finding is indistinguishable from one that was
|
|
93
|
+
// never considered.
|
|
94
|
+
function reportEdgeless(allPersonaIds, linked, reason, findings) {
|
|
95
|
+
const edgeless = [...allPersonaIds]
|
|
96
|
+
.filter((id) => !linked.has(id))
|
|
97
|
+
.sort((a, b) => a.localeCompare(b));
|
|
98
|
+
for (const id of edgeless) {
|
|
99
|
+
findings.add("coverage", `persona_c4system: persona "${id}" has no c4system edge — ${reason}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function parsePersons(content) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
105
|
+
const m = PERSON_RE.exec(raw.trim());
|
|
106
|
+
if (m) {
|
|
107
|
+
out.push({
|
|
108
|
+
entryId: m[2] ?? "",
|
|
109
|
+
name: m[3] ?? "",
|
|
110
|
+
external: m[1] === "_Ext",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
// Map a person entry-id → the label of a Rel connecting it to the system.
|
|
117
|
+
function relInteractionsTo(content, systemId) {
|
|
118
|
+
const out = new Map();
|
|
119
|
+
// The system's diagram entry-id is unknown here, so we accept any Rel
|
|
120
|
+
// whose `from` is a person and keep its label; the common L1 shape is
|
|
121
|
+
// `Rel(personEntryId, appSystem, "Uses", ...)`.
|
|
122
|
+
void systemId;
|
|
123
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
124
|
+
const m = REL_RE.exec(raw.trim());
|
|
125
|
+
if (m && m[3]) {
|
|
126
|
+
const from = m[1] ?? "";
|
|
127
|
+
if (!out.has(from))
|
|
128
|
+
out.set(from, m[3]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
// Identity-sourcing L1 only (M-ADR 0189 §4.5): a sibling with no own L1
|
|
134
|
+
// contributes no persona edges rather than matching the platform's personas
|
|
135
|
+
// off the syndicated copy.
|
|
136
|
+
async function readL1(sddPath, isPlatformRoot) {
|
|
137
|
+
const resolved = await resolveIdentityL1(sddPath, isPlatformRoot);
|
|
138
|
+
if (resolved.path === "")
|
|
139
|
+
return "";
|
|
140
|
+
return await readFile(join(sddPath, resolved.path), "utf-8");
|
|
141
|
+
}
|
|
142
|
+
function slugify(s) {
|
|
143
|
+
return s
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/\s*\([^)]+\)/g, "")
|
|
146
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
147
|
+
.replace(/^-+|-+$/g, "");
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=persona-c4system.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// persona_container edge builder — M-ADR 0189 F26 §3 (schema 2.34.0).
|
|
2
|
+
//
|
|
3
|
+
// The L2-granularity companion to persona_c4system: a Rel(...) between a
|
|
4
|
+
// Person and a container had no catalogue and, until F26, no finding —
|
|
5
|
+
// eleven of one estate's L2s and the bulk of another's 81 unroutable Rels
|
|
6
|
+
// were exactly this class. F26 made the silence loud; this gives the
|
|
7
|
+
// loudness a destination.
|
|
8
|
+
//
|
|
9
|
+
// Person entries are file-local (a Person declared in the same diagram as
|
|
10
|
+
// the Rel); containers resolve SDD-wide (F20's rule). Either Rel direction
|
|
11
|
+
// normalises to persona→container. A Person that matches no persona row is
|
|
12
|
+
// a coverage finding, never a silent skip (the persona_c4system precedent).
|
|
13
|
+
// The syndicated platform L1 is skipped for non-root SDDs (F14); parallel
|
|
14
|
+
// labels concatenate loudly (F25).
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { collectC4DiagramFiles } from "./c4-external-systems.js";
|
|
17
|
+
import { mergeEdgeLabel } from "./edge-label-merge.js";
|
|
18
|
+
const REL_RE = /^(?:Bi)?Rel(?:_[A-Za-z]+)?\s*\(\s*(\w+)\s*,\s*(\w+)\s*(?:,\s*"([^"]*)")?/;
|
|
19
|
+
const PERSON_DECL_RE = /Person(?:_Ext)?\s*\(\s*(\w+)\s*,\s*"([^"]+)"/;
|
|
20
|
+
export async function buildPersonaContainer(inputs) {
|
|
21
|
+
if (inputs.containerIdByEntryId.size === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const isRoot = inputs.manifest.category === "platform-root";
|
|
24
|
+
const files = await collectC4DiagramFiles(inputs.sddPath);
|
|
25
|
+
const rows = [];
|
|
26
|
+
const rowsById = new Map();
|
|
27
|
+
for (const { abs, syndicated } of files) {
|
|
28
|
+
if (syndicated && !isRoot)
|
|
29
|
+
continue;
|
|
30
|
+
const content = await readFile(abs, "utf-8");
|
|
31
|
+
const personNameByEntry = new Map();
|
|
32
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
33
|
+
const pm = PERSON_DECL_RE.exec(raw.trim());
|
|
34
|
+
if (pm && !personNameByEntry.has(pm[1] ?? "")) {
|
|
35
|
+
personNameByEntry.set(pm[1] ?? "", pm[2] ?? "");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (personNameByEntry.size === 0)
|
|
39
|
+
continue;
|
|
40
|
+
// F31: only the SDD's OWN system entry is persona_c4system's domain —
|
|
41
|
+
// that catalogue emits one edge per persona to THE system, nothing
|
|
42
|
+
// else. The 0.89.0 skip silenced person Rels to ANY System(...) entry,
|
|
43
|
+
// a silence delegated to a catalogue that never held those edges
|
|
44
|
+
// (F5's shape, one tier over): four of an estate's best-authored
|
|
45
|
+
// persona edges vanished, while the same file's person→external edge
|
|
46
|
+
// was announced. Own entry = the first System(...) line, mirroring
|
|
47
|
+
// c4-systems' parseSystemLine; every other target takes exits 1/3/4.
|
|
48
|
+
let ownSystemEntry = "";
|
|
49
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
50
|
+
const sm = /^System\s*\(\s*(\w+)\s*,/.exec(raw.trim());
|
|
51
|
+
if (sm) {
|
|
52
|
+
ownSystemEntry = sm[1] ?? "";
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
57
|
+
const m = REL_RE.exec(raw.trim());
|
|
58
|
+
if (!m)
|
|
59
|
+
continue;
|
|
60
|
+
const [, fromEntry, toEntry, label] = m;
|
|
61
|
+
// F29/F30 — the person tier's exits, designed once. Identify the
|
|
62
|
+
// person endpoint (file-local; container resolution wins on a
|
|
63
|
+
// dual-use id per F20), then route by the OTHER endpoint's kind:
|
|
64
|
+
// local container → persona_container row
|
|
65
|
+
// own System(...) → persona_c4system's domain; silent here
|
|
66
|
+
// external → announced skip (F30): a human using an
|
|
67
|
+
// external system has no persona-tier
|
|
68
|
+
// catalogue YET — it must never transmute
|
|
69
|
+
// into sed/ssd, whose semantics are runtime
|
|
70
|
+
// calls between software
|
|
71
|
+
// anything else → announced skip (F29): claimed by no
|
|
72
|
+
// catalogue is said, never silent
|
|
73
|
+
const fromIsContainer = inputs.containerIdByEntryId.has(fromEntry ?? "");
|
|
74
|
+
const toIsContainer = inputs.containerIdByEntryId.has(toEntry ?? "");
|
|
75
|
+
let personEntry;
|
|
76
|
+
let targetEntry;
|
|
77
|
+
let targetContainer;
|
|
78
|
+
if (!fromIsContainer && personNameByEntry.has(fromEntry ?? "")) {
|
|
79
|
+
personEntry = fromEntry;
|
|
80
|
+
targetEntry = toEntry;
|
|
81
|
+
targetContainer = toIsContainer ? inputs.containerIdByEntryId.get(toEntry ?? "") : undefined;
|
|
82
|
+
}
|
|
83
|
+
else if (!toIsContainer && personNameByEntry.has(toEntry ?? "")) {
|
|
84
|
+
personEntry = toEntry;
|
|
85
|
+
targetEntry = fromEntry;
|
|
86
|
+
targetContainer = fromIsContainer ? inputs.containerIdByEntryId.get(fromEntry ?? "") : undefined;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ownSystemEntry !== "" && targetEntry === ownSystemEntry)
|
|
92
|
+
continue;
|
|
93
|
+
const personName = personNameByEntry.get(personEntry ?? "") ?? "";
|
|
94
|
+
const personaId = inputs.personaIdByName.get(personName.toLowerCase()) ??
|
|
95
|
+
inputs.personaIdBySlug.get(slugify(personName));
|
|
96
|
+
if (personaId === undefined) {
|
|
97
|
+
inputs.findings.add("coverage", `persona_container: Person "${personName}" on an L2 diagram has no matching persona row — edge skipped`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (targetContainer === undefined) {
|
|
101
|
+
const isExternal = inputs.externalIdByEntryId?.has(targetEntry ?? "") ?? false;
|
|
102
|
+
inputs.findings.add("coverage", isExternal
|
|
103
|
+
? `persona_container: persona "${personaId}" → external "${inputs.externalIdByEntryId.get(targetEntry ?? "")}"${label ? ` ("${label}")` : ""} — a human using an external system has no persona-tier catalogue; skipped, never transmuted into a system edge (F30)`
|
|
104
|
+
: `persona_container: persona "${personaId}" → "${targetEntry}"${label ? ` ("${label}")` : ""} resolves to no container — claimed by no catalogue; skipped (F29)`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const id = `${personaId}__${targetContainer}`;
|
|
108
|
+
const existing = rowsById.get(id);
|
|
109
|
+
if (existing !== undefined) {
|
|
110
|
+
mergeEdgeLabel(existing, label ?? "", "persona_container", inputs.findings);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const row = {
|
|
114
|
+
id,
|
|
115
|
+
persona_id: personaId,
|
|
116
|
+
container_id: targetContainer,
|
|
117
|
+
interaction: label ?? "",
|
|
118
|
+
};
|
|
119
|
+
rowsById.set(id, row);
|
|
120
|
+
rows.push(row);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
124
|
+
return rows;
|
|
125
|
+
}
|
|
126
|
+
function slugify(s) {
|
|
127
|
+
return s
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/\s*\([^)]+\)/g, "")
|
|
130
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
131
|
+
.replace(/^-+|-+$/g, "");
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=persona-container.js.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Persona/journey edge builders — emit:
|
|
2
|
+
// - persona_journey (persona ↔ journey, role: primary | handoff | supporting)
|
|
3
|
+
// - persona_feature (persona ↔ feature, from `## Related Features` in persona files)
|
|
4
|
+
// - journey_feature (journey ↔ feature, from `**Feature:**` link or
|
|
5
|
+
// `## Related Features` in journey files)
|
|
6
|
+
//
|
|
7
|
+
// Resolution rules:
|
|
8
|
+
// - Persona references in journeys are free-text (e.g. "Fleet Manager")
|
|
9
|
+
// because the persona convention doesn't enforce slug references. We
|
|
10
|
+
// resolve by name match (case-insensitive) against the personas index;
|
|
11
|
+
// unresolved entries log `[finding] orphan` and are skipped.
|
|
12
|
+
// - Feature references resolve via the path index from the features
|
|
13
|
+
// builder when the journey/persona uses a markdown link, OR via slug
|
|
14
|
+
// match against the feature id namespace ({capability_id}-{slug}).
|
|
15
|
+
// - "X → Y (handoff)" in primary_persona triggers a handoff role for Y
|
|
16
|
+
// against the same journey. The "primary" role goes to X.
|
|
17
|
+
export function buildPersonaJourney(inputs) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const [journeyId, primaryRaw] of inputs.journeyPrimaryPersonaById) {
|
|
21
|
+
const { primary, handoff } = splitHandoff(primaryRaw);
|
|
22
|
+
if (primary !== "") {
|
|
23
|
+
addEdge(inputs, rows, seen, journeyId, primary, "primary");
|
|
24
|
+
}
|
|
25
|
+
if (handoff !== "") {
|
|
26
|
+
addEdge(inputs, rows, seen, journeyId, handoff, "handoff");
|
|
27
|
+
}
|
|
28
|
+
const supporting = inputs.journeySupportingPersonasById.get(journeyId) ?? [];
|
|
29
|
+
for (const name of supporting) {
|
|
30
|
+
addEdge(inputs, rows, seen, journeyId, name, "supporting");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
function addEdge(inputs, rows, seen, journeyId, personaName, role) {
|
|
36
|
+
const personaId = resolvePersona(inputs.personaIdByName, inputs.personaIdBySlug, personaName);
|
|
37
|
+
if (personaId === undefined) {
|
|
38
|
+
inputs.findings.add("orphan", `persona_journey: journey ${journeyId} references persona "${personaName}" — no matching personas/<slug>.md`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const id = `${personaId}__${journeyId}__${role}`;
|
|
42
|
+
if (seen.has(id))
|
|
43
|
+
return;
|
|
44
|
+
seen.add(id);
|
|
45
|
+
rows.push({
|
|
46
|
+
id,
|
|
47
|
+
persona_id: personaId,
|
|
48
|
+
journey_id: journeyId,
|
|
49
|
+
role,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function resolvePersona(byName, bySlug, raw) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
if (trimmed === "")
|
|
55
|
+
return undefined;
|
|
56
|
+
const byNameHit = byName.get(trimmed.toLowerCase());
|
|
57
|
+
if (byNameHit !== undefined)
|
|
58
|
+
return byNameHit;
|
|
59
|
+
// Try slugified form: "Fleet Manager" → "fleet-manager".
|
|
60
|
+
const slug = trimmed
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
63
|
+
.replace(/^-+|-+$/g, "");
|
|
64
|
+
return bySlug.get(slug);
|
|
65
|
+
}
|
|
66
|
+
function splitHandoff(raw) {
|
|
67
|
+
// Pattern: `X → Y (handoff)` or `X -> Y (handoff)`.
|
|
68
|
+
const m = /^(.+?)\s*(?:→|->)\s*(.+?)\s*\(handoff\)\s*$/i.exec(raw.trim());
|
|
69
|
+
if (m)
|
|
70
|
+
return { primary: (m[1] ?? "").trim(), handoff: (m[2] ?? "").trim() };
|
|
71
|
+
return { primary: raw.trim(), handoff: "" };
|
|
72
|
+
}
|
|
73
|
+
export function buildPersonaFeature(inputs) {
|
|
74
|
+
return buildEdgesByFeatureRef({
|
|
75
|
+
findings: inputs.findings,
|
|
76
|
+
leftKey: "persona_id",
|
|
77
|
+
leftIdToRefs: inputs.relatedFeaturesById,
|
|
78
|
+
leftIdToPaths: new Map(),
|
|
79
|
+
featureIdByPath: inputs.featureIdByPath,
|
|
80
|
+
featureIdBySlug: inputs.featureIdBySlug,
|
|
81
|
+
edgeName: "persona_feature",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function buildJourneyFeature(inputs) {
|
|
85
|
+
return buildEdgesByFeatureRef({
|
|
86
|
+
findings: inputs.findings,
|
|
87
|
+
leftKey: "journey_id",
|
|
88
|
+
leftIdToRefs: inputs.journeyRelatedFeatureSlugsById,
|
|
89
|
+
leftIdToPaths: inputs.journeyFeaturePathsById,
|
|
90
|
+
featureIdByPath: inputs.featureIdByPath,
|
|
91
|
+
featureIdBySlug: inputs.featureIdBySlug,
|
|
92
|
+
edgeName: "journey_feature",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function buildEdgesByFeatureRef(inputs) {
|
|
96
|
+
const rows = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const addRow = (leftId, featureId) => {
|
|
99
|
+
const id = `${leftId}__${featureId}`;
|
|
100
|
+
if (seen.has(id))
|
|
101
|
+
return;
|
|
102
|
+
seen.add(id);
|
|
103
|
+
rows.push({
|
|
104
|
+
id,
|
|
105
|
+
[inputs.leftKey]: leftId,
|
|
106
|
+
feature_id: featureId,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
// Path-based references (markdown links to feature spec files).
|
|
110
|
+
for (const [leftId, paths] of inputs.leftIdToPaths) {
|
|
111
|
+
for (const path of paths) {
|
|
112
|
+
const featureId = inputs.featureIdByPath.get(path);
|
|
113
|
+
if (featureId === undefined) {
|
|
114
|
+
inputs.findings.add("orphan", `${inputs.edgeName}: ${leftId} links to "${path}" — no matching features.csv row`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
addRow(leftId, featureId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Slug-based references (bare bullets in `## Related Features`).
|
|
121
|
+
for (const [leftId, slugs] of inputs.leftIdToRefs) {
|
|
122
|
+
for (const slug of slugs) {
|
|
123
|
+
const featureId = inputs.featureIdBySlug.get(slug);
|
|
124
|
+
if (featureId === undefined) {
|
|
125
|
+
inputs.findings.add("orphan", `${inputs.edgeName}: ${leftId} references feature slug "${slug}" — no feature with this slug in features.csv`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
addRow(leftId, featureId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=persona-journey-edges.js.map
|