@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph projection — `sdd-cli catalogue graph` (M-ADR 0205 §4.6, Phase C0b).
|
|
3
|
+
*
|
|
4
|
+
* Derives a typed node/edge list (JSON Lines) from built catalogues. The
|
|
5
|
+
* projection is DERIVED, never authored, never a second source of truth:
|
|
6
|
+
* regenerated on every run, digest-stamped, not committed. The derivation
|
|
7
|
+
* rule is the C0a rule table (catalogue-engine-evolution plan), keyed on
|
|
8
|
+
* the rubric-coverage-map's `kind` × the schema's FK arity:
|
|
9
|
+
*
|
|
10
|
+
* entity (less `applications`) → nodes, id = "<catalogue>/<pk>"
|
|
11
|
+
* entity FK columns → edges typed "<catalogue>.<column>"
|
|
12
|
+
* edge, 2 FKs → edges, declaration order = source→target;
|
|
13
|
+
* a row with an empty nullable endpoint
|
|
14
|
+
* degrades to a property record, never an edge
|
|
15
|
+
* edge, 3 FKs → edges with declared endpoint pairs
|
|
16
|
+
* (THREE_FK_ENDPOINTS below)
|
|
17
|
+
* edge, 1 FK → property records on the FK owner
|
|
18
|
+
* `applications` → excluded (C0a ruling 2026-08-06: filtered
|
|
19
|
+
* view of products — its rows ARE product
|
|
20
|
+
* rows; emitting it creates edge-less
|
|
21
|
+
* shadow nodes beside the products nodes)
|
|
22
|
+
* finding / enum kinds → excluded (diagnostics / vocabularies)
|
|
23
|
+
*
|
|
24
|
+
* Emission reads the PARSED CSV ROWS, deliberately not an engine readback:
|
|
25
|
+
* the engine's load TRY_CASTs typed columns and silently nulls unparseable
|
|
26
|
+
* cells (a real, measured case: prose in a date-typed column). A projection
|
|
27
|
+
* that inherits cast smoothing is no longer a faithful derivation of the
|
|
28
|
+
* CSV canon. C1's impact queries run on the engine; this emission does not.
|
|
29
|
+
*
|
|
30
|
+
* Every edge endpoint and property owner MUST resolve to a node record in
|
|
31
|
+
* the same export — a dangling reference here is a defect in the projection,
|
|
32
|
+
* not a finding about the estate (the FK layer already reports those). A
|
|
33
|
+
* candidate whose endpoint does not resolve is OMITTED and COUNTED, loudly,
|
|
34
|
+
* in the header and summary: `omitted_external` for external /
|
|
35
|
+
* external-for-profiles FKs (expected at member tier, resolving at
|
|
36
|
+
* aggregate), `omitted_unresolved` for the rest (mirrors an FK-layer
|
|
37
|
+
* finding). Stub nodes are never emitted — inventing nodes is the failure
|
|
38
|
+
* mode the C0a rule exists to prevent.
|
|
39
|
+
*/
|
|
40
|
+
import { createHash } from "node:crypto";
|
|
41
|
+
/** Node-kind catalogues excluded from projection by C0a ruling. */
|
|
42
|
+
const EXCLUDED_NODE_CATALOGUES = new Set(["applications"]);
|
|
43
|
+
/**
|
|
44
|
+
* Declared endpoint pairs for the three 3-FK edge catalogues (C0a).
|
|
45
|
+
* `product_platform_dependencies` is SERVICE-FIRST — the aggregator already
|
|
46
|
+
* derives product→capability (cross_sdd_platform_capability) by joining
|
|
47
|
+
* target_service_id, so a capability-first rule would emit the same
|
|
48
|
+
* relationship twice; its resolution is per-row in projectRow below.
|
|
49
|
+
*/
|
|
50
|
+
const THREE_FK_ENDPOINTS = {
|
|
51
|
+
system_external_dependency: ["c4_system_id", "external_system_id"],
|
|
52
|
+
cross_sdd_platform_capability: [
|
|
53
|
+
"consuming_product_id",
|
|
54
|
+
"platform_capability_id",
|
|
55
|
+
],
|
|
56
|
+
product_platform_dependencies: null, // per-row: target_service_id, else target_capability_id
|
|
57
|
+
// schema 3.9.0 (M-ADR 0211 §4.1): the third FK, contract_id, is a PROPERTY of the
|
|
58
|
+
// capability→service dependency — the contract it is against — not an endpoint. The
|
|
59
|
+
// edge stays capability→service; the contract rides as a property.
|
|
60
|
+
capability_service: ["capability_id", "service_id"],
|
|
61
|
+
// schema 3.9.0 (M-ADR 0211) added entity_id as a third FK. M-ADR 0158 decides the
|
|
62
|
+
// edge: "entity_glossary is entity-to-term". domain_id is the row's scope, a
|
|
63
|
+
// property; a row with no entity_id degrades to a property of its glossary term.
|
|
64
|
+
entity_glossary: ["entity_id", "glossary_item_id"],
|
|
65
|
+
// schema 3.18.0 (M-ADR 0216): coverage is feature→integration; limiting_item_id is the
|
|
66
|
+
// PROPERTY that says why an unsupported pair is unsupported, not an endpoint.
|
|
67
|
+
feature_integration_support: ["feature_id", "integration_id"],
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The catalogues a tier reads when no directory is named. The aggregate tier's
|
|
71
|
+
* data is the aggregator's output, `catalogues-aggregated/`: defaulting it to the
|
|
72
|
+
* member `catalogues/` made `--tier aggregate` run the aggregate rules over MEMBER
|
|
73
|
+
* rows — measured at a platform root, 1 reachable node instead of 2,316, a clean-
|
|
74
|
+
* looking miss of every cross-SDD consumer.
|
|
75
|
+
*/
|
|
76
|
+
export function defaultCataloguesDir(tier) {
|
|
77
|
+
return tier === "aggregate" ? "catalogues-aggregated" : "catalogues";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Kinds must cover every schema catalogue — a catalogue without a kind
|
|
81
|
+
* cannot be classified and the projection must not guess (the map is
|
|
82
|
+
* guarded method-side by the entity-scoring-coverage correspondence
|
|
83
|
+
* instance; this is the consumer-side half of that guarantee).
|
|
84
|
+
*/
|
|
85
|
+
export function assertKindCoverage(schema, kinds) {
|
|
86
|
+
const missing = schema.catalogues
|
|
87
|
+
.map((c) => c.name)
|
|
88
|
+
.filter((n) => !kinds.has(n));
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
throw new Error(`rubric-coverage-map has no kind for: ${missing.join(", ")} — ` +
|
|
91
|
+
`sync the vendored map (it must cover every schema catalogue).`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function projectGraph(schema, kinds, rowsByCatalogue, tier,
|
|
95
|
+
/**
|
|
96
|
+
* When provided (M-ADR 0206 §4.6), every edge record is stamped with its
|
|
97
|
+
* declared `class` and RESOLVED `impact_flow` (role-conditional flows
|
|
98
|
+
* resolve per row), so consumers need no map lookup. Additive fields; the
|
|
99
|
+
* export regenerates per run, so the format change costs one restated
|
|
100
|
+
* determinism proof.
|
|
101
|
+
*/
|
|
102
|
+
semantics) {
|
|
103
|
+
assertKindCoverage(schema, kinds);
|
|
104
|
+
const stampFor = (type, props) => {
|
|
105
|
+
if (semantics === undefined)
|
|
106
|
+
return {};
|
|
107
|
+
const decl = type.includes(".")
|
|
108
|
+
? semantics.entityFks.get(type)
|
|
109
|
+
: semantics.edges.get(type);
|
|
110
|
+
if (decl === undefined) {
|
|
111
|
+
throw new Error(`graph: relationship ${type} has no declaration in the ` +
|
|
112
|
+
`rubric-coverage-map (M-ADR 0206) — sync the vendored map.`);
|
|
113
|
+
}
|
|
114
|
+
const flow = decl.impactFlow ??
|
|
115
|
+
decl.impactFlowBy.values[(props[decl.impactFlowBy.column] ?? "").trim()];
|
|
116
|
+
if (flow === undefined) {
|
|
117
|
+
throw new Error(`graph: ${type} resolves flow by column '${decl.impactFlowBy.column}' ` +
|
|
118
|
+
`but the row value is not in the declared map.`);
|
|
119
|
+
}
|
|
120
|
+
return { class: decl.class, impact_flow: flow };
|
|
121
|
+
};
|
|
122
|
+
const nodeCats = schema.catalogues.filter((c) => kinds.get(c.name) === "entity" && !EXCLUDED_NODE_CATALOGUES.has(c.name));
|
|
123
|
+
// `annotation` (M-ADR 0133 §4.2b; kinds corrected at schema 3.9.0): a single-FK assertion
|
|
124
|
+
// ABOUT one entity. It takes the same path as a 1-FK edge — a property record on the FK
|
|
125
|
+
// owner — so it joins the edge set here rather than vanishing from the projection.
|
|
126
|
+
const edgeCats = schema.catalogues.filter((c) => kinds.get(c.name) === "edge" || kinds.get(c.name) === "annotation");
|
|
127
|
+
// Pass 1 — the node set. Endpoint resolution below is against THIS
|
|
128
|
+
// export's nodes, which is exactly the per-export invariant.
|
|
129
|
+
const nodeIds = new Set();
|
|
130
|
+
const nodeLines = [];
|
|
131
|
+
for (const cat of nodeCats) {
|
|
132
|
+
for (const row of rowsByCatalogue.get(cat.name) ?? []) {
|
|
133
|
+
const key = cell(row, cat.primaryKey);
|
|
134
|
+
const id = `${cat.name}/${key}`;
|
|
135
|
+
nodeIds.add(id);
|
|
136
|
+
nodeLines.push(jsonLine({
|
|
137
|
+
t: "node",
|
|
138
|
+
id,
|
|
139
|
+
catalogue: cat.name,
|
|
140
|
+
key,
|
|
141
|
+
properties: propsOf(cat, row, new Set([cat.primaryKey])),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const resolveTarget = (fk, value) => {
|
|
146
|
+
const [refCat, refCol] = fk.references.split(".");
|
|
147
|
+
// The rule resolves FK values against node PKs; a reference to a
|
|
148
|
+
// non-PK column would resolve against the wrong namespace. All 127
|
|
149
|
+
// parsed FKs reference `.id` today — guard it rather than assume it.
|
|
150
|
+
const refDef = schema.catalogues.find((c) => c.name === refCat);
|
|
151
|
+
if (refDef && refDef.primaryKey !== refCol) {
|
|
152
|
+
throw new Error(`FK ${fk.column} references non-PK column ${fk.references} — ` +
|
|
153
|
+
`the projection cannot resolve non-PK references.`);
|
|
154
|
+
}
|
|
155
|
+
const target = `${refCat}/${value}`;
|
|
156
|
+
return nodeIds.has(target) ? target : null;
|
|
157
|
+
};
|
|
158
|
+
const isExternal = (fk) => fk.external || (fk.externalForProfiles ?? []).length > 0;
|
|
159
|
+
const edgeLines = [];
|
|
160
|
+
const propertyLines = [];
|
|
161
|
+
const omissions = { external: 0, unresolved: 0 };
|
|
162
|
+
const emitEdge = (cat, row, type, sourceFk, targetFk) => {
|
|
163
|
+
const sv = cell(row, sourceFk.column);
|
|
164
|
+
const tv = cell(row, targetFk.column);
|
|
165
|
+
const source = resolveTarget(sourceFk, sv);
|
|
166
|
+
const target = resolveTarget(targetFk, tv);
|
|
167
|
+
if (source === null || target === null) {
|
|
168
|
+
const failedFk = source === null ? sourceFk : targetFk;
|
|
169
|
+
if (isExternal(failedFk))
|
|
170
|
+
omissions.external += 1;
|
|
171
|
+
else
|
|
172
|
+
omissions.unresolved += 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const props = propsOf(cat, row, new Set([sourceFk.column, targetFk.column]));
|
|
176
|
+
edgeLines.push(jsonLine({
|
|
177
|
+
t: "edge",
|
|
178
|
+
type,
|
|
179
|
+
...stampFor(type, props),
|
|
180
|
+
source,
|
|
181
|
+
target,
|
|
182
|
+
properties: props,
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
const emitProperty = (cat, row, ownerFk) => {
|
|
186
|
+
const ov = cell(row, ownerFk.column);
|
|
187
|
+
const owner = ov === "" ? null : resolveTarget(ownerFk, ov);
|
|
188
|
+
if (owner === null) {
|
|
189
|
+
if (ov !== "" && isExternal(ownerFk))
|
|
190
|
+
omissions.external += 1;
|
|
191
|
+
else
|
|
192
|
+
omissions.unresolved += 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
propertyLines.push(jsonLine({
|
|
196
|
+
t: "property",
|
|
197
|
+
owner,
|
|
198
|
+
catalogue: cat.name,
|
|
199
|
+
via: ownerFk.column,
|
|
200
|
+
properties: propsOf(cat, row, new Set([ownerFk.column])),
|
|
201
|
+
}));
|
|
202
|
+
};
|
|
203
|
+
// Pass 2a — edges from edge catalogues, schema declaration order.
|
|
204
|
+
for (const cat of edgeCats) {
|
|
205
|
+
const fks = cat.foreignKeys;
|
|
206
|
+
for (const row of rowsByCatalogue.get(cat.name) ?? []) {
|
|
207
|
+
if (fks.length === 1) {
|
|
208
|
+
// Attribute table the metamodel calls an edge: the other "end" is
|
|
209
|
+
// an enum/key/name — a property record, never an edge.
|
|
210
|
+
emitProperty(cat, row, fks[0]);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
let sourceFk;
|
|
214
|
+
let targetFk;
|
|
215
|
+
if (fks.length === 2) {
|
|
216
|
+
[sourceFk, targetFk] = fks;
|
|
217
|
+
}
|
|
218
|
+
else if (cat.name === "product_platform_dependencies") {
|
|
219
|
+
sourceFk = fks.find((f) => f.column === "product_id");
|
|
220
|
+
const svc = cell(row, "target_service_id");
|
|
221
|
+
const cap = cell(row, "target_capability_id");
|
|
222
|
+
const targetCol = svc !== ""
|
|
223
|
+
? "target_service_id"
|
|
224
|
+
: cap !== ""
|
|
225
|
+
? "target_capability_id"
|
|
226
|
+
: null;
|
|
227
|
+
if (targetCol === null) {
|
|
228
|
+
// Neither target declared: property-only row on the product.
|
|
229
|
+
emitProperty(cat, row, sourceFk);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
targetFk = fks.find((f) => f.column === targetCol);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const pair = THREE_FK_ENDPOINTS[cat.name];
|
|
236
|
+
if (pair === undefined || pair === null) {
|
|
237
|
+
throw new Error(`edge catalogue ${cat.name} has ${fks.length} FKs and no declared ` +
|
|
238
|
+
`endpoint pair — extend THREE_FK_ENDPOINTS (a C0a-level decision, ` +
|
|
239
|
+
`not a default).`);
|
|
240
|
+
}
|
|
241
|
+
sourceFk = fks.find((f) => f.column === pair[0]);
|
|
242
|
+
targetFk = fks.find((f) => f.column === pair[1]);
|
|
243
|
+
}
|
|
244
|
+
if (sourceFk === undefined || targetFk === undefined) {
|
|
245
|
+
throw new Error(`edge catalogue ${cat.name}: declared endpoint column missing from ` +
|
|
246
|
+
`schema foreign_keys — schema and rule table disagree.`);
|
|
247
|
+
}
|
|
248
|
+
const sv = cell(row, sourceFk.column);
|
|
249
|
+
const tv = cell(row, targetFk.column);
|
|
250
|
+
if (sv === "" || tv === "") {
|
|
251
|
+
// Empty nullable endpoint: degrade to a property record on the
|
|
252
|
+
// present endpoint (screen_action is the live case — measured
|
|
253
|
+
// 2026-08-06: all 85+1,652 rows on both estates have no event_id).
|
|
254
|
+
const present = sv !== "" ? sourceFk : tv !== "" ? targetFk : null;
|
|
255
|
+
if (present === null) {
|
|
256
|
+
omissions.unresolved += 1; // no endpoint at all — counted, never guessed
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
emitProperty(cat, row, present);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
emitEdge(cat, row, cat.name, sourceFk, targetFk);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Pass 2b — edges carried on node rows (41 FK columns on 22 catalogues;
|
|
266
|
+
// 34–40% of all edges on the measured estates — the third of the graph
|
|
267
|
+
// the naive "edge catalogues = edges" rule drops).
|
|
268
|
+
for (const cat of nodeCats) {
|
|
269
|
+
for (const fk of cat.foreignKeys) {
|
|
270
|
+
for (const row of rowsByCatalogue.get(cat.name) ?? []) {
|
|
271
|
+
const value = cell(row, fk.column);
|
|
272
|
+
if (value === "")
|
|
273
|
+
continue;
|
|
274
|
+
const source = `${cat.name}/${cell(row, cat.primaryKey)}`;
|
|
275
|
+
const target = resolveTarget(fk, value);
|
|
276
|
+
if (target === null) {
|
|
277
|
+
if (isExternal(fk))
|
|
278
|
+
omissions.external += 1;
|
|
279
|
+
else
|
|
280
|
+
omissions.unresolved += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
edgeLines.push(jsonLine({
|
|
284
|
+
t: "edge",
|
|
285
|
+
type: `${cat.name}.${fk.column}`,
|
|
286
|
+
...stampFor(`${cat.name}.${fk.column}`, {}),
|
|
287
|
+
source,
|
|
288
|
+
target,
|
|
289
|
+
properties: {},
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const body = [...nodeLines, ...edgeLines, ...propertyLines];
|
|
295
|
+
const bodyText = body.map((l) => `${l}\n`).join("");
|
|
296
|
+
const digest = createHash("sha256").update(bodyText, "utf8").digest("hex");
|
|
297
|
+
const counts = {
|
|
298
|
+
nodes: nodeLines.length,
|
|
299
|
+
edges: edgeLines.length,
|
|
300
|
+
properties: propertyLines.length,
|
|
301
|
+
omittedExternal: omissions.external,
|
|
302
|
+
omittedUnresolved: omissions.unresolved,
|
|
303
|
+
};
|
|
304
|
+
const header = jsonLine({
|
|
305
|
+
t: "header",
|
|
306
|
+
format: "sdd-graph/1",
|
|
307
|
+
schema_version: schema.schemaVersion,
|
|
308
|
+
tier,
|
|
309
|
+
nodes: counts.nodes,
|
|
310
|
+
edges: counts.edges,
|
|
311
|
+
properties: counts.properties,
|
|
312
|
+
omitted_external: counts.omittedExternal,
|
|
313
|
+
omitted_unresolved: counts.omittedUnresolved,
|
|
314
|
+
digest: `sha256:${digest}`,
|
|
315
|
+
});
|
|
316
|
+
return { text: `${header}\n${bodyText}`, counts, digest };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Row columns as a properties object: schema column order (deterministic),
|
|
320
|
+
* empty string omitted (the schema's own convention — empty means absent),
|
|
321
|
+
* excluded columns (PK / endpoint / owner) carried structurally instead.
|
|
322
|
+
* Values stay the strings the CSV canon holds — no cast, no smoothing.
|
|
323
|
+
*/
|
|
324
|
+
function propsOf(cat, row, exclude) {
|
|
325
|
+
const out = {};
|
|
326
|
+
for (const col of cat.columns) {
|
|
327
|
+
if (exclude.has(col.name))
|
|
328
|
+
continue;
|
|
329
|
+
const v = row[col.name];
|
|
330
|
+
const s = v === null || v === undefined ? "" : String(v);
|
|
331
|
+
if (s === "")
|
|
332
|
+
continue;
|
|
333
|
+
out[col.name] = s;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/** JSON.stringify with stable insertion-order keys — the whole line contract. */
|
|
338
|
+
function jsonLine(obj) {
|
|
339
|
+
return JSON.stringify(obj);
|
|
340
|
+
}
|
|
341
|
+
/** One cell as the trimmed string the CSV canon holds ('' = absent). */
|
|
342
|
+
function cell(row, column) {
|
|
343
|
+
const v = row[column];
|
|
344
|
+
return v === null || v === undefined ? "" : String(v).trim();
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=graph-projection.js.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transitive impact analysis — `sdd-cli catalogue impact` (M-ADR 0206 §4.6).
|
|
3
|
+
*
|
|
4
|
+
* Impact traverses the graph-projection edge set BY DECLARED SEMANTICS: every
|
|
5
|
+
* relationship carries an impact-flow token in the rubric-coverage-map
|
|
6
|
+
* (M-ADR 0206 §4.1) naming which endpoint RECEIVES impact — `toward-source`
|
|
7
|
+
* (dependency shape: a change at the target propagates to the source),
|
|
8
|
+
* `toward-target` (membership shape: a part's change reaches its whole),
|
|
9
|
+
* `both` (realisation), `none` (xref, lineage records, attributes). One
|
|
10
|
+
* relationship (`service_event`) resolves its flow per row from a named
|
|
11
|
+
* column (§4.2). The closure is the set of nodes impact can reach from the
|
|
12
|
+
* seed along declared propagation directions, computed by recursive SQL
|
|
13
|
+
* (`WITH RECURSIVE`) on the Phase A engine.
|
|
14
|
+
*
|
|
15
|
+
* THE CO-DIRECTIONALITY INVARIANT (§4.4, a condition of 0206's acceptance):
|
|
16
|
+
* the Phase C mirror-dedup heuristic is retired because under the declared
|
|
17
|
+
* tokens every true-mirror pair propagates the SAME direction. That premise
|
|
18
|
+
* is exercised, not assumed: buildImpactPropagation asserts that no node
|
|
19
|
+
* pair receives propagation in BOTH directions from two DIFFERENT
|
|
20
|
+
* single-direction relationship types, and fails loudly naming the pair and
|
|
21
|
+
* types. A single relationship declaring `both` is deliberate bidirectional
|
|
22
|
+
* coupling and is exempt; a self-referential type contributing both
|
|
23
|
+
* directions from different rows (a hierarchy cycle) is likewise exempt.
|
|
24
|
+
* A future token edit that would silently bring the undirected flood back
|
|
25
|
+
* trips this assertion instead.
|
|
26
|
+
*
|
|
27
|
+
* Cycle discipline: the recursion carries NO depth column — recursive UNION
|
|
28
|
+
* deduplicates whole rows, and a depth column makes every lap of a cycle a
|
|
29
|
+
* "new" row: the query that looks more informative is the query that never
|
|
30
|
+
* terminates. As a bare id-set the closure saturates and stops.
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync } from "node:fs";
|
|
33
|
+
import { join, resolve } from "node:path";
|
|
34
|
+
import { readCatalogueCsv } from "./csv-reader.js";
|
|
35
|
+
import { loadCanonicalSchema } from "./schema-loader.js";
|
|
36
|
+
import { loadCatalogueKinds, loadRelationshipSemantics, } from "./relationship-semantics.js";
|
|
37
|
+
import { defaultCataloguesDir, projectGraph, } from "./graph-projection.js";
|
|
38
|
+
function resolveFlow(edge, semantics) {
|
|
39
|
+
const decl = edge.type.includes(".")
|
|
40
|
+
? semantics.entityFks.get(edge.type)
|
|
41
|
+
: semantics.edges.get(edge.type);
|
|
42
|
+
if (decl === undefined) {
|
|
43
|
+
throw new Error(`impact: relationship ${edge.type} has no declaration in the ` +
|
|
44
|
+
`rubric-coverage-map (M-ADR 0206) — sync the vendored map.`);
|
|
45
|
+
}
|
|
46
|
+
if (decl.impactFlow !== undefined)
|
|
47
|
+
return decl.impactFlow;
|
|
48
|
+
const by = decl.impactFlowBy;
|
|
49
|
+
const value = (edge.properties?.[by.column] ?? "").trim();
|
|
50
|
+
const flow = by.values[value];
|
|
51
|
+
if (flow === undefined) {
|
|
52
|
+
throw new Error(`impact: ${edge.type} resolves flow by column '${by.column}' but row ` +
|
|
53
|
+
`value '${value}' is not in the declared map — extend the ` +
|
|
54
|
+
`declaration or fix the row.`);
|
|
55
|
+
}
|
|
56
|
+
return flow;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve every declared edge into propagation steps per its flow, and
|
|
60
|
+
* exercise the §4.4 co-directionality invariant.
|
|
61
|
+
*/
|
|
62
|
+
export function buildImpactPropagation(edges, semantics) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const byFlow = new Map();
|
|
65
|
+
let flowNone = 0;
|
|
66
|
+
const pairs = new Map();
|
|
67
|
+
const notePair = (from, to, type) => {
|
|
68
|
+
const [a, b] = from < to ? [from, to] : [to, from];
|
|
69
|
+
const key = `${a}␟${b}`;
|
|
70
|
+
let rec = pairs.get(key);
|
|
71
|
+
if (rec === undefined) {
|
|
72
|
+
rec = { ab: new Set(), ba: new Set() };
|
|
73
|
+
pairs.set(key, rec);
|
|
74
|
+
}
|
|
75
|
+
(from === a ? rec.ab : rec.ba).add(type);
|
|
76
|
+
};
|
|
77
|
+
for (const e of edges) {
|
|
78
|
+
const flow = resolveFlow(e, semantics);
|
|
79
|
+
byFlow.set(flow, (byFlow.get(flow) ?? 0) + 1);
|
|
80
|
+
if (flow === "none") {
|
|
81
|
+
flowNone += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (flow === "toward-source" || flow === "both") {
|
|
85
|
+
out.push({ from: e.target, to: e.source, type: e.type });
|
|
86
|
+
notePair(e.target, e.source, e.type);
|
|
87
|
+
}
|
|
88
|
+
if (flow === "toward-target" || flow === "both") {
|
|
89
|
+
out.push({ from: e.source, to: e.target, type: e.type });
|
|
90
|
+
notePair(e.source, e.target, e.type);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// §4.4 invariant, refined on measured data (2026-08-06): a pair fails only
|
|
94
|
+
// when one type contributes EXCLUSIVELY a→b against another contributing
|
|
95
|
+
// EXCLUSIVELY b→a. A type that itself spans both directions on the pair —
|
|
96
|
+
// opposing role-rows of a role-conditional relationship (a service that
|
|
97
|
+
// publishes AND consumes the same event: five real pairs on the reverse
|
|
98
|
+
// estate, one service, all session-broadcast shaped), a self-referential
|
|
99
|
+
// hierarchy cycle, or a declared `both` flow — has DECLARED that pair
|
|
100
|
+
// bidirectional, so other types aligning with either direction are
|
|
101
|
+
// consistent with declarations, not drift. The flipped-token flood shape
|
|
102
|
+
// (two single-direction declarations opposing) still fails, loudly.
|
|
103
|
+
for (const [key, rec] of pairs) {
|
|
104
|
+
const abOnly = [...rec.ab].filter((t) => !rec.ba.has(t));
|
|
105
|
+
const baOnly = [...rec.ba].filter((t) => !rec.ab.has(t));
|
|
106
|
+
if (abOnly.length > 0 && baOnly.length > 0) {
|
|
107
|
+
const [a, b] = key.split("␟");
|
|
108
|
+
throw new Error(`impact: counter-directional reciprocal pair (M-ADR 0206 §4.4): ` +
|
|
109
|
+
`${abOnly[0]} propagates ${a} -> ${b} while ${baOnly[0]} propagates ` +
|
|
110
|
+
`${b} -> ${a}, and neither declares the pair bidirectional. The ` +
|
|
111
|
+
`declared tokens have re-created an undirected pair; fix the ` +
|
|
112
|
+
`classification before trusting any closure.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { edges: out, flowNone, byFlow };
|
|
116
|
+
}
|
|
117
|
+
function sqlStringLiteral(value) {
|
|
118
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Closure of `seed` over resolved propagation edges, on the engine.
|
|
122
|
+
* Exported for the fixture controls; the verb goes through runCatalogueImpact.
|
|
123
|
+
*/
|
|
124
|
+
export async function computeImpactClosure(propagation, seed) {
|
|
125
|
+
const { DuckDBInstance } = await import("@duckdb/node-api");
|
|
126
|
+
const instance = await DuckDBInstance.create(":memory:");
|
|
127
|
+
const conn = await instance.connect();
|
|
128
|
+
try {
|
|
129
|
+
await conn.run(`CREATE TABLE prop (f VARCHAR, t VARCHAR);`);
|
|
130
|
+
const CHUNK = 500;
|
|
131
|
+
for (let i = 0; i < propagation.length; i += CHUNK) {
|
|
132
|
+
const tuples = propagation
|
|
133
|
+
.slice(i, i + CHUNK)
|
|
134
|
+
.map((e) => `(${sqlStringLiteral(e.from)}, ${sqlStringLiteral(e.to)})`)
|
|
135
|
+
.join(",\n");
|
|
136
|
+
await conn.run(`INSERT INTO prop VALUES\n${tuples};`);
|
|
137
|
+
}
|
|
138
|
+
const seedLit = sqlStringLiteral(seed);
|
|
139
|
+
// CYCLE GUARD: the recursion carries NO depth column, deliberately.
|
|
140
|
+
// Recursive UNION deduplicates whole rows; a depth column makes every
|
|
141
|
+
// lap around a cycle a "new" row (same id, bigger depth), so the query
|
|
142
|
+
// that looks more informative is the query that never terminates — the
|
|
143
|
+
// catalogue graph is not a DAG. As a bare id-set the closure saturates
|
|
144
|
+
// and stops.
|
|
145
|
+
const reader = await conn.runAndReadAll(`WITH RECURSIVE reach(id) AS (\n` +
|
|
146
|
+
` SELECT t FROM prop WHERE f = ${seedLit}\n` +
|
|
147
|
+
` UNION\n` +
|
|
148
|
+
` SELECT p.t FROM prop p JOIN reach r ON p.f = r.id\n` +
|
|
149
|
+
`)\n` +
|
|
150
|
+
`SELECT DISTINCT id FROM reach WHERE id <> ${seedLit} ORDER BY id;`);
|
|
151
|
+
return reader.getRows().map((row) => String(row[0]));
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
conn.disconnectSync();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function runCatalogueImpact(inputs, write) {
|
|
158
|
+
const sddRoot = resolve(inputs.sddPath ?? process.cwd());
|
|
159
|
+
const tier = inputs.tier ?? "member";
|
|
160
|
+
const dir = resolve(sddRoot, inputs.cataloguesDir ?? defaultCataloguesDir(tier));
|
|
161
|
+
if (!existsSync(dir)) {
|
|
162
|
+
write(`catalogues directory not found at ${dir}\n` +
|
|
163
|
+
` Run \`sdd-cli catalogue build\` (or aggregate) first.\n`);
|
|
164
|
+
return { exitCode: 2 };
|
|
165
|
+
}
|
|
166
|
+
const schema = await loadCanonicalSchema();
|
|
167
|
+
const kinds = await loadCatalogueKinds();
|
|
168
|
+
const semantics = await loadRelationshipSemantics();
|
|
169
|
+
const rowsByCatalogue = new Map();
|
|
170
|
+
for (const cat of schema.catalogues) {
|
|
171
|
+
const filePath = join(dir, `${cat.name}.csv`);
|
|
172
|
+
if (!existsSync(filePath))
|
|
173
|
+
continue;
|
|
174
|
+
rowsByCatalogue.set(cat.name, (await readCatalogueCsv(filePath)).rows);
|
|
175
|
+
}
|
|
176
|
+
const projection = projectGraph(schema, kinds, rowsByCatalogue, tier);
|
|
177
|
+
const lines = projection.text.trimEnd().split("\n");
|
|
178
|
+
const nodeIds = new Set();
|
|
179
|
+
const edges = [];
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
const o = JSON.parse(line);
|
|
182
|
+
if (o.t === "node")
|
|
183
|
+
nodeIds.add(o.id);
|
|
184
|
+
else if (o.t === "edge")
|
|
185
|
+
edges.push({
|
|
186
|
+
source: o.source,
|
|
187
|
+
target: o.target,
|
|
188
|
+
type: o.type,
|
|
189
|
+
...(o.properties !== undefined ? { properties: o.properties } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!nodeIds.has(inputs.of)) {
|
|
193
|
+
write(`node not found in the projection: ${inputs.of}\n` +
|
|
194
|
+
` Node ids have the form "<catalogue>/<id>", e.g. contracts/orders-api.\n`);
|
|
195
|
+
return { exitCode: 2 };
|
|
196
|
+
}
|
|
197
|
+
const prop = buildImpactPropagation(edges, semantics);
|
|
198
|
+
const closure = await computeImpactClosure(prop.edges, inputs.of);
|
|
199
|
+
const byCatalogue = new Map();
|
|
200
|
+
for (const id of closure) {
|
|
201
|
+
const cat = id.slice(0, id.indexOf("/"));
|
|
202
|
+
byCatalogue.set(cat, (byCatalogue.get(cat) ?? 0) + 1);
|
|
203
|
+
}
|
|
204
|
+
write(`impact closure of ${inputs.of} (${tier} tier, schema ${schema.schemaVersion})\n`);
|
|
205
|
+
// Required stated lines (M-ADR 0206 §4.6): what the number means, and what
|
|
206
|
+
// was traversed under each declared flow — visible per run, not
|
|
207
|
+
// discoverable from the code.
|
|
208
|
+
write(` semantics: change impact over classified relationships (M-ADR 0206) —\n` +
|
|
209
|
+
` each relationship traversed per its declared impact-flow; the\n` +
|
|
210
|
+
` co-directionality invariant is asserted on every run\n`);
|
|
211
|
+
const flowCounts = [...prop.byFlow.entries()]
|
|
212
|
+
.sort()
|
|
213
|
+
.map(([f, n]) => `${f}=${n}`)
|
|
214
|
+
.join(" ");
|
|
215
|
+
write(` declared edges by resolved flow: ${flowCounts}\n`);
|
|
216
|
+
write(` reachable nodes: ${closure.length}\n`);
|
|
217
|
+
for (const [cat, n] of [...byCatalogue.entries()].sort()) {
|
|
218
|
+
write(` ${cat}: ${n}\n`);
|
|
219
|
+
}
|
|
220
|
+
for (const id of closure) {
|
|
221
|
+
write(` ${id}\n`);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
exitCode: 0,
|
|
225
|
+
result: { seed: inputs.of, closure, byCatalogue },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=impact-run.js.map
|