@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,488 @@
|
|
|
1
|
+
// Capability discoverer — emits `capabilities.csv` rows from
|
|
2
|
+
// traceability YAMLs (authoritative) plus capability spec files
|
|
3
|
+
// (fallback for specs not covered by a traceability).
|
|
4
|
+
//
|
|
5
|
+
// Per ADR 0134 and the catalogue-builder-specification §"Inputs":
|
|
6
|
+
// - Authoritative source: docs/domains/<domain>/product/<cap>.traceability.yaml
|
|
7
|
+
// declares capability.id (canonical), domain, capability_spec, runway.
|
|
8
|
+
// - Fallback: capability spec markdown files at
|
|
9
|
+
// docs/domains/<domain>/product/<cap>.md without an accompanying
|
|
10
|
+
// traceability YAML. Filename stem is used as the capability id.
|
|
11
|
+
// - When a traceability's capability.id differs from the spec file's
|
|
12
|
+
// stem, the canonical id wins; the divergence is logged as a finding.
|
|
13
|
+
import { readFile, stat } from "node:fs/promises";
|
|
14
|
+
import { join, relative, sep } from "node:path";
|
|
15
|
+
import { readdir } from "node:fs/promises";
|
|
16
|
+
const DOMAINS_DIR_REL = join("docs", "domains");
|
|
17
|
+
/**
|
|
18
|
+
* The vocabulary `catalogue-schema.yaml` documents for `capabilities.status`.
|
|
19
|
+
* WIDENED at catalogue schema 3.5.0 from the prior three-member set: the census
|
|
20
|
+
* this constant powers is what produced the evidence, and the ruling went the
|
|
21
|
+
* census's way — `exploration`, `placeholder` and `detailed-pre-lock` each VARY
|
|
22
|
+
* WITHIN a domain, which is the ruled discriminator for a genuine lifecycle
|
|
23
|
+
* phase rather than an authoring convention, so all three were admitted rather
|
|
24
|
+
* than re-authored away.
|
|
25
|
+
*
|
|
26
|
+
* Still held as the census's reference set, NOT as a binding — the column
|
|
27
|
+
* declares no `column_enums`, deliberately and stagedly (binding carries
|
|
28
|
+
* hard-fail teeth, and one out-of-vocabulary row remains at one estate).
|
|
29
|
+
*/
|
|
30
|
+
const DOCUMENTED_CAPABILITY_STATUS = new Set([
|
|
31
|
+
"active",
|
|
32
|
+
"planned",
|
|
33
|
+
"exploration",
|
|
34
|
+
"deprecated",
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* M-ADR 0201 SPEC-AUTHORING stages. Not lifecycle phases, and not this column's
|
|
38
|
+
* axis — they belong in the capability spec's own `**Status:**` header, gated
|
|
39
|
+
* there by the spec-lifecycle gate.
|
|
40
|
+
*
|
|
41
|
+
* Held separately so the census can say WHICH AXIS owns the value instead of
|
|
42
|
+
* reporting "unknown word". `placeholder` and `detailed-pre-lock` were briefly
|
|
43
|
+
* admitted into `capabilities.status` at catalogue schema 3.5.0 and retracted at
|
|
44
|
+
* 3.6.0: one column carrying two orthogonal axes cannot be read, because
|
|
45
|
+
* `placeholder` gave no way to tell whether the CAPABILITY was a placeholder or
|
|
46
|
+
* its DOCUMENT was.
|
|
47
|
+
*/
|
|
48
|
+
const SPEC_AUTHORING_STAGES = new Set([
|
|
49
|
+
"placeholder",
|
|
50
|
+
"first-draft",
|
|
51
|
+
"detailed-pre-lock",
|
|
52
|
+
"reverse-derived",
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* `traceability.contracts.provenance` — how the capability's contract SET was
|
|
56
|
+
* produced. Added at catalogue schema 3.5.0, reusing the vocabulary the method
|
|
57
|
+
* already ships for `authoring-mode` rather than minting a second one.
|
|
58
|
+
*
|
|
59
|
+
* STRICTLY ORTHOGONAL to `traceability.contracts.status`: a reverse-extracted
|
|
60
|
+
* contract set that is fully mapped declares `status: complete` AND
|
|
61
|
+
* `provenance: reverse-extracted`. Two reverse-estate manifests had written
|
|
62
|
+
* `reverse-extracted` into `contracts.status`, where it is invalid, because
|
|
63
|
+
* there was nowhere to say both true things at once.
|
|
64
|
+
*/
|
|
65
|
+
const DOCUMENTED_CONTRACT_PROVENANCE = new Set([
|
|
66
|
+
"hand-authored",
|
|
67
|
+
"reverse-extracted",
|
|
68
|
+
"generated",
|
|
69
|
+
]);
|
|
70
|
+
export async function buildCapabilities(inputs) {
|
|
71
|
+
const rows = [];
|
|
72
|
+
// Row identity for sliced capabilities is (id, slice_scope); use that as the
|
|
73
|
+
// dedup key so legitimately-sliced capabilities emit one row per slice
|
|
74
|
+
// without colliding on `id`.
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
// The manifest that WON each dedup key, kept so a later duplicate can be
|
|
77
|
+
// DIFFED against it rather than merely counted. See the divergence finding.
|
|
78
|
+
const keptBy = new Map();
|
|
79
|
+
const usedSpecPaths = new Set();
|
|
80
|
+
const sliceKey = (id, sliceScope) => sliceScope === "" ? id : `${id}::${sliceScope}`;
|
|
81
|
+
// Pass 1 — traceability-declared capabilities (authoritative).
|
|
82
|
+
for (const m of inputs.manifests) {
|
|
83
|
+
const cap = m.capability;
|
|
84
|
+
const slice = (cap.slice_scope ?? "").trim();
|
|
85
|
+
const dedupKey = sliceKey(cap.id, slice);
|
|
86
|
+
if (seen.has(dedupKey)) {
|
|
87
|
+
// ⚠ THE DISCARDED COPY'S CONTENT IS THE POINT, not the fact of duplication.
|
|
88
|
+
// This finding used to say only that a duplicate existed. An estate probe
|
|
89
|
+
// found SEVEN capabilities with two canonically-named manifests where the
|
|
90
|
+
// catalogue keeps the first-seen copy and discards the other — and for FOUR
|
|
91
|
+
// of them the score registry's file_path names the DISCARDED file. So a
|
|
92
|
+
// scorer read one file while the catalogue reported another, disagreeing on
|
|
93
|
+
// capability.status, exploration.uncertainty and runway.classification.
|
|
94
|
+
//
|
|
95
|
+
// A duplicate whose copies AGREE is harmless. One that disagrees means a
|
|
96
|
+
// consumer may have read the copy that was thrown away — and a wrong value
|
|
97
|
+
// in a discarded file emits no other finding, ever. So name the fields.
|
|
98
|
+
const kept = keptBy.get(dedupKey);
|
|
99
|
+
const diffs = [];
|
|
100
|
+
if (kept !== undefined) {
|
|
101
|
+
const pairs = [
|
|
102
|
+
["capability.status", (kept.capability.status ?? "").trim(), (cap.status ?? "").trim()],
|
|
103
|
+
[
|
|
104
|
+
"traceability.runway.classification",
|
|
105
|
+
(kept.traceability.runway?.classification ?? "").trim(),
|
|
106
|
+
(m.traceability.runway?.classification ?? "").trim(),
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
"capability.capability_spec",
|
|
110
|
+
(kept.capability.capability_spec ?? "").trim(),
|
|
111
|
+
(cap.capability_spec ?? "").trim(),
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
"capability.domain",
|
|
115
|
+
(kept.capability.domain ?? "").trim(),
|
|
116
|
+
(cap.domain ?? "").trim(),
|
|
117
|
+
],
|
|
118
|
+
];
|
|
119
|
+
for (const [field, keptVal, dropVal] of pairs) {
|
|
120
|
+
if (keptVal !== dropVal) {
|
|
121
|
+
diffs.push(`${field} kept="${keptVal}" discarded="${dropVal}"`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const head = slice === ""
|
|
126
|
+
? `${m.relPath}: capability.id="${cap.id}" already emitted by an earlier manifest`
|
|
127
|
+
: `${m.relPath}: capability.id="${cap.id}" slice="${slice}" already emitted by an earlier manifest`;
|
|
128
|
+
const keptPath = kept === undefined ? "an earlier manifest" : kept.relPath;
|
|
129
|
+
inputs.findings.add("divergence", diffs.length === 0
|
|
130
|
+
? `${head} (${keptPath}) — THIS FILE IS DISCARDED. The two copies agree on ` +
|
|
131
|
+
`status, runway classification, spec path and domain, so no consumer can be ` +
|
|
132
|
+
`misled by which one was kept; the duplication itself is still worth removing.`
|
|
133
|
+
: `${head} (${keptPath}) — THIS FILE IS DISCARDED AND THE COPIES DISAGREE: ` +
|
|
134
|
+
`${diffs.join("; ")}. Any consumer pointed at the discarded path (a score ` +
|
|
135
|
+
`registry file_path, a review, a link) read values the catalogue does not ` +
|
|
136
|
+
`report. Decide which copy survives per capability; do not bulk-delete, which ` +
|
|
137
|
+
`would publish the discarded values rather than remove them.`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!inputs.knownDomainIds.has(cap.domain)) {
|
|
141
|
+
inputs.findings.add("orphan", `${m.relPath}: capability.domain="${cap.domain}" has no docs/domains/${cap.domain}/`);
|
|
142
|
+
}
|
|
143
|
+
const specRel = cap.capability_spec ?? "";
|
|
144
|
+
let name = titleCase(cap.id);
|
|
145
|
+
let description = "";
|
|
146
|
+
if (specRel) {
|
|
147
|
+
usedSpecPaths.add(specRel);
|
|
148
|
+
const specAbs = join(inputs.sddPath, specRel);
|
|
149
|
+
// F22: an emitted spec_path must exist — a dangling path silently
|
|
150
|
+
// produced an empty name/description while the column pointed at
|
|
151
|
+
// nothing. The row still emits (the capability is declared); the
|
|
152
|
+
// dangle is loud.
|
|
153
|
+
if (!(await fileExists(specAbs))) {
|
|
154
|
+
inputs.findings.add("coverage", `${m.relPath}: capability "${cap.id}" declares capability_spec "${specRel}" which does not exist — name/description left empty, spec_path emitted dangling (F22)`);
|
|
155
|
+
}
|
|
156
|
+
// M-ADR 0211 §4.2 — the manifest is a declaration and its row is correct. The finding
|
|
157
|
+
// fires ONLY on a CONTRADICTION between two declaration surfaces: this manifest
|
|
158
|
+
// catalogues the spec as a capability, and the spec declares a Kind that does not
|
|
159
|
+
// catalogue. A missing template marker is never a finding (measured: six of seven
|
|
160
|
+
// such files in one estate were correct meta-aggregates).
|
|
161
|
+
const declaredKind = (await readSpecDeclaration(specAbs)).kind;
|
|
162
|
+
if (declaredKind !== "" && !CATALOGUING_KINDS.has(declaredKind)) {
|
|
163
|
+
inputs.findings.add(KNOWN_KINDS.has(declaredKind) ? "divergence" : "convention", KNOWN_KINDS.has(declaredKind)
|
|
164
|
+
? `${m.relPath}: declaration-mismatch — the manifest catalogues "${cap.id}" as a capability but ${specRel} declares Kind: ${declaredKind}, which does not catalogue; the row is emitted from the manifest and the contradiction is the finding (M-ADR 0211 §4.2)`
|
|
165
|
+
: `${specRel}: declares Kind: "${declaredKind}", not in the vocabulary capability | meta-aggregate | policy | synthesis | guide (M-ADR 0211 §4.2)`);
|
|
166
|
+
}
|
|
167
|
+
const headingName = await readSpecH1Name(specAbs);
|
|
168
|
+
if (headingName !== "") {
|
|
169
|
+
name = headingName;
|
|
170
|
+
}
|
|
171
|
+
description = await readSpecOverview(specAbs);
|
|
172
|
+
// Log id ↔ spec stem divergence (informational).
|
|
173
|
+
const stem = stemOf(specRel);
|
|
174
|
+
if (stem !== "" && stem !== cap.id) {
|
|
175
|
+
inputs.findings.add("divergence", `${m.relPath}: capability.id="${cap.id}" differs from spec stem "${stem}"`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// WILD-VOCABULARY CENSUS, reporting-only and deliberately NOT a binding.
|
|
179
|
+
//
|
|
180
|
+
// ✅ THE CENSUS WORKED AND ITS QUESTION IS NOW RULED. It measured 44 of the
|
|
181
|
+
// forward estate's rows outside the old three-member vocabulary — exploration
|
|
182
|
+
// 26, placeholder 11, detailed-pre-lock 6, first-draft 1 — and that evidence
|
|
183
|
+
// is what settled the ruling at catalogue schema 3.5.0: all three recurring
|
|
184
|
+
// values were ADMITTED, not re-authored away, because each varies WITHIN a
|
|
185
|
+
// domain (the ruled phase-vs-convention discriminator). Re-authoring them
|
|
186
|
+
// would have rewritten 37 rows that were declaring something true.
|
|
187
|
+
//
|
|
188
|
+
// So this census now reports the RESIDUE, not the population: 1 row at the
|
|
189
|
+
// forward estate (`first-draft`, n=1) and 0 at the reverse estate, measured
|
|
190
|
+
// structurally by key path. It stays reporting-only because the column still
|
|
191
|
+
// binds no enum — `convention` is non-gate-acting under M-ADR 0132 §4.3, so
|
|
192
|
+
// it cannot fail a gate on arrival. Binding is a later deliberate act, gated
|
|
193
|
+
// on that residue reaching zero at both estates.
|
|
194
|
+
//
|
|
195
|
+
// It keeps earning its place: an unenforced field cannot notice a wrong value
|
|
196
|
+
// when one is written, and this is the only thing that notices.
|
|
197
|
+
//
|
|
198
|
+
// Emitted only for an AUTHORED value: Pass 2's hardcoded default is a
|
|
199
|
+
// default, not a declaration, and reporting it would blame an adopter for
|
|
200
|
+
// a value the builder chose.
|
|
201
|
+
const declaredStatus = (cap.status ?? "").trim();
|
|
202
|
+
if (declaredStatus !== "" && !DOCUMENTED_CAPABILITY_STATUS.has(declaredStatus)) {
|
|
203
|
+
inputs.findings.add("convention", SPEC_AUTHORING_STAGES.has(declaredStatus)
|
|
204
|
+
? `${m.relPath}: capability.status="${declaredStatus}" is a SPEC-AUTHORING stage ` +
|
|
205
|
+
`(M-ADR 0201), not a capability lifecycle phase — this column carries lifecycle ` +
|
|
206
|
+
`only (${[...DOCUMENTED_CAPABILITY_STATUS].join(" | ")}). Authoring stage belongs ` +
|
|
207
|
+
`in the spec's own '**Status:**' header. The value is catalogued as authored and ` +
|
|
208
|
+
`nothing is dropped; re-author to the lifecycle phase TRUE of the capability, ` +
|
|
209
|
+
`which is not a translation of this word`
|
|
210
|
+
: `${m.relPath}: capability.status="${declaredStatus}" is outside the documented ` +
|
|
211
|
+
`vocabulary (${[...DOCUMENTED_CAPABILITY_STATUS].join(" | ")}) — the value is ` +
|
|
212
|
+
`catalogued as authored and nothing is dropped; reported so the population is ` +
|
|
213
|
+
`countable`);
|
|
214
|
+
}
|
|
215
|
+
// contracts.provenance — orthogonal to contracts.status, and read WITHOUT
|
|
216
|
+
// consulting it. A value outside the set is reported and still catalogued,
|
|
217
|
+
// matching the status census's posture rather than dropping the cell.
|
|
218
|
+
const declaredProvenance = (m.traceability.contracts?.provenance ?? "").trim();
|
|
219
|
+
if (declaredProvenance !== "" &&
|
|
220
|
+
!DOCUMENTED_CONTRACT_PROVENANCE.has(declaredProvenance)) {
|
|
221
|
+
inputs.findings.add("convention", `${m.relPath}: traceability.contracts.provenance="${declaredProvenance}" is outside ` +
|
|
222
|
+
`the documented vocabulary (${[...DOCUMENTED_CONTRACT_PROVENANCE].join(" | ")}) — ` +
|
|
223
|
+
`the value is catalogued as authored and nothing is dropped`);
|
|
224
|
+
}
|
|
225
|
+
seen.add(dedupKey);
|
|
226
|
+
keptBy.set(dedupKey, m);
|
|
227
|
+
rows.push({
|
|
228
|
+
id: cap.id,
|
|
229
|
+
name,
|
|
230
|
+
domain_id: cap.domain,
|
|
231
|
+
tree_category: "",
|
|
232
|
+
parent_capability_id: "",
|
|
233
|
+
evidence_class: (cap.evidence_class ?? "").trim(),
|
|
234
|
+
context: (cap.context ?? "").trim(),
|
|
235
|
+
description,
|
|
236
|
+
status: cap.status ?? "active",
|
|
237
|
+
spec_path: specRel,
|
|
238
|
+
contract_provenance: declaredProvenance,
|
|
239
|
+
source: "capability-spec",
|
|
240
|
+
runway_classification: m.traceability.runway?.classification ?? "",
|
|
241
|
+
data_subset: cap.data_subset ?? "",
|
|
242
|
+
slice_scope: slice,
|
|
243
|
+
source_sdd_id: "",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Pass 2 — capability spec files not covered by any traceability.
|
|
247
|
+
//
|
|
248
|
+
// M-ADR 0211 §4.2: placement asserts NOTHING. A product/*.md is catalogued only when it
|
|
249
|
+
// DECLARES something — the template H1, a lifecycle Status, or a Kind — and its kind
|
|
250
|
+
// catalogues. A file declaring nothing is silence: named, no row. A file declaring a
|
|
251
|
+
// non-cataloguing kind (policy, synthesis, guide) is respected: no row, no finding.
|
|
252
|
+
// §4.3: files=N rows=M excluded=K, every excluded class named, so the count can DISAGREE.
|
|
253
|
+
const specFiles = await collectCapabilitySpecs(inputs.sddPath);
|
|
254
|
+
const productFiles = new Set(specFiles.map((p) => relative(inputs.sddPath, p)));
|
|
255
|
+
let notACapabilitySpec = 0, nonCataloguingKind = 0, collisions = 0, unknownKind = 0;
|
|
256
|
+
for (const spec of specFiles) {
|
|
257
|
+
const relPath = relative(inputs.sddPath, spec);
|
|
258
|
+
if (usedSpecPaths.has(relPath))
|
|
259
|
+
continue;
|
|
260
|
+
if (rows.some((r) => r["spec_path"] === relPath))
|
|
261
|
+
continue;
|
|
262
|
+
const stem = stemOf(relPath);
|
|
263
|
+
if (seen.has(stem)) {
|
|
264
|
+
// The bare `continue` that dropped platform-tier specs silently for months (M-ADR
|
|
265
|
+
// 0211 §1(c)) — now the same divergence finding the manifest path emits.
|
|
266
|
+
collisions++;
|
|
267
|
+
const kept = rows.find((r) => r["id"] === stem);
|
|
268
|
+
inputs.findings.add("divergence", `capabilities: ${relPath} shares primary key "${stem}" with ${kept ? String(kept["spec_path"]) : "an earlier row"} — the earlier row is kept and THIS file emits no row (M-ADR 0211 §4.3: a stem collision on the spec-only path is now loud; two different things sharing a slug is authoring debt to split, not a key to widen)`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const decl = await readSpecDeclaration(spec);
|
|
272
|
+
if (!decl.templateH1 && decl.status === "" && decl.kind === "") {
|
|
273
|
+
notACapabilitySpec++;
|
|
274
|
+
inputs.findings.add("coverage", `${relPath}: not-a-capability-spec — the file declares nothing (no template H1, no Status, no Kind), so its directory is not allowed to assert a capability for it; no row (M-ADR 0211 §4.2)`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const kind = decl.kind === "" ? "capability" : decl.kind;
|
|
278
|
+
if (!KNOWN_KINDS.has(kind)) {
|
|
279
|
+
unknownKind++;
|
|
280
|
+
inputs.findings.add("convention", `${relPath}: declares Kind: "${kind}", not in the vocabulary capability | meta-aggregate | policy | synthesis | guide — catalogued as a capability meanwhile (M-ADR 0211 §4.2)`);
|
|
281
|
+
}
|
|
282
|
+
else if (!CATALOGUING_KINDS.has(kind)) {
|
|
283
|
+
nonCataloguingKind++;
|
|
284
|
+
continue; // respected, silently: the author declared it is not a capability
|
|
285
|
+
}
|
|
286
|
+
const domain = inferDomainFromSpec(relPath);
|
|
287
|
+
if (domain === "") {
|
|
288
|
+
inputs.findings.add("orphan", `${relPath}: cannot infer domain from path`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!inputs.knownDomainIds.has(domain)) {
|
|
292
|
+
inputs.findings.add("orphan", `${relPath}: domain "${domain}" has no docs/domains/${domain}/`);
|
|
293
|
+
}
|
|
294
|
+
const heading = await readSpecH1Name(spec);
|
|
295
|
+
const description = await readSpecOverview(spec);
|
|
296
|
+
seen.add(sliceKey(stem, ""));
|
|
297
|
+
rows.push({
|
|
298
|
+
id: stem,
|
|
299
|
+
name: heading !== "" ? heading : titleCase(stem),
|
|
300
|
+
domain_id: domain,
|
|
301
|
+
tree_category: "",
|
|
302
|
+
parent_capability_id: "",
|
|
303
|
+
evidence_class: "",
|
|
304
|
+
context: "",
|
|
305
|
+
description,
|
|
306
|
+
// Pass-2 specs have no manifest to declare a lifecycle phase; `active` is the STATED
|
|
307
|
+
// default (M-ADR 0211 §4.2), not an assertion made by the directory.
|
|
308
|
+
status: "active",
|
|
309
|
+
spec_path: relPath,
|
|
310
|
+
contract_provenance: "",
|
|
311
|
+
source: "capability-spec",
|
|
312
|
+
runway_classification: "",
|
|
313
|
+
data_subset: "",
|
|
314
|
+
slice_scope: "",
|
|
315
|
+
source_sdd_id: "",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// M-ADR 0211 §4.3 — files against rows, never distinct keys against themselves.
|
|
319
|
+
const rowFiles = new Set(rows.map((r) => String(r["spec_path"])).filter((p) => productFiles.has(p)));
|
|
320
|
+
const excluded = notACapabilitySpec + nonCataloguingKind + collisions;
|
|
321
|
+
inputs.findings.add("metric", `capabilities: files=${productFiles.size} rows=${rows.length} rows-on-product-files=${rowFiles.size} excluded=${excluded} (not-a-capability-spec=${notACapabilitySpec} non-cataloguing-kind=${nonCataloguingKind} collision=${collisions} unknown-kind=${unknownKind})`);
|
|
322
|
+
if (productFiles.size !== rowFiles.size + excluded) {
|
|
323
|
+
inputs.findings.add("coverage", `capabilities: the population does not balance — ${productFiles.size} product/*.md files ≠ ${rowFiles.size} files with a row + ${excluded} excluded for a named reason. Files and rows are two instruments; they disagree, and the disagreement is the finding (M-ADR 0211 §4.3)`);
|
|
324
|
+
}
|
|
325
|
+
return rows;
|
|
326
|
+
}
|
|
327
|
+
export const CATALOGUING_KINDS = new Set(["capability", "meta-aggregate"]);
|
|
328
|
+
export const KNOWN_KINDS = new Set(["capability", "meta-aggregate", "policy", "synthesis", "guide"]);
|
|
329
|
+
export async function readSpecDeclaration(specAbs) {
|
|
330
|
+
let text;
|
|
331
|
+
try {
|
|
332
|
+
const st = await stat(specAbs);
|
|
333
|
+
if (!st.isFile())
|
|
334
|
+
return { templateH1: false, status: "", kind: "" };
|
|
335
|
+
text = await readFile(specAbs, "utf-8");
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return { templateH1: false, status: "", kind: "" };
|
|
339
|
+
}
|
|
340
|
+
const head = text.split(/\r?\n/).slice(0, 60);
|
|
341
|
+
const templateH1 = head.some((l) => /^#\s*(Capability Specification|Spec):/i.test(l.trim()));
|
|
342
|
+
const bold = (field) => {
|
|
343
|
+
for (const l of head) {
|
|
344
|
+
const m = new RegExp(`\\*\\*${field}:\\*\\*\\s*([^|\\n]*)`).exec(l);
|
|
345
|
+
if (m)
|
|
346
|
+
return (m[1] ?? "").trim();
|
|
347
|
+
}
|
|
348
|
+
return "";
|
|
349
|
+
};
|
|
350
|
+
const prose = (field) => {
|
|
351
|
+
for (const l of head) {
|
|
352
|
+
if (!l.startsWith(">"))
|
|
353
|
+
continue;
|
|
354
|
+
const m = new RegExp(`${field}:\\s*([^|\\n]*)`).exec(l);
|
|
355
|
+
if (m)
|
|
356
|
+
return (m[1] ?? "").trim();
|
|
357
|
+
}
|
|
358
|
+
return "";
|
|
359
|
+
};
|
|
360
|
+
return { templateH1, status: bold("Status") || prose("Status"), kind: bold("Kind") || prose("Kind") };
|
|
361
|
+
}
|
|
362
|
+
async function readSpecH1Name(specAbs) {
|
|
363
|
+
try {
|
|
364
|
+
const s = await stat(specAbs);
|
|
365
|
+
if (!s.isFile())
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
const text = await readFile(specAbs, "utf-8");
|
|
372
|
+
for (const line of text.split(/\r?\n/)) {
|
|
373
|
+
const t = line.trim();
|
|
374
|
+
if (t === "")
|
|
375
|
+
continue;
|
|
376
|
+
if (!t.startsWith("#"))
|
|
377
|
+
return "";
|
|
378
|
+
const stripped = t
|
|
379
|
+
.replace(/^#+\s*/, "")
|
|
380
|
+
.replace(/^(Capability Specification|Spec):\s*/i, "")
|
|
381
|
+
.trim();
|
|
382
|
+
return stripped;
|
|
383
|
+
}
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Extract the lead paragraph of a capability spec's `## Overview` section
|
|
388
|
+
* to use as `capabilities.description`.
|
|
389
|
+
*
|
|
390
|
+
* Returns the first non-empty paragraph block after the `## Overview`
|
|
391
|
+
* heading — lines are joined with single spaces, stopping at the next blank
|
|
392
|
+
* line or the next heading. Returns "" when the spec has no Overview section
|
|
393
|
+
* (not all specs require one).
|
|
394
|
+
*/
|
|
395
|
+
async function readSpecOverview(specAbs) {
|
|
396
|
+
try {
|
|
397
|
+
const s = await stat(specAbs);
|
|
398
|
+
if (!s.isFile())
|
|
399
|
+
return "";
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
const text = await readFile(specAbs, "utf-8");
|
|
405
|
+
const lines = text.split(/\r?\n/);
|
|
406
|
+
let inOverview = false;
|
|
407
|
+
const paragraph = [];
|
|
408
|
+
for (const line of lines) {
|
|
409
|
+
const t = line.trim();
|
|
410
|
+
if (!inOverview) {
|
|
411
|
+
if (/^##\s+Overview\s*$/i.test(t))
|
|
412
|
+
inOverview = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// Stop at the next heading.
|
|
416
|
+
if (t.startsWith("#"))
|
|
417
|
+
break;
|
|
418
|
+
if (t === "") {
|
|
419
|
+
// Blank line ends the lead paragraph once we've captured content.
|
|
420
|
+
if (paragraph.length > 0)
|
|
421
|
+
break;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
paragraph.push(t);
|
|
425
|
+
}
|
|
426
|
+
return paragraph.join(" ");
|
|
427
|
+
}
|
|
428
|
+
async function collectCapabilitySpecs(sddPath) {
|
|
429
|
+
const out = [];
|
|
430
|
+
const domainsDir = join(sddPath, DOMAINS_DIR_REL);
|
|
431
|
+
let domains;
|
|
432
|
+
try {
|
|
433
|
+
domains = await readdir(domainsDir, { withFileTypes: true });
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
for (const d of domains) {
|
|
439
|
+
if (!d.isDirectory())
|
|
440
|
+
continue;
|
|
441
|
+
const productDir = join(domainsDir, d.name, "product");
|
|
442
|
+
let entries;
|
|
443
|
+
try {
|
|
444
|
+
entries = await readdir(productDir, { withFileTypes: true });
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
for (const e of entries) {
|
|
450
|
+
if (!e.isFile())
|
|
451
|
+
continue;
|
|
452
|
+
if (!e.name.endsWith(".md"))
|
|
453
|
+
continue;
|
|
454
|
+
const lower = e.name.toLowerCase();
|
|
455
|
+
if (lower === "readme.md" || lower === "index.md")
|
|
456
|
+
continue;
|
|
457
|
+
out.push(join(productDir, e.name));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
function inferDomainFromSpec(relPath) {
|
|
463
|
+
const parts = relPath.split(sep);
|
|
464
|
+
const i = parts.indexOf("domains");
|
|
465
|
+
if (i === -1 || i + 1 >= parts.length)
|
|
466
|
+
return "";
|
|
467
|
+
return parts[i + 1] ?? "";
|
|
468
|
+
}
|
|
469
|
+
function stemOf(relPath) {
|
|
470
|
+
const base = relPath.split(sep).slice(-1)[0] ?? "";
|
|
471
|
+
return base.replace(/\.md$/i, "").replace(/\.traceability\.yaml$/i, "");
|
|
472
|
+
}
|
|
473
|
+
function titleCase(slug) {
|
|
474
|
+
return slug
|
|
475
|
+
.split("-")
|
|
476
|
+
.map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
477
|
+
.join(" ");
|
|
478
|
+
}
|
|
479
|
+
async function fileExists(path) {
|
|
480
|
+
try {
|
|
481
|
+
const { stat } = await import("node:fs/promises");
|
|
482
|
+
return (await stat(path)).isFile();
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Edge builder for capability_domain_model — emits `capability_domain_model.csv`.
|
|
2
|
+
//
|
|
3
|
+
// Platform-profile only. Non-platform SDDs return an empty list.
|
|
4
|
+
//
|
|
5
|
+
// Sources:
|
|
6
|
+
// - Forward: for each loaded traceability manifest with
|
|
7
|
+
// `traceability.domain_model.path` set, emit one row linking
|
|
8
|
+
// capability_id → domain_model_id with source="traceability-manifest".
|
|
9
|
+
// - Reverse: for every capability whose domain has a row in
|
|
10
|
+
// domain_models.csv but which did NOT declare via the manifest,
|
|
11
|
+
// emit a row with source="inferred". This captures the
|
|
12
|
+
// "capability lives in domain D, and D has a domain model" inference.
|
|
13
|
+
//
|
|
14
|
+
// Domain model ids follow the `{domain_id}-data-model` convention from
|
|
15
|
+
// the domain-models builder.
|
|
16
|
+
//
|
|
17
|
+
// Added in schema 2.4.0 per ADR 0142.
|
|
18
|
+
export function buildCapabilityDomainModel(inputs) {
|
|
19
|
+
if (inputs.manifest.profile !== "platform-profile") {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const rows = [];
|
|
24
|
+
// Path 1: Forward — declared in traceability manifests.
|
|
25
|
+
const declaredCapIds = new Set();
|
|
26
|
+
for (const m of inputs.manifests) {
|
|
27
|
+
if (!m.traceability.domain_model)
|
|
28
|
+
continue;
|
|
29
|
+
const capId = m.capability.id;
|
|
30
|
+
if (!inputs.knownCapabilityIds.has(capId))
|
|
31
|
+
continue;
|
|
32
|
+
const domainModelId = `${m.capability.domain}-data-model`;
|
|
33
|
+
if (!inputs.knownDomainModelIds.has(domainModelId))
|
|
34
|
+
continue;
|
|
35
|
+
const id = `${capId}__${domainModelId}`;
|
|
36
|
+
if (seen.has(id))
|
|
37
|
+
continue;
|
|
38
|
+
seen.add(id);
|
|
39
|
+
declaredCapIds.add(capId);
|
|
40
|
+
rows.push({
|
|
41
|
+
id,
|
|
42
|
+
capability_id: capId,
|
|
43
|
+
domain_model_id: domainModelId,
|
|
44
|
+
source: "traceability-manifest",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Path 2: Reverse — inferred from capability's domain having a domain model.
|
|
48
|
+
// Only emit for capabilities that didn't declare via manifest.
|
|
49
|
+
for (const cap of inputs.capabilityRows) {
|
|
50
|
+
const capId = String(cap["id"] ?? "");
|
|
51
|
+
if (capId === "" || declaredCapIds.has(capId))
|
|
52
|
+
continue;
|
|
53
|
+
if (!inputs.knownCapabilityIds.has(capId))
|
|
54
|
+
continue;
|
|
55
|
+
const domainId = String(cap["domain_id"] ?? "");
|
|
56
|
+
if (domainId === "")
|
|
57
|
+
continue;
|
|
58
|
+
const domainModelId = `${domainId}-data-model`;
|
|
59
|
+
if (!inputs.knownDomainModelIds.has(domainModelId))
|
|
60
|
+
continue;
|
|
61
|
+
const id = `${capId}__${domainModelId}`;
|
|
62
|
+
if (seen.has(id))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(id);
|
|
65
|
+
rows.push({
|
|
66
|
+
id,
|
|
67
|
+
capability_id: capId,
|
|
68
|
+
domain_model_id: domainModelId,
|
|
69
|
+
source: "inferred",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=capability-domain-model.js.map
|