@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.
Files changed (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +437 -0
  151. package/dist/lib/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. 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