@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,61 @@
|
|
|
1
|
+
// repo_capability_exemption builder — M-ADR 0188 §4.3.
|
|
2
|
+
//
|
|
3
|
+
// Emits one row per repo-config declaring a `capability_exemption:` block:
|
|
4
|
+
//
|
|
5
|
+
// capability_exemption:
|
|
6
|
+
// reason: infrastructure # closed vocabulary, see canonical-schema.yaml
|
|
7
|
+
// note: "Terraform modules; provisions the estate, realises no capability."
|
|
8
|
+
//
|
|
9
|
+
// The table exists so that "this repository deliberately bears no capability"
|
|
10
|
+
// is a fact in the model rather than an absence in it. Under M-ADR 0188 §4.1
|
|
11
|
+
// every `repositories` row must resolve — by a `repo_capability` edge, or by a
|
|
12
|
+
// row here. FK integrity cannot express that: a missing edge is not a dangling
|
|
13
|
+
// reference, it is nothing, and nothing is what a referential check is blind
|
|
14
|
+
// to. Coverage is a participation constraint, checked, not declared.
|
|
15
|
+
//
|
|
16
|
+
// VALIDATION ORDER IS LOad-BEARING. Every rejection below drops the exemption
|
|
17
|
+
// row and leaves the repository unresolved — reported as an orphan by
|
|
18
|
+
// `check-repo-capability-coverage.sh`. It never drops the repository. An
|
|
19
|
+
// exemption that fails validation must not quietly succeed at the one thing an
|
|
20
|
+
// exemption does, which is remove a repository from the invariant's attention.
|
|
21
|
+
// Fail open on the row; fail closed on the repository.
|
|
22
|
+
export function buildRepoCapabilityExemption(inputs) {
|
|
23
|
+
const rows = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const cfg of inputs.configs) {
|
|
26
|
+
const block = cfg.data.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}: capability_exemption declares no reason — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!inputs.allowedReasons.has(reason)) {
|
|
36
|
+
const allowed = [...inputs.allowedReasons].sort().join(", ");
|
|
37
|
+
inputs.findings.add("convention", `${cfg.relPath}: capability_exemption reason "${reason}" is outside the closed vocabulary (${allowed}) — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// The note is what makes an exemption reviewable rather than merely
|
|
41
|
+
// countable. An unexplained suppression is a defect; an explained one is
|
|
42
|
+
// a decision. The method takes the lint-suppression position here.
|
|
43
|
+
if (note === "") {
|
|
44
|
+
inputs.findings.add("convention", `${cfg.relPath}: capability_exemption (${reason}) has an empty note — exemption dropped, repo stays unresolved (per M-ADR 0188 §4.2)`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (inputs.knownRepoIds !== undefined &&
|
|
48
|
+
!inputs.knownRepoIds.has(cfg.repoId)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const id = cfg.repoId;
|
|
52
|
+
if (seen.has(id)) {
|
|
53
|
+
inputs.findings.add("divergence", `${cfg.relPath}: duplicate capability_exemption for repo ${cfg.repoId} — first declaration wins`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seen.add(id);
|
|
57
|
+
rows.push({ id, repo_id: cfg.repoId, reason, note });
|
|
58
|
+
}
|
|
59
|
+
return rows;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=repo-capability-exemption.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// repo_dependency — a repository depends on another repository's code without either
|
|
2
|
+
// being a running service: a library linked at build time, or a template family it was
|
|
3
|
+
// generated from (M-ADR 0217, schema 3.20.0).
|
|
4
|
+
//
|
|
5
|
+
// Two sources, merged by id (M-ADR 0157 parity):
|
|
6
|
+
// DECLARED repo-config `depends_on: [{repo, kind, version?}]`; from_repo_id is the
|
|
7
|
+
// repository the repo-config describes. `kind` is required, so a provenance
|
|
8
|
+
// dependency is never recorded as a library by default.
|
|
9
|
+
// DERIVED a per-component integration manifest's `dependencies.sharedLibraries`
|
|
10
|
+
// (`repos/<member>/integration-manifest.yaml`), each entry kind `library`,
|
|
11
|
+
// from_repo_id the member directory. `none` is a declared absence.
|
|
12
|
+
// The declared row wins `version` and `declared_in` when both name one dependency.
|
|
13
|
+
//
|
|
14
|
+
// to_repo_id is EXTERNAL: it may belong to another SDD, so build reports what it cannot
|
|
15
|
+
// resolve and the aggregator fails what still resolves to nothing. An entry is never
|
|
16
|
+
// renamed or guessed to make it resolve.
|
|
17
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { parse as parseYaml } from "yaml";
|
|
20
|
+
export const REPO_DEPENDENCY_KINDS = ["library", "generated-from"];
|
|
21
|
+
const rowId = (d) => `${d.from}__${d.to}__${d.kind}`;
|
|
22
|
+
function declared(inputs) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const cfg of inputs.configs) {
|
|
25
|
+
const entries = cfg.data.depends_on ?? [];
|
|
26
|
+
entries.forEach((raw, i) => {
|
|
27
|
+
const where = `${cfg.relPath}: depends_on[${i}]`;
|
|
28
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
29
|
+
inputs.findings.add("convention", `${where}: expected a mapping {repo, kind, version?} — entry skipped (M-ADR 0217 §4.2)`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const e = raw;
|
|
33
|
+
const repo = typeof e["repo"] === "string" ? e["repo"].trim() : "";
|
|
34
|
+
const kind = typeof e["kind"] === "string" ? e["kind"].trim() : "";
|
|
35
|
+
if (repo === "") {
|
|
36
|
+
inputs.findings.add("convention", `${where}: no repo — entry skipped (M-ADR 0217 §4.2)`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!REPO_DEPENDENCY_KINDS.includes(kind)) {
|
|
40
|
+
inputs.findings.add("convention", `${where}: kind "${kind}" is not one of ${REPO_DEPENDENCY_KINDS.join(" | ")} — entry skipped. ` +
|
|
41
|
+
`kind is required: a provenance dependency is never recorded as a library by default (M-ADR 0217 §4.2)`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const version = e["version"] === undefined || e["version"] === null ? "" : String(e["version"]).trim();
|
|
45
|
+
out.push({ from: cfg.repoId, to: repo, kind: kind, version, declaredIn: cfg.relPath });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
async function derived(inputs) {
|
|
51
|
+
let dirs;
|
|
52
|
+
try {
|
|
53
|
+
dirs = (await readdir(join(inputs.sddPath, "repos"), { withFileTypes: true }))
|
|
54
|
+
.filter((e) => e.isDirectory())
|
|
55
|
+
.map((e) => e.name)
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err.code === "ENOENT")
|
|
60
|
+
return [];
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const dir of dirs) {
|
|
65
|
+
const rel = `repos/${dir}/integration-manifest.yaml`;
|
|
66
|
+
let text;
|
|
67
|
+
try {
|
|
68
|
+
text = await readFile(join(inputs.sddPath, rel), "utf-8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err.code === "ENOENT")
|
|
72
|
+
continue;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
let doc;
|
|
76
|
+
try {
|
|
77
|
+
doc = parseYaml(text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue; // integration_component already reports an unparsable manifest
|
|
81
|
+
}
|
|
82
|
+
const deps = doc?.dependencies;
|
|
83
|
+
const libs = deps?.sharedLibraries;
|
|
84
|
+
if (libs === undefined || libs === null)
|
|
85
|
+
continue; // the field is optional; absence is no finding
|
|
86
|
+
if (!Array.isArray(libs)) {
|
|
87
|
+
inputs.findings.add("convention", `${rel}: dependencies.sharedLibraries is not a list — ignored (M-ADR 0217 §4.2)`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const names = libs.map((l) => String(l).trim()).filter((l) => l !== "" && l !== "none");
|
|
91
|
+
if (names.length === 0)
|
|
92
|
+
continue; // `none` is a declared absence
|
|
93
|
+
// from_repo_id is an INTERNAL key: the member directory must be one of this SDD's
|
|
94
|
+
// repositories, or the row would fail the build's own FK check.
|
|
95
|
+
if (!inputs.knownRepoIds.has(dir)) {
|
|
96
|
+
inputs.findings.add("coverage", `repo_dependency: ${rel} declares ${names.length} shared librar${names.length === 1 ? "y" : "ies"} but its directory "${dir}" is no repositories.id in this SDD — not emitted. Catalogue the repository or correct the directory name.`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const to of names)
|
|
100
|
+
out.push({ from: dir, to, kind: "library", version: "", declaredIn: rel });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
export async function buildRepoDependencies(inputs) {
|
|
105
|
+
const byId = new Map();
|
|
106
|
+
for (const d of await derived(inputs))
|
|
107
|
+
if (!byId.has(rowId(d)))
|
|
108
|
+
byId.set(rowId(d), d);
|
|
109
|
+
// Declared second, so it overrides version and declared_in.
|
|
110
|
+
for (const d of declared(inputs)) {
|
|
111
|
+
const prior = byId.get(rowId(d));
|
|
112
|
+
byId.set(rowId(d), prior === undefined ? d : { ...d, version: d.version !== "" ? d.version : prior.version });
|
|
113
|
+
}
|
|
114
|
+
return [...byId.values()]
|
|
115
|
+
.sort((a, b) => rowId(a).localeCompare(rowId(b)))
|
|
116
|
+
.map((d) => ({
|
|
117
|
+
id: rowId(d),
|
|
118
|
+
from_repo_id: d.from,
|
|
119
|
+
to_repo_id: d.to,
|
|
120
|
+
kind: d.kind,
|
|
121
|
+
version: d.version,
|
|
122
|
+
declared_in: d.declaredIn,
|
|
123
|
+
source_sdd_id: "",
|
|
124
|
+
platform_sdd_id: "",
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=repo-dependency.js.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Repo-edge builders — repo_capability, repo_contract, feature_repo.
|
|
2
|
+
//
|
|
3
|
+
// Per ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
|
|
4
|
+
// both edges ingest from forward-method declared sources AND reverse-method
|
|
5
|
+
// derived sources, merged by primary-key deduplication. The two paths are
|
|
6
|
+
// equal-status per the parity principle; neither displaces the other.
|
|
7
|
+
//
|
|
8
|
+
// Forward-method declared sources (typically authored by forward-method
|
|
9
|
+
// adopters in `orchestration/repo-config/<domain>/<repo>.yaml`):
|
|
10
|
+
//
|
|
11
|
+
// capabilities: # M-ADR 0188 §6.1
|
|
12
|
+
// - <capability spec_path, exactly as emitted in capabilities.csv>
|
|
13
|
+
//
|
|
14
|
+
// The spec_path layout is profile-dependent, NOT fixed — a reverse platform SDD emits
|
|
15
|
+
// `docs/domains/<domain>/product/<capability>.md`, a forward application-product may emit
|
|
16
|
+
// `docs/<capability>.md`. Match against capabilities.csv, never against a path convention.
|
|
17
|
+
//
|
|
18
|
+
// additional_context: # legacy, multi-purpose
|
|
19
|
+
// - docs/domains/<domain>/product/<capability>.md
|
|
20
|
+
// - docs/domains/<domain>/architecture/adr/ADR XXXX: ....md
|
|
21
|
+
//
|
|
22
|
+
// contracts:
|
|
23
|
+
// openapi: [<path-relative-to-contracts/openapi/>]
|
|
24
|
+
// proto: [<path-relative-to-contracts/proto/>]
|
|
25
|
+
// avro: [<path-relative-to-contracts/avro/>]
|
|
26
|
+
//
|
|
27
|
+
// Reverse-method derived sources (universal — work for any SDD with the
|
|
28
|
+
// underlying catalogue rows populated):
|
|
29
|
+
//
|
|
30
|
+
// repo_contract Path X (universal) — `service_contract ⋈
|
|
31
|
+
// services.primary_repo_id`. A service exposing a contract is implemented
|
|
32
|
+
// by a repo (its primary_repo_id) — the repo "owns" the contract by
|
|
33
|
+
// transitive inference. Works universally because primary_repo_id and
|
|
34
|
+
// service_contract are core-schema populated. Added per ADR 0157 §4.2
|
|
35
|
+
// amendment after an early-adopter 0/30 source_repo_id outcome.
|
|
36
|
+
// repo_contract Path Y — `contracts.source_repo_id` denormalisation
|
|
37
|
+
// (from OpenAPI `x-source-repo` hints per ADR 0110, or whatever
|
|
38
|
+
// populates the column). Augments Path X when adopters carry the hint.
|
|
39
|
+
// repo_capability Path X (universal) — `capability_service ⋈
|
|
40
|
+
// services.primary_repo_id`. Works in both forward and reverse method
|
|
41
|
+
// shapes because primary_repo_id is core-schema and capability_service
|
|
42
|
+
// is heavily authored.
|
|
43
|
+
// repo_capability Path Y (augments via ADR 0143 feature-grounding) —
|
|
44
|
+
// `capability_feature ⋈ feature_repo`. Adopters who have adopted the
|
|
45
|
+
// feature-grounding extractor get many additional rows.
|
|
46
|
+
//
|
|
47
|
+
// feature_repo: ADR 0143's feature-grounding extractor populates this in
|
|
48
|
+
// build.ts (10a). The forward declared `contributes_to.artifact_type:
|
|
49
|
+
// feature-set` path is documented in the metamodel but unused by any
|
|
50
|
+
// observed adopter — left empty here pending first-adopter authoring.
|
|
51
|
+
import { basename, sep } from "node:path";
|
|
52
|
+
import { normaliseAdditionalContextEntry, } from "./repositories.js";
|
|
53
|
+
/**
|
|
54
|
+
* feature_repo derivation — `feature_service ⋈ services.primary_repo_id`.
|
|
55
|
+
*
|
|
56
|
+
* The same shape as repo_capability's Path X one rung down the hierarchy: a
|
|
57
|
+
* feature implemented by a service is implemented in the repo that builds that
|
|
58
|
+
* service. Added per D8 of the catalogue-coverage-invariants work, and only
|
|
59
|
+
* become POSSIBLE once the app-product Feature Services table was wired into
|
|
60
|
+
* feature_service (0.115.0) — before that the join's left side was empty in the
|
|
61
|
+
* profile where feature_repo had no source at all.
|
|
62
|
+
*
|
|
63
|
+
* ⚠ IT KEYS ON `primary_repo_id`, A DECLARED FK — NOT ON THE `service.id ==
|
|
64
|
+
* repo.id` IDENTITY. That distinction is why M-ADR 0197 does not endanger it:
|
|
65
|
+
* 0197 breaks the identity, and Phase A explicitly preserves primary_repo_id on
|
|
66
|
+
* every declared entry. Path X is identity-VALUED today, not
|
|
67
|
+
* identity-DEPENDENT, and so is this. A first review of D8 flagged the opposite
|
|
68
|
+
* and was wrong.
|
|
69
|
+
*
|
|
70
|
+
* Caller supplies `existingIds` so this only ever ADDS: the ADR 0143
|
|
71
|
+
* feature-grounding extractor and the capability-spec walker own their rows and
|
|
72
|
+
* are authoritative where they overlap.
|
|
73
|
+
*/
|
|
74
|
+
export function deriveFeatureRepoFromServices(inputs) {
|
|
75
|
+
const repoBySvc = new Map();
|
|
76
|
+
for (const s of inputs.serviceRows) {
|
|
77
|
+
const sid = String(s["id"] ?? "");
|
|
78
|
+
const rid = String(s["primary_repo_id"] ?? "");
|
|
79
|
+
if (sid !== "" && rid !== "")
|
|
80
|
+
repoBySvc.set(sid, rid);
|
|
81
|
+
}
|
|
82
|
+
const rows = [];
|
|
83
|
+
const seen = new Set(inputs.existingIds);
|
|
84
|
+
for (const fs of inputs.featureServiceRows) {
|
|
85
|
+
const featureId = String(fs["feature_id"] ?? "");
|
|
86
|
+
const svcId = String(fs["service_id"] ?? "");
|
|
87
|
+
if (featureId === "" || svcId === "")
|
|
88
|
+
continue;
|
|
89
|
+
const repoId = repoBySvc.get(svcId);
|
|
90
|
+
// A service with no primary_repo_id contributes nothing — silently, because
|
|
91
|
+
// repo_contract's Path X already reports that condition once per build and
|
|
92
|
+
// a second identical finding per feature would be noise, not signal.
|
|
93
|
+
if (repoId === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
// Never emit a dangling repo reference. Measured across two estates at the
|
|
96
|
+
// time of writing: 0 of 1,221 derived rows referenced an unresolvable
|
|
97
|
+
// primary_repo_id, so this guard is a safety net rather than a filter.
|
|
98
|
+
if (!inputs.knownRepoIds.has(repoId))
|
|
99
|
+
continue;
|
|
100
|
+
const id = `${featureId}__${repoId}`;
|
|
101
|
+
if (seen.has(id))
|
|
102
|
+
continue;
|
|
103
|
+
seen.add(id);
|
|
104
|
+
rows.push({ id, feature_id: featureId, repo_id: repoId });
|
|
105
|
+
}
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
export function buildRepoEdges(inputs) {
|
|
109
|
+
const repoCapability = collectRepoCapability(inputs);
|
|
110
|
+
const repoContract = collectRepoContract(inputs);
|
|
111
|
+
// feature_repo is populated later in build.ts by the ADR 0143
|
|
112
|
+
// feature-grounding extractor; this builder doesn't contribute.
|
|
113
|
+
const featureRepo = [];
|
|
114
|
+
return { repoCapability, repoContract, featureRepo };
|
|
115
|
+
}
|
|
116
|
+
function collectRepoCapability(inputs) {
|
|
117
|
+
const rows = [];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
const serviceRows = inputs.serviceRows ?? [];
|
|
120
|
+
const capabilityServiceRows = inputs.capabilityServiceRows ?? [];
|
|
121
|
+
const capabilityFeatureRows = inputs.capabilityFeatureRows ?? [];
|
|
122
|
+
const featureRepoRows = inputs.featureRepoRows ?? [];
|
|
123
|
+
const knownRepoIds = inputs.knownRepoIds;
|
|
124
|
+
const emit = (repoId, capId) => {
|
|
125
|
+
if (knownRepoIds !== undefined && !knownRepoIds.has(repoId))
|
|
126
|
+
return;
|
|
127
|
+
const id = `${repoId}__${capId}`;
|
|
128
|
+
if (seen.has(id))
|
|
129
|
+
return;
|
|
130
|
+
seen.add(id);
|
|
131
|
+
rows.push({ id, repo_id: repoId, capability_id: capId });
|
|
132
|
+
};
|
|
133
|
+
// Forward path A — declared in repo-config `capabilities[]` (M-ADR 0188 §6.1).
|
|
134
|
+
//
|
|
135
|
+
// This block exists because the older `additional_context[]` path below,
|
|
136
|
+
// though it works, cannot be validated: the field is multi-purpose, so an
|
|
137
|
+
// entry that resolves to no capability is indistinguishable from an ADR
|
|
138
|
+
// reference and must be skipped in silence. A typo'd capability path is
|
|
139
|
+
// therefore undetectable there — which is why, on the reference estate,
|
|
140
|
+
// 0 of 377 repo-configs used it to declare a capability and 264 repos
|
|
141
|
+
// resolved to nothing.
|
|
142
|
+
//
|
|
143
|
+
// `capabilities[]` is single-purpose. Every entry is an assertion that this
|
|
144
|
+
// repository realises that capability, so a non-match is an authoring error
|
|
145
|
+
// and is reported as one. Same edge, same table; the difference is that this
|
|
146
|
+
// surface can tell you when you got it wrong.
|
|
147
|
+
for (const cfg of inputs.configs) {
|
|
148
|
+
for (const rawEntry of cfg.data.capabilities ?? []) {
|
|
149
|
+
const cleaned = rawEntry.replace(/^\/+/, "").trim();
|
|
150
|
+
if (cleaned === "")
|
|
151
|
+
continue;
|
|
152
|
+
const capId = inputs.capabilityIdByPath.get(cleaned);
|
|
153
|
+
if (capId === undefined) {
|
|
154
|
+
inputs.findings.add("orphan", `${cfg.relPath}: capabilities entry "${cleaned}" did not match any capability spec_path (per M-ADR 0188 §6.1)`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
emit(cfg.repoId, capId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Forward path B (legacy) — declared in repo-config additional_context[].
|
|
161
|
+
for (const cfg of inputs.configs) {
|
|
162
|
+
const extras = cfg.data.additional_context ?? [];
|
|
163
|
+
for (const raw of extras) {
|
|
164
|
+
const reconstructed = normaliseAdditionalContextEntry(raw);
|
|
165
|
+
if (reconstructed === null)
|
|
166
|
+
continue;
|
|
167
|
+
const cleaned = reconstructed.replace(/^\/+/, "");
|
|
168
|
+
const capId = inputs.capabilityIdByPath.get(cleaned);
|
|
169
|
+
if (capId === undefined) {
|
|
170
|
+
// Not a capability spec path (likely an ADR file or freeform doc).
|
|
171
|
+
// additional_context is intentionally multi-purpose; only matches
|
|
172
|
+
// that resolve to capabilities count. Cannot warn here — see above.
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
emit(cfg.repoId, capId);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Reverse path X (universal) — capability_service ⋈ services.primary_repo_id.
|
|
179
|
+
const repoBySvcId = new Map();
|
|
180
|
+
for (const s of serviceRows) {
|
|
181
|
+
const sid = String(s["id"]);
|
|
182
|
+
const repoId = String(s["primary_repo_id"] ?? "");
|
|
183
|
+
if (sid !== "" && repoId !== "")
|
|
184
|
+
repoBySvcId.set(sid, repoId);
|
|
185
|
+
}
|
|
186
|
+
// M-ADR 0157 §4.3 (amended): `repo_capability` means "repo CONTRIBUTES to
|
|
187
|
+
// capability". A capability_service row with role `dependency` says the
|
|
188
|
+
// capability DEPENDS ON the service — the inverse direction — so its repo
|
|
189
|
+
// does not contribute and Path X skips it. `primary`, `related` and
|
|
190
|
+
// `backend-gateway` all participate in delivering the capability and stay.
|
|
191
|
+
// Measured before the filter: 62% of one estate's repo_capability rows and
|
|
192
|
+
// one row of another's rested on non-primary roles; `related` was realising
|
|
193
|
+
// in every measured use, so only `dependency` is excluded.
|
|
194
|
+
let dependencyRowsSkipped = 0;
|
|
195
|
+
for (const cs of capabilityServiceRows) {
|
|
196
|
+
const svcId = String(cs["service_id"] ?? "");
|
|
197
|
+
const capId = String(cs["capability_id"] ?? "");
|
|
198
|
+
if (svcId === "" || capId === "")
|
|
199
|
+
continue;
|
|
200
|
+
if (String(cs["role"] ?? "").trim() === "dependency") {
|
|
201
|
+
dependencyRowsSkipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const repoId = repoBySvcId.get(svcId);
|
|
205
|
+
if (repoId === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
emit(repoId, capId);
|
|
208
|
+
}
|
|
209
|
+
if (dependencyRowsSkipped > 0) {
|
|
210
|
+
inputs.findings.add("coverage", `repo_capability: ${dependencyRowsSkipped} capability_service rows with role "dependency" excluded from Path X — a dependency is the inverse direction of "repo contributes to capability" (M-ADR 0157 §4.3 as amended); the relationship itself is unchanged in capability_service`);
|
|
211
|
+
}
|
|
212
|
+
// Surface a single summary finding when capability_service rows exist with
|
|
213
|
+
// services lacking primary_repo_id. The walker quietly skips those edges;
|
|
214
|
+
// this finding tells the reader why the universal path missed some.
|
|
215
|
+
let missingPrimaryRepoSvcs = 0;
|
|
216
|
+
for (const cs of capabilityServiceRows) {
|
|
217
|
+
const svcId = String(cs["service_id"] ?? "");
|
|
218
|
+
if (svcId === "")
|
|
219
|
+
continue;
|
|
220
|
+
if (!repoBySvcId.has(svcId))
|
|
221
|
+
missingPrimaryRepoSvcs++;
|
|
222
|
+
}
|
|
223
|
+
if (missingPrimaryRepoSvcs > 0) {
|
|
224
|
+
inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.3)`);
|
|
225
|
+
}
|
|
226
|
+
// Reverse path Y — capability_feature ⋈ feature_repo (transitive via ADR 0143).
|
|
227
|
+
const featuresToRepos = new Map();
|
|
228
|
+
for (const fr of featureRepoRows) {
|
|
229
|
+
const fid = String(fr["feature_id"] ?? "");
|
|
230
|
+
const rid = String(fr["repo_id"] ?? "");
|
|
231
|
+
if (fid === "" || rid === "")
|
|
232
|
+
continue;
|
|
233
|
+
if (!featuresToRepos.has(fid))
|
|
234
|
+
featuresToRepos.set(fid, new Set());
|
|
235
|
+
featuresToRepos.get(fid).add(rid);
|
|
236
|
+
}
|
|
237
|
+
for (const cf of capabilityFeatureRows) {
|
|
238
|
+
const fid = String(cf["feature_id"] ?? "");
|
|
239
|
+
const cid = String(cf["capability_id"] ?? "");
|
|
240
|
+
if (fid === "" || cid === "")
|
|
241
|
+
continue;
|
|
242
|
+
const repos = featuresToRepos.get(fid);
|
|
243
|
+
if (!repos)
|
|
244
|
+
continue;
|
|
245
|
+
for (const repoId of repos)
|
|
246
|
+
emit(repoId, cid);
|
|
247
|
+
}
|
|
248
|
+
return rows;
|
|
249
|
+
}
|
|
250
|
+
function collectRepoContract(inputs) {
|
|
251
|
+
const rows = [];
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
const contractRows = inputs.contractRows ?? [];
|
|
254
|
+
const serviceRows = inputs.serviceRows ?? [];
|
|
255
|
+
const serviceContractRows = inputs.serviceContractRows ?? [];
|
|
256
|
+
const knownRepoIds = inputs.knownRepoIds;
|
|
257
|
+
const emit = (repoId, contractId) => {
|
|
258
|
+
if (knownRepoIds !== undefined && !knownRepoIds.has(repoId))
|
|
259
|
+
return;
|
|
260
|
+
const id = `${repoId}__${contractId}`;
|
|
261
|
+
if (seen.has(id))
|
|
262
|
+
return;
|
|
263
|
+
seen.add(id);
|
|
264
|
+
rows.push({ id, repo_id: repoId, contract_id: contractId });
|
|
265
|
+
};
|
|
266
|
+
// Forward path — declared in repo-config contracts.{openapi,proto,avro}[].
|
|
267
|
+
// Pre-compute suffix and basename indexes for fuzzy matching.
|
|
268
|
+
const suffixIndex = new Map();
|
|
269
|
+
const basenameIndex = new Map();
|
|
270
|
+
for (const [path, id] of inputs.contractIdByPath) {
|
|
271
|
+
const parts = path.split(sep);
|
|
272
|
+
if (parts.length >= 2) {
|
|
273
|
+
suffixIndex.set(parts.slice(-2).join("/"), id);
|
|
274
|
+
}
|
|
275
|
+
basenameIndex.set(basename(path), id);
|
|
276
|
+
}
|
|
277
|
+
for (const cfg of inputs.configs) {
|
|
278
|
+
const block = cfg.data.contracts;
|
|
279
|
+
if (!block)
|
|
280
|
+
continue;
|
|
281
|
+
for (const kind of ["openapi", "proto", "avro"]) {
|
|
282
|
+
const list = block[kind] ?? [];
|
|
283
|
+
for (const rawEntry of list) {
|
|
284
|
+
const entry = rawEntry.replace(/^\/+/, "");
|
|
285
|
+
const fullCandidate = `contracts/${kind}/${entry}`;
|
|
286
|
+
const id = inputs.contractIdByPath.get(fullCandidate)
|
|
287
|
+
?? inputs.contractIdByPath.get(entry)
|
|
288
|
+
?? suffixIndex.get(fullCandidate)
|
|
289
|
+
?? suffixIndex.get(entry)
|
|
290
|
+
?? basenameIndex.get(basename(entry));
|
|
291
|
+
if (id === undefined) {
|
|
292
|
+
inputs.findings.add("orphan", `${cfg.relPath}: contracts.${kind} entry "${entry}" did not match any contract`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
emit(cfg.repoId, id);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Reverse path X (universal) — service_contract ⋈ services.primary_repo_id.
|
|
300
|
+
// A service exposing a contract is implemented by a repo; the repo "owns"
|
|
301
|
+
// the contract by transitive inference. Added per ADR 0157 §4.2 amendment
|
|
302
|
+
// (an early adopter's source_repo_id was 0/30 populated, so
|
|
303
|
+
// a derivation that depends only on it leaves them empty).
|
|
304
|
+
const repoBySvcId = new Map();
|
|
305
|
+
for (const s of serviceRows) {
|
|
306
|
+
const sid = String(s["id"]);
|
|
307
|
+
const repoId = String(s["primary_repo_id"] ?? "");
|
|
308
|
+
if (sid !== "" && repoId !== "")
|
|
309
|
+
repoBySvcId.set(sid, repoId);
|
|
310
|
+
}
|
|
311
|
+
let missingPrimaryRepoSvcs = 0;
|
|
312
|
+
for (const sc of serviceContractRows) {
|
|
313
|
+
const svcId = String(sc["service_id"] ?? "");
|
|
314
|
+
const ctrId = String(sc["contract_id"] ?? "");
|
|
315
|
+
if (svcId === "" || ctrId === "")
|
|
316
|
+
continue;
|
|
317
|
+
const repoId = repoBySvcId.get(svcId);
|
|
318
|
+
if (repoId === undefined) {
|
|
319
|
+
missingPrimaryRepoSvcs++;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
emit(repoId, ctrId);
|
|
323
|
+
}
|
|
324
|
+
if (missingPrimaryRepoSvcs > 0) {
|
|
325
|
+
inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.2)`);
|
|
326
|
+
}
|
|
327
|
+
// Reverse path Y — contracts.source_repo_id denormalisation.
|
|
328
|
+
// Adopters who carry OpenAPI x-source-repo hints (per ADR 0110) or whose
|
|
329
|
+
// contracts builder otherwise populates source_repo_id get one row per
|
|
330
|
+
// (source_repo_id, contract.id) for free.
|
|
331
|
+
for (const c of contractRows) {
|
|
332
|
+
const cid = String(c["id"]);
|
|
333
|
+
const srcRepo = String(c["source_repo_id"] ?? "");
|
|
334
|
+
if (cid === "" || srcRepo === "")
|
|
335
|
+
continue;
|
|
336
|
+
emit(srcRepo, cid);
|
|
337
|
+
}
|
|
338
|
+
return rows;
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=repo-edges.js.map
|