@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,402 @@
|
|
|
1
|
+
// Contracts discoverer — emits `contracts.csv` rows from
|
|
2
|
+
// `contracts/{openapi,proto,avro}/<domain>/<name>.<ext>` (canonical) and
|
|
3
|
+
// `docs/domains/<domain>/contracts/{openapi,proto,avro}/<name>.<ext>`
|
|
4
|
+
// (platform-profile per reverse/workflows/contract.md).
|
|
5
|
+
//
|
|
6
|
+
// Per the canonical schema (catalogue-schema.yaml):
|
|
7
|
+
// - kind enum: rest | graphql | rpc | event
|
|
8
|
+
// - openapi/*.yaml → rest
|
|
9
|
+
// - proto/*.proto → rpc
|
|
10
|
+
// - avro/*.avsc → event
|
|
11
|
+
// - service_id and source_repo_id are nullable; per ADR 0110 the real
|
|
12
|
+
// contract sources may live in sibling contracts-* repos. The
|
|
13
|
+
// canonical builder leaves these empty by default; an OpenAPI
|
|
14
|
+
// `x-source-repo` extension is honoured as a hint when present
|
|
15
|
+
// (resolved against the known repository set).
|
|
16
|
+
// - For OpenAPI files only, `info.title` becomes `description`,
|
|
17
|
+
// `info.version` becomes `version`. Other formats keep these empty.
|
|
18
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
19
|
+
import { extname, join, relative, sep } from "node:path";
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
import { contractIdWithKind } from "../contract-id.js";
|
|
22
|
+
const CONTRACTS_DIR_REL = "contracts";
|
|
23
|
+
const DOCS_DOMAINS_DIR_REL = join("docs", "domains");
|
|
24
|
+
const KIND_SPECS = [
|
|
25
|
+
{ subdir: "openapi", extensions: [".yaml", ".yml"], kind: "rest" },
|
|
26
|
+
{ subdir: "proto", extensions: [".proto"], kind: "rpc" },
|
|
27
|
+
{ subdir: "avro", extensions: [".avsc"], kind: "event" },
|
|
28
|
+
// `authz` — authorization/policy schemas (SpiceDB `.zed` and equivalents).
|
|
29
|
+
//
|
|
30
|
+
// Added because an adopter's contract lock-in gate APPROVED two `.zed` schemas
|
|
31
|
+
// as contracts ("all 11 contracts lock"), while `kind` had no member able to
|
|
32
|
+
// represent them — so the catalogue could not hold an artefact the method's own
|
|
33
|
+
// gate had certified. That gate had already recorded the tension itself, passing
|
|
34
|
+
// SpiceDB "with-waiver on rubric-fit grounds"; this closes the same gap one
|
|
35
|
+
// layer down.
|
|
36
|
+
//
|
|
37
|
+
// Zero-delta when added: no estate holds an in-repo `contracts/spicedb/` tree,
|
|
38
|
+
// so this discovers nothing today and exists so that a declared or migrated
|
|
39
|
+
// authz schema HAS a kind rather than being silently unrepresentable.
|
|
40
|
+
{ subdir: "spicedb", extensions: [".zed"], kind: "authz" },
|
|
41
|
+
// `graphql` — GraphQL SDL. Added last of the five, and the reason it was last is
|
|
42
|
+
// the finding: `graphql` had been a member of the `kind` enum since the enum
|
|
43
|
+
// existed, while NO code path could ever produce it. Not discovered here, not
|
|
44
|
+
// mapped from a manifest key, not extracted by the loader, not walked by the
|
|
45
|
+
// validator, and not offered by the template. **A vocabulary member unreachable
|
|
46
|
+
// by construction** — the same class as a rubric anchor no artefact can satisfy,
|
|
47
|
+
// and it stayed invisible for the same reason: an unreachable value is emitted
|
|
48
|
+
// for nothing and so never complains.
|
|
49
|
+
//
|
|
50
|
+
// NOT zero-delta, unlike spicedb: the reverse estate already holds 7 `.graphql`
|
|
51
|
+
// files under the conventional `docs/domains/<domain>/contracts/graphql/` layout
|
|
52
|
+
// (vehicle 5, applications 2), invisible to the catalogue until now. An author
|
|
53
|
+
// whose GraphQL surface could not be keyed documented the wall in their own
|
|
54
|
+
// manifest — naming the schema lines that blocked them — and wrote the only
|
|
55
|
+
// token left in `contracts.status`.
|
|
56
|
+
{ subdir: "graphql", extensions: [".graphql", ".graphqls"], kind: "graphql" },
|
|
57
|
+
];
|
|
58
|
+
export async function buildContracts(inputs) {
|
|
59
|
+
const rows = [];
|
|
60
|
+
const idByPath = new Map();
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
// Contract deprecation source (schema 2.22.0): BASELINE.md deprecation-
|
|
63
|
+
// runway entries per ADR 0073 §4.8 — the same source
|
|
64
|
+
// validate-deprecation-runway.sh enforces. Absent file = no deprecations.
|
|
65
|
+
const deprecatedPaths = await readBaselineDeprecations(inputs.sddPath, inputs.findings);
|
|
66
|
+
const discovered = [];
|
|
67
|
+
// Layout 1: canonical SDD-root layout — contracts/<subdir>/<domain>/<file>.<ext>.
|
|
68
|
+
for (const spec of KIND_SPECS) {
|
|
69
|
+
const dir = join(inputs.sddPath, CONTRACTS_DIR_REL, spec.subdir);
|
|
70
|
+
const files = await collectFiles(dir, spec.extensions);
|
|
71
|
+
for (const path of files) {
|
|
72
|
+
const rel = relative(inputs.sddPath, path);
|
|
73
|
+
discovered.push({
|
|
74
|
+
absPath: path,
|
|
75
|
+
relPath: rel,
|
|
76
|
+
domain: domainSegmentCanonical(rel),
|
|
77
|
+
kind: spec.kind,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Layout 2: platform-profile layout — docs/domains/<domain>/contracts/<subdir>/<file>.<ext>.
|
|
82
|
+
// Canonical for platform-profile SDDs per reverse/workflows/contract.md and
|
|
83
|
+
// reverse/workflow.md §3 ("docs/domains/{domain}/contracts/openapi/*.yaml ←
|
|
84
|
+
// refresh API surfaces"). Federated SDDs without docs/domains/ harmlessly
|
|
85
|
+
// contribute zero rows.
|
|
86
|
+
const domainsRoot = join(inputs.sddPath, DOCS_DOMAINS_DIR_REL);
|
|
87
|
+
const domainDirs = await listDirs(domainsRoot);
|
|
88
|
+
for (const domainName of domainDirs) {
|
|
89
|
+
for (const spec of KIND_SPECS) {
|
|
90
|
+
const dir = join(domainsRoot, domainName, CONTRACTS_DIR_REL, spec.subdir);
|
|
91
|
+
const files = await collectFiles(dir, spec.extensions);
|
|
92
|
+
for (const path of files) {
|
|
93
|
+
const rel = relative(inputs.sddPath, path);
|
|
94
|
+
discovered.push({
|
|
95
|
+
absPath: path,
|
|
96
|
+
relPath: rel,
|
|
97
|
+
domain: domainName,
|
|
98
|
+
kind: spec.kind,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
discovered.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
104
|
+
for (const entry of discovered) {
|
|
105
|
+
const filename = basenameOf(entry.absPath);
|
|
106
|
+
// Skip schema/config files at any depth.
|
|
107
|
+
if (filename === "schema.yaml" || filename === "schema.yml")
|
|
108
|
+
continue;
|
|
109
|
+
const stem = stemOf(filename);
|
|
110
|
+
// Schema 3.0.0 (M-ADR 0203 §4.4): the id carries its kind component,
|
|
111
|
+
// so one service published as two contract families (openapi + proto)
|
|
112
|
+
// mints two rows instead of colliding — the measured 2.x standing cost
|
|
113
|
+
// this format exists to end. A remaining collision is same-kind and
|
|
114
|
+
// still reported.
|
|
115
|
+
const kindless = entry.domain !== ""
|
|
116
|
+
? slugify(`${entry.domain}-${stem}`)
|
|
117
|
+
: slugify(stem);
|
|
118
|
+
const slug = contractIdWithKind(kindless, entry.kind);
|
|
119
|
+
if (seen.has(slug)) {
|
|
120
|
+
inputs.findings.add("divergence", `${entry.relPath}: contract id "${slug}" already emitted by an earlier file`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
seen.add(slug);
|
|
124
|
+
let description = "";
|
|
125
|
+
let version = "";
|
|
126
|
+
let sourceRepoId = "";
|
|
127
|
+
let context = "";
|
|
128
|
+
let evidenceClass = "";
|
|
129
|
+
if (entry.kind === "rest") {
|
|
130
|
+
const meta = await tryReadOpenapiInfo(entry.absPath);
|
|
131
|
+
description = meta.description;
|
|
132
|
+
version = meta.version;
|
|
133
|
+
context = meta.context;
|
|
134
|
+
evidenceClass = meta.evidenceClass;
|
|
135
|
+
if (meta.xSourceRepo !== "" && inputs.knownRepositoryIds.has(meta.xSourceRepo)) {
|
|
136
|
+
sourceRepoId = meta.xSourceRepo;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const normalisedRelPath = entry.relPath.split(sep).join("/");
|
|
140
|
+
rows.push({
|
|
141
|
+
id: slug,
|
|
142
|
+
name: stem,
|
|
143
|
+
service_id: "",
|
|
144
|
+
source_repo_id: sourceRepoId,
|
|
145
|
+
kind: entry.kind,
|
|
146
|
+
version,
|
|
147
|
+
status: deprecatedPaths.has(normalisedRelPath) ? "deprecated" : "active",
|
|
148
|
+
spec_path: entry.relPath,
|
|
149
|
+
visibility: "internal",
|
|
150
|
+
context,
|
|
151
|
+
evidence_class: evidenceClass,
|
|
152
|
+
description,
|
|
153
|
+
discovery: "discovered",
|
|
154
|
+
source_sdd_id: "",
|
|
155
|
+
});
|
|
156
|
+
idByPath.set(entry.relPath, slug);
|
|
157
|
+
}
|
|
158
|
+
// A deprecated BASELINE.md entry that matched no discovered spec is doc
|
|
159
|
+
// drift (path typo, file moved, or already removed) — surface it; the
|
|
160
|
+
// runway gate enforces file existence separately.
|
|
161
|
+
const emittedPaths = new Set(rows.map((r) => String(r["spec_path"]).split(sep).join("/")));
|
|
162
|
+
for (const path of deprecatedPaths) {
|
|
163
|
+
if (!emittedPaths.has(path)) {
|
|
164
|
+
inputs.findings.add("divergence", `contracts: BASELINE.md deprecates "${path}" but no discovered contract spec matches that path`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { rows, idByPath };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Read deprecated contract paths from BASELINE.md at the SDD root.
|
|
171
|
+
*
|
|
172
|
+
* Shape per ADR 0073 §4.8 (the deprecation-runway gate's source):
|
|
173
|
+
* contracts:
|
|
174
|
+
* openapi: [ { path, status, sunset_date? }, ... ]
|
|
175
|
+
* proto: [ ... ]
|
|
176
|
+
* avro: [ ... ]
|
|
177
|
+
*
|
|
178
|
+
* Returns the set of `path` values (forward-slash, SDD-root-relative) whose
|
|
179
|
+
* status is "deprecated". Missing file → empty set (pre-go-live SDDs have no
|
|
180
|
+
* BASELINE.md). Unparseable file → empty set with a divergence finding.
|
|
181
|
+
*/
|
|
182
|
+
async function readBaselineDeprecations(sddPath, findings) {
|
|
183
|
+
const baselinePath = join(sddPath, "BASELINE.md");
|
|
184
|
+
let text;
|
|
185
|
+
try {
|
|
186
|
+
text = await readFile(baselinePath, "utf-8");
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const code = err.code;
|
|
190
|
+
if (code === "ENOENT")
|
|
191
|
+
return new Set();
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
let raw;
|
|
195
|
+
try {
|
|
196
|
+
raw = parseYaml(text);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
findings.add("divergence", `contracts: BASELINE.md exists but is not parseable YAML — contract deprecation statuses not wired`);
|
|
200
|
+
return new Set();
|
|
201
|
+
}
|
|
202
|
+
const deprecated = new Set();
|
|
203
|
+
const contracts = raw?.["contracts"];
|
|
204
|
+
if (!contracts || typeof contracts !== "object")
|
|
205
|
+
return deprecated;
|
|
206
|
+
for (const typeKey of ["openapi", "proto", "avro"]) {
|
|
207
|
+
const entries = contracts[typeKey];
|
|
208
|
+
if (!Array.isArray(entries))
|
|
209
|
+
continue;
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (!entry || typeof entry !== "object")
|
|
212
|
+
continue;
|
|
213
|
+
const e = entry;
|
|
214
|
+
if (String(e["status"] ?? "") !== "deprecated")
|
|
215
|
+
continue;
|
|
216
|
+
const path = String(e["path"] ?? "").trim();
|
|
217
|
+
if (path !== "")
|
|
218
|
+
deprecated.add(path);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return deprecated;
|
|
222
|
+
}
|
|
223
|
+
async function listDirs(parent) {
|
|
224
|
+
try {
|
|
225
|
+
const entries = await readdir(parent, { withFileTypes: true });
|
|
226
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
const code = err.code;
|
|
230
|
+
if (code === "ENOENT")
|
|
231
|
+
return [];
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function tryReadOpenapiInfo(path) {
|
|
236
|
+
try {
|
|
237
|
+
const text = await readFile(path, "utf-8");
|
|
238
|
+
const raw = parseYaml(text);
|
|
239
|
+
if (!raw || typeof raw !== "object") {
|
|
240
|
+
return { description: "", version: "", xSourceRepo: "", context: "", evidenceClass: "" };
|
|
241
|
+
}
|
|
242
|
+
const info = raw["info"];
|
|
243
|
+
const xSourceRepo = String(raw["x-source-repo"] ?? "").trim();
|
|
244
|
+
// `x-context` may sit on info (preferred) or at the document root.
|
|
245
|
+
const contextRaw = info?.["x-context"] ?? raw["x-context"];
|
|
246
|
+
const context = typeof contextRaw === "string" ? contextRaw.trim() : "";
|
|
247
|
+
// `x-evidence-class` mirrors x-context: info (preferred), document root fallback.
|
|
248
|
+
const evidenceRaw = info?.["x-evidence-class"] ?? raw["x-evidence-class"];
|
|
249
|
+
const evidenceClass = typeof evidenceRaw === "string" ? evidenceRaw.trim() : "";
|
|
250
|
+
const versionRaw = info?.["version"];
|
|
251
|
+
const descRaw = info?.["description"];
|
|
252
|
+
const description = typeof descRaw === "string"
|
|
253
|
+
? firstSentence(descRaw)
|
|
254
|
+
: "";
|
|
255
|
+
const version = typeof versionRaw === "string"
|
|
256
|
+
? versionRaw
|
|
257
|
+
: versionRaw !== undefined
|
|
258
|
+
? String(versionRaw)
|
|
259
|
+
: "";
|
|
260
|
+
return { description, version, xSourceRepo, context, evidenceClass };
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Not all *.yaml files in contracts/openapi/ are full specs (some are
|
|
264
|
+
// shared component fragments). Failing to parse is not an error here.
|
|
265
|
+
return { description: "", version: "", xSourceRepo: "", context: "", evidenceClass: "" };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function firstSentence(s) {
|
|
269
|
+
// Take up to the first paragraph break, capped at 500 chars.
|
|
270
|
+
const para = s.split(/\r?\n\r?\n/)[0] ?? "";
|
|
271
|
+
return para.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
272
|
+
}
|
|
273
|
+
async function collectFiles(dir, extensions) {
|
|
274
|
+
const out = [];
|
|
275
|
+
let entries;
|
|
276
|
+
try {
|
|
277
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
const code = err.code;
|
|
281
|
+
if (code === "ENOENT")
|
|
282
|
+
return [];
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const full = join(dir, entry.name);
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
out.push(...(await collectFiles(full, extensions)));
|
|
289
|
+
}
|
|
290
|
+
else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
291
|
+
out.push(full);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
function basenameOf(absPath) {
|
|
297
|
+
return absPath.split(sep).slice(-1)[0] ?? "";
|
|
298
|
+
}
|
|
299
|
+
function stemOf(filename) {
|
|
300
|
+
return filename.replace(/\.[^.]+$/, "");
|
|
301
|
+
}
|
|
302
|
+
function domainSegmentCanonical(relPath) {
|
|
303
|
+
// Canonical layout: contracts/<subdir>/<domain>/<file>.<ext>
|
|
304
|
+
// → parts = ["contracts", subdir, domain, file]
|
|
305
|
+
const parts = relPath.split(sep);
|
|
306
|
+
if (parts.length < 4)
|
|
307
|
+
return "";
|
|
308
|
+
return parts[2] ?? "";
|
|
309
|
+
}
|
|
310
|
+
function slugify(s) {
|
|
311
|
+
return s
|
|
312
|
+
.toLowerCase()
|
|
313
|
+
.replace(/^_+/, "") // turn _common into common
|
|
314
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
315
|
+
.replace(/^-+|-+$/g, "");
|
|
316
|
+
}
|
|
317
|
+
const DECLARED_KINDS = {
|
|
318
|
+
openapi: "rest",
|
|
319
|
+
proto: "rpc",
|
|
320
|
+
avro: "event",
|
|
321
|
+
spicedb: "authz",
|
|
322
|
+
graphql: "graphql",
|
|
323
|
+
};
|
|
324
|
+
export function buildDeclaredExternalContracts(inputs) {
|
|
325
|
+
const rows = [];
|
|
326
|
+
const idByPath = new Map();
|
|
327
|
+
const emitted = new Set(inputs.existingIds);
|
|
328
|
+
// Every declared path processed so far — WINNERS AND LOSERS BOTH. A
|
|
329
|
+
// re-declaration of a path already processed is two capabilities citing
|
|
330
|
+
// one contract: a no-op whatever the first outcome was (row emitted, or
|
|
331
|
+
// collision already reported once). The first fix registered only paths
|
|
332
|
+
// that EMITTED, so a path that LOST a slug collision was never recorded
|
|
333
|
+
// and re-collided afresh in every manifest citing it — measured: the one
|
|
334
|
+
// genuine collision reported twice, once per citing manifest. A finding
|
|
335
|
+
// is a fact about the contract, not about each reader of it.
|
|
336
|
+
const seenDeclaredPaths = new Set();
|
|
337
|
+
const localBasenames = new Set([...inputs.discoveredIdByPath.keys()].map((p) => basenameOf(p.split("/").join(sep))));
|
|
338
|
+
for (const m of inputs.manifests) {
|
|
339
|
+
if (!m.contracts)
|
|
340
|
+
continue;
|
|
341
|
+
for (const [blockKey, entries] of Object.entries(m.contracts)) {
|
|
342
|
+
const kind = DECLARED_KINDS[blockKey];
|
|
343
|
+
if (kind === undefined || !Array.isArray(entries))
|
|
344
|
+
continue;
|
|
345
|
+
for (const raw of entries) {
|
|
346
|
+
// Only an OBJECT entry carrying `external: true` qualifies. A bare
|
|
347
|
+
// string is the pre-D13 form and must keep its existing behaviour
|
|
348
|
+
// (resolve locally, or drop with a finding) — otherwise every
|
|
349
|
+
// unresolvable typo in a contracts_root estate becomes a row.
|
|
350
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
351
|
+
continue;
|
|
352
|
+
const entry = raw;
|
|
353
|
+
if (entry["external"] !== true)
|
|
354
|
+
continue;
|
|
355
|
+
const declared = String(entry["path"] ?? "").replace(/^\/+/, "");
|
|
356
|
+
if (declared === "")
|
|
357
|
+
continue;
|
|
358
|
+
const base = basenameOf(declared.split("/").join(sep));
|
|
359
|
+
// If it resolves locally it is not external — the discovered row wins
|
|
360
|
+
// and carries real parsed metadata.
|
|
361
|
+
if (inputs.discoveredIdByPath.has(declared) || localBasenames.has(base))
|
|
362
|
+
continue;
|
|
363
|
+
const stem = stemOf(base);
|
|
364
|
+
const kindless = m.domain !== "" ? slugify(`${m.domain}-${stem}`) : slugify(stem);
|
|
365
|
+
const slug = contractIdWithKind(kindless, kind);
|
|
366
|
+
if (seenDeclaredPaths.has(declared)) {
|
|
367
|
+
// Same path, same contract: legitimate cross-capability sharing.
|
|
368
|
+
// Whatever happened on first sight (row, or collision reported)
|
|
369
|
+
// already happened once; a second citation adds no information.
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
seenDeclaredPaths.add(declared);
|
|
373
|
+
if (emitted.has(slug)) {
|
|
374
|
+
inputs.findings.add("divergence", `${m.relPath}: declared-external contract id "${slug}" (${declared}) already emitted — `
|
|
375
|
+
+ `a same-kind collision (the cross-family collision class ended at schema 3.0.0, `
|
|
376
|
+
+ `M-ADR 0203 §4.4); row dropped`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
emitted.add(slug);
|
|
380
|
+
idByPath.set(declared, slug);
|
|
381
|
+
rows.push({
|
|
382
|
+
id: slug,
|
|
383
|
+
name: stem,
|
|
384
|
+
service_id: "",
|
|
385
|
+
source_repo_id: "",
|
|
386
|
+
kind,
|
|
387
|
+
version: "",
|
|
388
|
+
status: "active",
|
|
389
|
+
spec_path: declared,
|
|
390
|
+
visibility: "",
|
|
391
|
+
context: "",
|
|
392
|
+
evidence_class: "",
|
|
393
|
+
description: "",
|
|
394
|
+
discovery: "declared-external",
|
|
395
|
+
source_sdd_id: "",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { rows, idByPath };
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=contracts.js.map
|