@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,63 @@
|
|
|
1
|
+
// service_capability_exemption builder — M-ADR 0133 §4.2b.
|
|
2
|
+
//
|
|
3
|
+
// Emits one row per repo-config declaring a `service_capability_exemption:` block:
|
|
4
|
+
//
|
|
5
|
+
// service_capability_exemption:
|
|
6
|
+
// reason: platform-internal # closed vocabulary, see canonical-schema.yaml
|
|
7
|
+
// note: "Internal ops dashboard; serves the platform, not a product capability."
|
|
8
|
+
//
|
|
9
|
+
// One tier down from `repo_capability_exemption` (M-ADR 0188 §4.3), for the same
|
|
10
|
+
// reason that table exists: M-ADR 0197 gave a repository a `services:` block, so a
|
|
11
|
+
// repository resolving under M-ADR 0188 no longer implies its services do. This
|
|
12
|
+
// keys on the repo's own DEFAULT/PRIMARY service id — a declared, non-primary
|
|
13
|
+
// `services:` entry has no per-entry authoring surface yet (M-ADR 0197 §4.7,
|
|
14
|
+
// design-only) and cannot be exempted through this block. That is a stated scope
|
|
15
|
+
// limit, not a silent one: an exemption block on a repo whose primary service id
|
|
16
|
+
// doesn't resolve is dropped with a finding, never guessed onto a different entry.
|
|
17
|
+
//
|
|
18
|
+
// VALIDATION ORDER MIRRORS repo-capability-exemption.ts. Every rejection below
|
|
19
|
+
// drops the exemption row and leaves the service unresolved — reported by
|
|
20
|
+
// check-service-capability-coverage.sh's tier-scoped verdict. It never drops the
|
|
21
|
+
// service. Fail open on the row; fail closed on the service.
|
|
22
|
+
export function buildServiceCapabilityExemption(inputs) {
|
|
23
|
+
const rows = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const cfg of inputs.configs) {
|
|
26
|
+
const block = cfg.data.service_capability_exemption;
|
|
27
|
+
if (block === undefined || block === null)
|
|
28
|
+
continue;
|
|
29
|
+
const reason = typeof block.reason === "string" ? block.reason.trim() : "";
|
|
30
|
+
const note = typeof block.note === "string" ? block.note.trim() : "";
|
|
31
|
+
if (reason === "") {
|
|
32
|
+
inputs.findings.add("convention", `${cfg.relPath}: service_capability_exemption declares no reason — exemption dropped, service stays unresolved (per M-ADR 0133 §4.2b)`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!inputs.allowedReasons.has(reason)) {
|
|
36
|
+
const allowed = [...inputs.allowedReasons].sort().join(", ");
|
|
37
|
+
inputs.findings.add("convention", `${cfg.relPath}: service_capability_exemption reason "${reason}" is outside the closed vocabulary (${allowed}) — exemption dropped, service stays unresolved (per M-ADR 0133 §4.2b)`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (note === "") {
|
|
41
|
+
inputs.findings.add("convention", `${cfg.relPath}: service_capability_exemption (${reason}) has an empty note — exemption dropped, service stays unresolved (per M-ADR 0133 §4.2b)`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Keyed to the repo's own default/primary service id. A repo whose
|
|
45
|
+
// `services:` block dropped that id entirely (declared entries only) has
|
|
46
|
+
// no service for this block to attach to yet — a stated scope limit
|
|
47
|
+
// (M-ADR 0197 §4.7), not a guess at which declared entry is "really" this
|
|
48
|
+
// one.
|
|
49
|
+
if (!inputs.knownServiceIds.has(cfg.repoId)) {
|
|
50
|
+
inputs.findings.add("orphan", `${cfg.relPath}: service_capability_exemption declared but "${cfg.repoId}" does not resolve to a known service — exemption dropped. A repo whose services: block dropped its own default id has no per-entry authoring surface for this yet (M-ADR 0197 §4.7)`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const id = cfg.repoId;
|
|
54
|
+
if (seen.has(id)) {
|
|
55
|
+
inputs.findings.add("divergence", `${cfg.relPath}: duplicate service_capability_exemption for service ${id} — first declaration wins`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
seen.add(id);
|
|
59
|
+
rows.push({ id, service_id: id, reason, note });
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=service-capability-exemption.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// service_contract consumes edges — emits one row per (service,
|
|
2
|
+
// contract, consumes) triple derived from each service's
|
|
3
|
+
// `repos/<service>/docs/service-architecture.md` External Interfaces
|
|
4
|
+
// table (Outbound rows).
|
|
5
|
+
//
|
|
6
|
+
// Resolution path:
|
|
7
|
+
// 1. Read the External Interfaces section of the service-architecture
|
|
8
|
+
// markdown. Pipe-delimited table; Direction column = "Outbound"
|
|
9
|
+
// (case-insensitive).
|
|
10
|
+
// 2. For each Outbound row, lookup the Interface column text against
|
|
11
|
+
// the adopter's `.sdd/service-contract-aliases.yaml` (interface_aliases
|
|
12
|
+
// exact match, then interface_contains substring) to get a target
|
|
13
|
+
// contract id.
|
|
14
|
+
// 3. Fall back to same_product_inference: if the interface matches a
|
|
15
|
+
// generic placeholder (e.g. "Private API Gateway"), substitute
|
|
16
|
+
// `{prefix}` with the caller's prefix (service_id minus known
|
|
17
|
+
// suffix). The resolved value is also a contract id.
|
|
18
|
+
// 4. Targets are contract IDs (not names). The schema marks both
|
|
19
|
+
// service_contract.{service_id,contract_id} as `external: true`
|
|
20
|
+
// (schema 2.8.0) — federated SDDs that lack a local services.csv +
|
|
21
|
+
// contracts.csv emit consumes rows whose FKs resolve at aggregation;
|
|
22
|
+
// the platform-root SDD validates internally first per the fk-validator
|
|
23
|
+
// optimistic-resolve rule.
|
|
24
|
+
//
|
|
25
|
+
// Pattern documented in sdd-method ADR 0147.
|
|
26
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
const REPOS_DIR_REL = "repos";
|
|
29
|
+
const SERVICE_ARCHITECTURE_REL = join("docs", "service-architecture.md");
|
|
30
|
+
const KNOWN_SUFFIXES = ["-api", "-bff", "-ui", "-consumer", "-web", "-service"];
|
|
31
|
+
export async function buildServiceContractConsumes(inputs) {
|
|
32
|
+
const rows = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
if (inputs.aliases === null)
|
|
35
|
+
return rows;
|
|
36
|
+
const useKnownGate = inputs.knownServiceIds.size > 0;
|
|
37
|
+
const repoNames = await listRepoDirs(join(inputs.sddPath, REPOS_DIR_REL));
|
|
38
|
+
for (const serviceId of repoNames) {
|
|
39
|
+
if (useKnownGate && !inputs.knownServiceIds.has(serviceId))
|
|
40
|
+
continue;
|
|
41
|
+
if (inputs.aliases.skipCallers.has(serviceId))
|
|
42
|
+
continue;
|
|
43
|
+
const archPath = join(inputs.sddPath, REPOS_DIR_REL, serviceId, SERVICE_ARCHITECTURE_REL);
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = await readFile(archPath, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const code = err.code;
|
|
50
|
+
if (code === "ENOENT")
|
|
51
|
+
continue;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
const outbound = parseOutboundInterfaces(text);
|
|
55
|
+
for (const iface of outbound) {
|
|
56
|
+
let contractId = resolveContractId(serviceId, iface.interfaceText, inputs.aliases);
|
|
57
|
+
if (contractId === null)
|
|
58
|
+
continue;
|
|
59
|
+
const mapped = inputs.legacyContractIdMap?.get(contractId);
|
|
60
|
+
if (mapped !== undefined && mapped.length === 1) {
|
|
61
|
+
inputs.findings.add("inferred", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") re-keyed to ` +
|
|
62
|
+
`"${mapped[0]}" — 2.x kindless contract id migrated per the 3.0.0 map (regenerable, not drift); ` +
|
|
63
|
+
`update .sdd/service-contract-aliases.yaml to retire this finding`);
|
|
64
|
+
contractId = mapped[0];
|
|
65
|
+
}
|
|
66
|
+
else if (mapped !== undefined && mapped.length > 1) {
|
|
67
|
+
inputs.findings.add("convention", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") maps to ` +
|
|
68
|
+
`${mapped.length} contracts at 3.0.0 [${mapped.join(", ")}] — a 2.x cross-family collision; ` +
|
|
69
|
+
`qualify the alias with the kind-suffixed id; edge dropped`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const id = `${serviceId}__${contractId}__consumes`;
|
|
73
|
+
if (seen.has(id))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(id);
|
|
76
|
+
rows.push({
|
|
77
|
+
id,
|
|
78
|
+
service_id: serviceId,
|
|
79
|
+
contract_id: contractId,
|
|
80
|
+
role: "consumes",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|
|
86
|
+
function resolveContractId(callerServiceId, interfaceText, aliases) {
|
|
87
|
+
const key = interfaceText.toLowerCase().trim();
|
|
88
|
+
// 1. Direct (exact) alias → contract id.
|
|
89
|
+
const direct = aliases.interfaceAliases.get(key);
|
|
90
|
+
if (direct !== undefined)
|
|
91
|
+
return direct;
|
|
92
|
+
// 2. Substring (contains) alias — longest-first.
|
|
93
|
+
for (const [substr, target] of aliases.interfaceContains) {
|
|
94
|
+
if (key.includes(substr))
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
// 3. Same-product template inference.
|
|
98
|
+
const template = aliases.sameProductTemplates.get(key);
|
|
99
|
+
if (template !== undefined) {
|
|
100
|
+
const prefix = derivePrefix(callerServiceId);
|
|
101
|
+
return template.replace(/\{prefix\}/g, prefix);
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function derivePrefix(serviceId) {
|
|
106
|
+
for (const suffix of KNOWN_SUFFIXES) {
|
|
107
|
+
if (serviceId.endsWith(suffix)) {
|
|
108
|
+
return serviceId.slice(0, -suffix.length);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return serviceId;
|
|
112
|
+
}
|
|
113
|
+
export function parseOutboundInterfaces(markdown) {
|
|
114
|
+
const out = [];
|
|
115
|
+
const sectionRegex = /^##\s+External Interfaces\s*$/im;
|
|
116
|
+
const match = sectionRegex.exec(markdown);
|
|
117
|
+
if (match === null)
|
|
118
|
+
return out;
|
|
119
|
+
// Slice from end of heading to start of next ## heading (or EOF).
|
|
120
|
+
const rest = markdown.slice(match.index + match[0].length);
|
|
121
|
+
const nextHeading = /^##\s+/m.exec(rest);
|
|
122
|
+
const section = nextHeading !== null ? rest.slice(0, nextHeading.index) : rest;
|
|
123
|
+
const lines = section.split(/\r?\n/);
|
|
124
|
+
let headerSeen = false;
|
|
125
|
+
let dividerSeen = false;
|
|
126
|
+
let cols = null;
|
|
127
|
+
for (const raw of lines) {
|
|
128
|
+
const line = raw.trim();
|
|
129
|
+
if (line === "" || !line.startsWith("|")) {
|
|
130
|
+
if (headerSeen)
|
|
131
|
+
break;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const cells = splitRow(line);
|
|
135
|
+
if (!headerSeen) {
|
|
136
|
+
const interfaceIdx = cells.findIndex((c) => c.toLowerCase() === "interface");
|
|
137
|
+
const directionIdx = cells.findIndex((c) => c.toLowerCase() === "direction");
|
|
138
|
+
if (interfaceIdx === -1 || directionIdx === -1)
|
|
139
|
+
continue;
|
|
140
|
+
cols = { iface: interfaceIdx, direction: directionIdx };
|
|
141
|
+
headerSeen = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!dividerSeen) {
|
|
145
|
+
// Divider row: cells of dashes (e.g. "---")
|
|
146
|
+
if (cells.every((c) => /^:?-+:?$/.test(c.trim()))) {
|
|
147
|
+
dividerSeen = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Some authors omit dividers — accept and fall through.
|
|
151
|
+
dividerSeen = true;
|
|
152
|
+
}
|
|
153
|
+
if (cols === null)
|
|
154
|
+
continue;
|
|
155
|
+
const direction = (cells[cols.direction] ?? "").trim().toLowerCase();
|
|
156
|
+
if (direction !== "outbound")
|
|
157
|
+
continue;
|
|
158
|
+
const ifaceText = (cells[cols.iface] ?? "").trim();
|
|
159
|
+
if (ifaceText === "")
|
|
160
|
+
continue;
|
|
161
|
+
out.push({ interfaceText: ifaceText });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function splitRow(line) {
|
|
166
|
+
// Drop leading + trailing | then split on internal |.
|
|
167
|
+
let s = line;
|
|
168
|
+
if (s.startsWith("|"))
|
|
169
|
+
s = s.slice(1);
|
|
170
|
+
if (s.endsWith("|"))
|
|
171
|
+
s = s.slice(0, -1);
|
|
172
|
+
return s.split("|").map((c) => c.trim());
|
|
173
|
+
}
|
|
174
|
+
async function listRepoDirs(reposRoot) {
|
|
175
|
+
try {
|
|
176
|
+
const entries = await readdir(reposRoot, { withFileTypes: true });
|
|
177
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const code = err.code;
|
|
181
|
+
if (code === "ENOENT")
|
|
182
|
+
return [];
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=service-contract-consumes.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// service_contract edge — emits one row per (service, contract, exposes)
|
|
2
|
+
// triple from `contracts.{openapi,proto,avro}` blocks in repo-config.
|
|
3
|
+
//
|
|
4
|
+
// Forward method convention: a service's repo-config declares the
|
|
5
|
+
// contracts it exposes. The "consumes" role isn't declared in current
|
|
6
|
+
// adopters; this builder emits role=exposes only.
|
|
7
|
+
import { basename, sep } from "node:path";
|
|
8
|
+
export function buildServiceContract(inputs) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
// Pre-compute fuzzy indexes for matching.
|
|
12
|
+
const suffixIndex = new Map();
|
|
13
|
+
const basenameIndex = new Map();
|
|
14
|
+
for (const [path, id] of inputs.contractIdByPath) {
|
|
15
|
+
const parts = path.split(sep);
|
|
16
|
+
if (parts.length >= 2)
|
|
17
|
+
suffixIndex.set(parts.slice(-2).join("/"), id);
|
|
18
|
+
basenameIndex.set(basename(path), id);
|
|
19
|
+
}
|
|
20
|
+
for (const cfg of inputs.configs) {
|
|
21
|
+
const block = cfg.data.contracts;
|
|
22
|
+
if (!inputs.knownServiceIds.has(cfg.repoId)) {
|
|
23
|
+
// Interim guard (M-ADR 0197 stopgap, ahead of Phase B's attribution
|
|
24
|
+
// mechanism): this check pre-dates M-ADR 0197 and always dropped a
|
|
25
|
+
// non-resolving repo id silently, on the assumption knownServiceIds
|
|
26
|
+
// was always a superset of every repo id — true until a repo could
|
|
27
|
+
// declare a `services:` block that drops its own default id. Only
|
|
28
|
+
// warn when there was actually a contracts block to lose; a repo
|
|
29
|
+
// with no service and no contracts (the ordinary library/infra case)
|
|
30
|
+
// stays silent, as it always has.
|
|
31
|
+
// NOTE: `block` here is repo-config's `contracts` (cfg.data.contracts), a
|
|
32
|
+
// DIFFERENT schema from the traceability manifest's contracts block — so its
|
|
33
|
+
// three kinds are not a stale copy of the manifest's five and must not be
|
|
34
|
+
// "fixed" by the shared traceability helper. Whether repo-config should also
|
|
35
|
+
// key graphql is its own question, on its own schema.
|
|
36
|
+
const hasContracts = block !== undefined &&
|
|
37
|
+
((block.openapi?.length ?? 0) > 0 ||
|
|
38
|
+
(block.proto?.length ?? 0) > 0 ||
|
|
39
|
+
(block.avro?.length ?? 0) > 0);
|
|
40
|
+
if (hasContracts) {
|
|
41
|
+
inputs.findings.add("orphan", `${cfg.relPath}: contracts block declares exposes-edges for service "${cfg.repoId}", which does not resolve in services.csv — dropped. Check a services: block entry exists at this id (M-ADR 0197); this can also mean the repo's own default service id was never a runtime type.`);
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!block)
|
|
46
|
+
continue;
|
|
47
|
+
for (const kind of ["openapi", "proto", "avro"]) {
|
|
48
|
+
const list = block[kind] ?? [];
|
|
49
|
+
for (const rawEntry of list) {
|
|
50
|
+
const entry = rawEntry.replace(/^\/+/, "");
|
|
51
|
+
const fullCandidate = `contracts/${kind}/${entry}`;
|
|
52
|
+
const cid = inputs.contractIdByPath.get(fullCandidate)
|
|
53
|
+
?? inputs.contractIdByPath.get(entry)
|
|
54
|
+
?? suffixIndex.get(fullCandidate)
|
|
55
|
+
?? suffixIndex.get(entry)
|
|
56
|
+
?? basenameIndex.get(basename(entry));
|
|
57
|
+
if (cid === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const id = `${cfg.repoId}__${cid}__exposes`;
|
|
60
|
+
if (seen.has(id))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(id);
|
|
63
|
+
rows.push({
|
|
64
|
+
id,
|
|
65
|
+
service_id: cfg.repoId,
|
|
66
|
+
contract_id: cid,
|
|
67
|
+
role: "exposes",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=service-contract.js.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Primary reverse-method walker for the service_entity edge catalogue per
|
|
2
|
+
// ADR 0159 §4.5. Reads per-domain `## Data Store Mapping` markdown tables in
|
|
3
|
+
// `docs/domains/{domain}/architecture/data-model.mermaid.md` files.
|
|
4
|
+
//
|
|
5
|
+
// Prototype findings (per ADR 0159 — data-store-mapping table variations):
|
|
6
|
+
// - Two heading variants: `## Data Store Mapping` and `## Data Store Inventory`
|
|
7
|
+
// (the latter is store-centric — different schema, no Entity column — and
|
|
8
|
+
// gets skipped with a convention finding).
|
|
9
|
+
// - Three column-shape variants: standard (Entity | Store | Schema/Collection
|
|
10
|
+
// | Owning Service | PPT/Notes), developer's first col = `Entity / Artefact`,
|
|
11
|
+
// and vehicle's lack of `Owning Service` column (skipped).
|
|
12
|
+
// - Compound owning-service cells split on ` / ` — emit one edge per part.
|
|
13
|
+
// - Trailing `(annotation)` on entity texts stripped before slug-lookup.
|
|
14
|
+
// - Cross-domain entity references in tables resolve against entities.csv
|
|
15
|
+
// from any domain (not just the table's own domain) — same-domain first.
|
|
16
|
+
//
|
|
17
|
+
// Service-name resolution rules (ADR 0159 §4.7):
|
|
18
|
+
// 1. Try alias-map exact match (case-insensitive on display name).
|
|
19
|
+
// 2. Try entitySlug(display_name) ⋈ entitySlug(services.name).
|
|
20
|
+
// 3. Try entitySlug(display_name) ⋈ last_segment(services.id).
|
|
21
|
+
// 4. Fire [finding] orphan; skip edge.
|
|
22
|
+
import { readFile } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { parse as parseYaml } from "yaml";
|
|
25
|
+
import { entitySlug } from "./entity-slug.js";
|
|
26
|
+
import { RE_DATA_STORE_HEADING, collectTableLines, splitRow, } from "./data-store-mapping.js";
|
|
27
|
+
const ALIAS_REL_PATH = join("docs", "platform", "reference", "service-name-aliases.yaml");
|
|
28
|
+
export async function buildServiceEntityFromTables(inputs) {
|
|
29
|
+
const aliasMap = await loadAliasMap(inputs.sddPath, inputs.findings);
|
|
30
|
+
// Pre-compute service lookups.
|
|
31
|
+
const serviceIdBySlugName = new Map();
|
|
32
|
+
const serviceIdByTrailSegment = new Map();
|
|
33
|
+
for (const s of inputs.services) {
|
|
34
|
+
const id = String(s["id"]);
|
|
35
|
+
const name = String(s["name"] ?? "");
|
|
36
|
+
if (name)
|
|
37
|
+
serviceIdBySlugName.set(entitySlug(name), id);
|
|
38
|
+
const lastSeg = id.split("-").pop() ?? "";
|
|
39
|
+
if (lastSeg && !serviceIdByTrailSegment.has(lastSeg)) {
|
|
40
|
+
serviceIdByTrailSegment.set(lastSeg, id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const rowsByPk = new Map();
|
|
44
|
+
for (const dm of inputs.domainModels) {
|
|
45
|
+
if (!dm.doc_path.endsWith(".mermaid.md"))
|
|
46
|
+
continue;
|
|
47
|
+
const fullPath = dm.doc_path.startsWith("/")
|
|
48
|
+
? dm.doc_path
|
|
49
|
+
: join(inputs.sddPath, dm.doc_path);
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = await readFile(fullPath, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
continue;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
const lines = text.split(/\r?\n/);
|
|
60
|
+
const headingIdx = lines.findIndex((l) => RE_DATA_STORE_HEADING.test(l));
|
|
61
|
+
if (headingIdx === -1)
|
|
62
|
+
continue;
|
|
63
|
+
const tableLines = collectTableLines(lines, headingIdx + 1);
|
|
64
|
+
if (tableLines === null || tableLines.length < 2) {
|
|
65
|
+
inputs.findings.add("convention", `service-entity-tables: ${dm.doc_path}: Data Store heading present but no markdown table follows — skipped`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const headers = splitRow(tableLines[0]);
|
|
69
|
+
let entityColIdx = -1;
|
|
70
|
+
let serviceColIdx = -1;
|
|
71
|
+
for (let i = 0; i < headers.length; i++) {
|
|
72
|
+
const h = headers[i].toLowerCase();
|
|
73
|
+
if (entityColIdx === -1 && (h === "entity" || h.startsWith("entity"))) {
|
|
74
|
+
entityColIdx = i;
|
|
75
|
+
}
|
|
76
|
+
if (serviceColIdx === -1 && h.includes("owning service")) {
|
|
77
|
+
serviceColIdx = i;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (entityColIdx === -1 || serviceColIdx === -1) {
|
|
81
|
+
inputs.findings.add("convention", `service-entity-tables: ${dm.doc_path}: Data Store table missing Entity or Owning Service column (headers: ${headers.join(" | ")}) — skipped`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Skip the |---|---| separator row (lineLine[1]); data rows start at index 2.
|
|
85
|
+
for (let i = 2; i < tableLines.length; i++) {
|
|
86
|
+
const cells = splitRow(tableLines[i]);
|
|
87
|
+
if (cells.length <= Math.max(entityColIdx, serviceColIdx))
|
|
88
|
+
continue;
|
|
89
|
+
const entityText = cells[entityColIdx].trim();
|
|
90
|
+
const serviceText = cells[serviceColIdx].trim();
|
|
91
|
+
if (!entityText || !serviceText)
|
|
92
|
+
continue;
|
|
93
|
+
// Strip trailing parenthetical annotation: "Role (fleet)" → "Role".
|
|
94
|
+
const entityNorm = entityText.replace(/\s*\([^)]+\)\s*$/, "").trim();
|
|
95
|
+
const entSlug = entitySlug(entityNorm);
|
|
96
|
+
let entityId = inputs.entityIdByDomainSlug.get(`${dm.domain_id}|${entSlug}`);
|
|
97
|
+
if (entityId === undefined) {
|
|
98
|
+
entityId = inputs.entityIdBySlug.get(entSlug);
|
|
99
|
+
}
|
|
100
|
+
if (entityId === undefined) {
|
|
101
|
+
inputs.findings.add("orphan", `service-entity-tables: ${dm.doc_path}: entity "${entityText}" doesn't resolve to an entities.csv row — edge skipped`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Compound owning-service: split on " / ".
|
|
105
|
+
const serviceParts = serviceText
|
|
106
|
+
.split(/\s+\/\s+/)
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter((s) => s.length > 0);
|
|
109
|
+
for (const sp of serviceParts) {
|
|
110
|
+
const serviceId = resolveService(sp, aliasMap, serviceIdBySlugName, serviceIdByTrailSegment, inputs.knownServiceIds);
|
|
111
|
+
if (serviceId === undefined) {
|
|
112
|
+
inputs.findings.add("orphan", `service-entity-tables: ${dm.doc_path}: owning-service "${sp}" doesn't resolve to a services.csv row — edge skipped`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const pk = `${serviceId}__${entityId}__owns`;
|
|
116
|
+
if (rowsByPk.has(pk))
|
|
117
|
+
continue;
|
|
118
|
+
rowsByPk.set(pk, {
|
|
119
|
+
id: pk,
|
|
120
|
+
service_id: serviceId,
|
|
121
|
+
entity_id: entityId,
|
|
122
|
+
relation: "owns",
|
|
123
|
+
evidence_path: dm.doc_path,
|
|
124
|
+
source_sdd_id: "",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...rowsByPk.values()];
|
|
130
|
+
}
|
|
131
|
+
async function loadAliasMap(sddPath, findings) {
|
|
132
|
+
const empty = { byDisplayLower: new Map() };
|
|
133
|
+
const path = join(sddPath, ALIAS_REL_PATH);
|
|
134
|
+
let text;
|
|
135
|
+
try {
|
|
136
|
+
text = await readFile(path, "utf-8");
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err.code === "ENOENT")
|
|
140
|
+
return empty;
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
let raw;
|
|
144
|
+
try {
|
|
145
|
+
raw = parseYaml(text);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
findings.add("convention", `${ALIAS_REL_PATH}: failed to parse YAML — ${err.message}`);
|
|
149
|
+
return empty;
|
|
150
|
+
}
|
|
151
|
+
if (!raw || typeof raw !== "object")
|
|
152
|
+
return empty;
|
|
153
|
+
const obj = raw;
|
|
154
|
+
const aliases = obj["aliases"];
|
|
155
|
+
if (!Array.isArray(aliases))
|
|
156
|
+
return empty;
|
|
157
|
+
const map = new Map();
|
|
158
|
+
for (const a of aliases) {
|
|
159
|
+
if (!a || typeof a !== "object")
|
|
160
|
+
continue;
|
|
161
|
+
const e = a;
|
|
162
|
+
const display = String(e["display"] ?? "").trim();
|
|
163
|
+
const canonical = String(e["canonical"] ?? "").trim();
|
|
164
|
+
if (display && canonical)
|
|
165
|
+
map.set(display.toLowerCase(), canonical);
|
|
166
|
+
}
|
|
167
|
+
return { byDisplayLower: map };
|
|
168
|
+
}
|
|
169
|
+
function resolveService(display, alias, serviceIdBySlugName, serviceIdByTrailSegment, knownServiceIds) {
|
|
170
|
+
// 1. Alias map exact (case-insensitive).
|
|
171
|
+
const aliased = alias.byDisplayLower.get(display.toLowerCase());
|
|
172
|
+
if (aliased && knownServiceIds.has(aliased))
|
|
173
|
+
return aliased;
|
|
174
|
+
// 2. Slug-of-name match.
|
|
175
|
+
const slug = entitySlug(display);
|
|
176
|
+
if (!slug)
|
|
177
|
+
return undefined;
|
|
178
|
+
const bySlugName = serviceIdBySlugName.get(slug);
|
|
179
|
+
if (bySlugName && knownServiceIds.has(bySlugName))
|
|
180
|
+
return bySlugName;
|
|
181
|
+
// 3. Trailing-id-segment match.
|
|
182
|
+
const byTrail = serviceIdByTrailSegment.get(slug);
|
|
183
|
+
if (byTrail && knownServiceIds.has(byTrail))
|
|
184
|
+
return byTrail;
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=service-entity-tables.js.map
|