@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-spec feature extractor per ADR 0145 (schema 2.7.0).
|
|
3
|
+
*
|
|
4
|
+
* Walks `docs/domains/<dom>/product/<capability>.md` (platform-profile),
|
|
5
|
+
* parses the `## Feature Grounding (code-path trace)` markdown table,
|
|
6
|
+
* and emits four catalogue contributions:
|
|
7
|
+
*
|
|
8
|
+
* - `features.csv` rows with `source: capability-spec`.
|
|
9
|
+
* - `capability_feature.csv` rows (1:1 with each feature row).
|
|
10
|
+
* - `feature_repo.csv` rows derived from file-path references in
|
|
11
|
+
* the Implementation column.
|
|
12
|
+
* - `feature_service.csv` rows derived from class names / file
|
|
13
|
+
* basenames / route fragments in the Implementation column,
|
|
14
|
+
* resolved via `services.csv` + the optional per-SDD alias map
|
|
15
|
+
* introduced by ADR 0143.
|
|
16
|
+
*
|
|
17
|
+
* Resolution is best-effort and never breaks FK validation — only
|
|
18
|
+
* confidently-resolved rows are emitted. Unresolved CAPABILITY ids emit
|
|
19
|
+
* findings (convention/orphan). Unresolved SERVICE and REPO tokens are
|
|
20
|
+
* dropped silently, deliberately: the token stream comes from free-text
|
|
21
|
+
* Implementation columns (class names, route fragments, path segments),
|
|
22
|
+
* so most tokens legitimately resolve to nothing and a finding per miss
|
|
23
|
+
* would be noise. The row-level finding — a non-empty Implementation
|
|
24
|
+
* cell yielding ZERO repo resolutions — IS signal and is emitted as a
|
|
25
|
+
* coverage finding per row plus one summary line (M-ADR 0143 §4.4 as
|
|
26
|
+
* amended); it measures resolution coverage, never correctness.
|
|
27
|
+
*
|
|
28
|
+
* Precedence per ADR 0145 §4.7: feature-tree, feature-spec, and
|
|
29
|
+
* feature-grounding rows win on id collision (they are more specific
|
|
30
|
+
* sources); capability-spec rows are skipped with a divergence finding
|
|
31
|
+
* when their id is already taken.
|
|
32
|
+
*/
|
|
33
|
+
import { readFile } from "node:fs/promises";
|
|
34
|
+
import { relative } from "node:path";
|
|
35
|
+
import { capabilityIdFromSpecPath, findCapabilitySpecFiles, parseCapabilitySpecTable, } from "./capability-spec-walker.js";
|
|
36
|
+
const DESCRIPTION_MAX = 500;
|
|
37
|
+
/** Columns the service-name resolver scans for class names / route fragments. */
|
|
38
|
+
const SERVICE_RESOLUTION_COLUMNS = ["implementation"];
|
|
39
|
+
/** Columns the repo-name resolver scans for file-path references. */
|
|
40
|
+
const REPO_RESOLUTION_COLUMNS = ["implementation", "grounding_doc"];
|
|
41
|
+
export async function buildCapabilitySpec(inputs) {
|
|
42
|
+
const featureRows = [];
|
|
43
|
+
const capabilityFeatureRows = [];
|
|
44
|
+
const featureRepoRows = [];
|
|
45
|
+
// M-ADR 0143 §4.4 (amended): rows whose Implementation cell is non-empty but resolves to
|
|
46
|
+
// ZERO repos. Counted for the summary finding below; each row also gets its own
|
|
47
|
+
// line. This measures evidence→repo RESOLUTION COVERAGE, not correctness.
|
|
48
|
+
let repoEvidenceRows = 0;
|
|
49
|
+
const repoZeroResolutionFeatureIds = [];
|
|
50
|
+
const featureServiceRows = [];
|
|
51
|
+
const seenFeatureRepo = new Set();
|
|
52
|
+
const seenFeatureService = new Set();
|
|
53
|
+
const seenCapabilityFeature = new Set();
|
|
54
|
+
const seenFeatures = new Set(inputs.existingFeatureIds);
|
|
55
|
+
// Frozen copy — `seenFeatures` grows during this walk, so only this set can
|
|
56
|
+
// answer "did an EARLIER SOURCE emit this id?" at the collision site below.
|
|
57
|
+
const fromEarlierSource = new Set(inputs.existingFeatureIds);
|
|
58
|
+
if (inputs.profile !== "platform-profile") {
|
|
59
|
+
return {
|
|
60
|
+
featureRows,
|
|
61
|
+
capabilityFeatureRows,
|
|
62
|
+
featureRepoRows,
|
|
63
|
+
featureServiceRows,
|
|
64
|
+
repoEvidenceRows,
|
|
65
|
+
repoZeroResolutionFeatureIds,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const files = await findCapabilitySpecFiles(inputs.sddPath);
|
|
69
|
+
for (const absPath of files) {
|
|
70
|
+
const capabilityId = capabilityIdFromSpecPath(absPath, inputs.sddPath);
|
|
71
|
+
const relPath = relative(inputs.sddPath, absPath);
|
|
72
|
+
if (capabilityId === "") {
|
|
73
|
+
inputs.findings.add("convention", `${relPath}: capability id could not be resolved from file path`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!inputs.knownCapabilityIds.has(capabilityId)) {
|
|
77
|
+
// Common for product README-style files that aren't capability specs.
|
|
78
|
+
// Treat as orphan, not convention — silently skip.
|
|
79
|
+
inputs.findings.add("orphan", `${relPath}: filename stem "${capabilityId}" does not match any capability id in capabilities.csv — skipping`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const text = await readFile(absPath, "utf-8");
|
|
83
|
+
const rows = parseCapabilitySpecTable(text);
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
inputs.findings.add("coverage", `${relPath}: capability spec found but no "## Feature Grounding (code-path trace)" table recognised`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const slugsInThisFile = new Set();
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
if (slugsInThisFile.has(row.slug)) {
|
|
91
|
+
inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: duplicate feature slug "${row.slug}" in same file (first occurrence wins)`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
slugsInThisFile.add(row.slug);
|
|
95
|
+
const featureId = `${capabilityId}-${row.slug}`;
|
|
96
|
+
// Narrow the collision skip to the feature ROW only. When an earlier
|
|
97
|
+
// source already emitted this feature id, feature-tree / feature-spec /
|
|
98
|
+
// feature-grounding win the ROW (they carry runway / group / grounding
|
|
99
|
+
// context). EVERY edge below still emits — including capability_feature:
|
|
100
|
+
// a file-authored feature's row arrives from the feature files with NO
|
|
101
|
+
// capability_feature edge, so for that shape this extractor's edge is
|
|
102
|
+
// the ONLY source of the binding, and suppressing a non-duplicate edge
|
|
103
|
+
// because its sibling row duplicated silently unbinds the feature
|
|
104
|
+
// (measured: four owned-but-untraced pairs from exactly this rule).
|
|
105
|
+
// When the earlier source DID also emit the edge, the ids are identical
|
|
106
|
+
// (`${cap}__${feature}` in every emitter) and keyed-table dedup unions
|
|
107
|
+
// them — dedup rows, keep edges. feature_repo / feature_service remain
|
|
108
|
+
// this source's unique contribution (ADR 0145) as before.
|
|
109
|
+
if (seenFeatures.has(featureId)) {
|
|
110
|
+
// Same one-check-two-meanings split as feature-grounding.ts: a hit on
|
|
111
|
+
// an id seeded from an EARLIER SOURCE is the documented precedence rule
|
|
112
|
+
// firing (correct output, `inferred`, does not gate); a hit on an id
|
|
113
|
+
// this pass emitted itself is a real collision between two capability
|
|
114
|
+
// specs (`divergence`, gates). See the long comment there.
|
|
115
|
+
if (fromEarlierSource.has(featureId)) {
|
|
116
|
+
inputs.findings.add("inferred", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by an earlier source (feature-tree / feature-spec / feature-grounding wins the row; edges still emitted, incl. capability_feature)`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by another capability spec in this same pass (first occurrence wins the row; edges still emitted, incl. capability_feature)`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
seenFeatures.add(featureId);
|
|
124
|
+
featureRows.push({
|
|
125
|
+
id: featureId,
|
|
126
|
+
name: row.slug,
|
|
127
|
+
product_id: "",
|
|
128
|
+
capability_id: capabilityId,
|
|
129
|
+
parent_group_id: "",
|
|
130
|
+
parent_feature_id: "",
|
|
131
|
+
description: deriveDescription(row),
|
|
132
|
+
maturity: "",
|
|
133
|
+
status: "active",
|
|
134
|
+
source: "capability-spec",
|
|
135
|
+
runway: "",
|
|
136
|
+
source_sdd_id: "",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// capability_feature edge — emitted on BOTH paths (the binding is not a
|
|
140
|
+
// duplicate just because the row was; PK-identical duplicates union at
|
|
141
|
+
// keyed-table dedup).
|
|
142
|
+
const cfId = `${capabilityId}__${featureId}`;
|
|
143
|
+
if (!seenCapabilityFeature.has(cfId)) {
|
|
144
|
+
seenCapabilityFeature.add(cfId);
|
|
145
|
+
capabilityFeatureRows.push({
|
|
146
|
+
id: cfId,
|
|
147
|
+
capability_id: capabilityId,
|
|
148
|
+
feature_id: featureId,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// feature_service edges — always emit (unique to capability-spec)
|
|
152
|
+
const services = resolveServices(row, inputs);
|
|
153
|
+
for (const serviceId of services) {
|
|
154
|
+
const edgeId = `${featureId}__${serviceId}`;
|
|
155
|
+
if (seenFeatureService.has(edgeId))
|
|
156
|
+
continue;
|
|
157
|
+
seenFeatureService.add(edgeId);
|
|
158
|
+
featureServiceRows.push({
|
|
159
|
+
id: edgeId,
|
|
160
|
+
feature_id: featureId,
|
|
161
|
+
service_id: serviceId,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// feature_repo edges — always emit (unique to capability-spec)
|
|
165
|
+
const repos = resolveRepos(row, inputs);
|
|
166
|
+
if ((row.cells.get("implementation") ?? "").trim() !== "") {
|
|
167
|
+
repoEvidenceRows++;
|
|
168
|
+
if (repos.length === 0) {
|
|
169
|
+
repoZeroResolutionFeatureIds.push(featureId);
|
|
170
|
+
inputs.findings.add("coverage", `${relPath}:${row.lineNumber}: feature "${featureId}" — Implementation cell is non-empty but resolved to ZERO repos; name a repo key or path the resolver can match, or map the token in .sdd/feature-grounding-aliases.yaml repo_aliases (M-ADR 0143 §4.4)`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const repoKey of repos) {
|
|
174
|
+
const edgeId = `${featureId}__${repoKey}`;
|
|
175
|
+
if (seenFeatureRepo.has(edgeId))
|
|
176
|
+
continue;
|
|
177
|
+
seenFeatureRepo.add(edgeId);
|
|
178
|
+
featureRepoRows.push({
|
|
179
|
+
id: edgeId,
|
|
180
|
+
feature_id: featureId,
|
|
181
|
+
repo_id: repoKey,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// The summary line is emitted by the build (build.ts, after feature_repo is
|
|
187
|
+
// final) so it can split rows fired against features left with NO anchor.
|
|
188
|
+
return {
|
|
189
|
+
featureRows,
|
|
190
|
+
capabilityFeatureRows,
|
|
191
|
+
featureRepoRows,
|
|
192
|
+
featureServiceRows,
|
|
193
|
+
repoEvidenceRows,
|
|
194
|
+
repoZeroResolutionFeatureIds,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the description column by joining non-feature cells, capped at
|
|
199
|
+
* 500 chars to match the feature-grounding convention.
|
|
200
|
+
*/
|
|
201
|
+
function deriveDescription(row) {
|
|
202
|
+
const parts = [];
|
|
203
|
+
for (const [key, value] of row.cells) {
|
|
204
|
+
if (value === "")
|
|
205
|
+
continue;
|
|
206
|
+
parts.push(`${prettyHeader(key)}: ${value}`);
|
|
207
|
+
}
|
|
208
|
+
const joined = parts.join(" • ");
|
|
209
|
+
return joined.length > DESCRIPTION_MAX
|
|
210
|
+
? joined.slice(0, DESCRIPTION_MAX)
|
|
211
|
+
: joined;
|
|
212
|
+
}
|
|
213
|
+
function prettyHeader(key) {
|
|
214
|
+
return key
|
|
215
|
+
.split("_")
|
|
216
|
+
.map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
217
|
+
.join(" ");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve service ids referenced in a row's Implementation cell.
|
|
221
|
+
* Same strategy as feature-grounding: tokenize, alias-lookup, normalise,
|
|
222
|
+
* known-id lookup.
|
|
223
|
+
*/
|
|
224
|
+
function resolveServices(row, inputs) {
|
|
225
|
+
const out = new Set();
|
|
226
|
+
for (const col of SERVICE_RESOLUTION_COLUMNS) {
|
|
227
|
+
const cell = row.cells.get(col) ?? "";
|
|
228
|
+
if (cell === "")
|
|
229
|
+
continue;
|
|
230
|
+
for (const token of tokenize(cell)) {
|
|
231
|
+
const resolved = resolveServiceToken(token, inputs);
|
|
232
|
+
if (resolved !== null)
|
|
233
|
+
out.add(resolved);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return [...out].sort();
|
|
237
|
+
}
|
|
238
|
+
function resolveServiceToken(rawToken, inputs) {
|
|
239
|
+
if (inputs.aliases !== null) {
|
|
240
|
+
const aliased = inputs.aliases.serviceAliases.get(rawToken);
|
|
241
|
+
if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
|
|
242
|
+
return aliased;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const normalised = normaliseToken(rawToken);
|
|
246
|
+
if (normalised === "")
|
|
247
|
+
return null;
|
|
248
|
+
if (inputs.knownServiceIds.has(normalised))
|
|
249
|
+
return normalised;
|
|
250
|
+
if (inputs.aliases !== null) {
|
|
251
|
+
const aliased = inputs.aliases.serviceAliases.get(normalised);
|
|
252
|
+
if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
|
|
253
|
+
return aliased;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
function resolveRepos(row, inputs) {
|
|
259
|
+
const out = new Set();
|
|
260
|
+
for (const col of REPO_RESOLUTION_COLUMNS) {
|
|
261
|
+
const cell = row.cells.get(col) ?? "";
|
|
262
|
+
if (cell === "")
|
|
263
|
+
continue;
|
|
264
|
+
const coded = inputs.repoTokenPolicy === "identifier" ? codeSpanTokens(cell) : null;
|
|
265
|
+
for (const token of tokenize(cell)) {
|
|
266
|
+
const resolved = resolveRepoToken(token, inputs);
|
|
267
|
+
if (resolved === null)
|
|
268
|
+
continue;
|
|
269
|
+
if (coded !== null && isPlainWord(token) && !coded.has(token) && inputs.aliases?.repoAliases.get(token) !== resolved)
|
|
270
|
+
continue;
|
|
271
|
+
out.add(resolved);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...out].sort();
|
|
275
|
+
}
|
|
276
|
+
function resolveRepoToken(rawToken, inputs) {
|
|
277
|
+
if (inputs.aliases !== null) {
|
|
278
|
+
const aliased = inputs.aliases.repoAliases.get(rawToken);
|
|
279
|
+
if (aliased !== undefined && inputs.knownRepoKeys.has(aliased)) {
|
|
280
|
+
return aliased;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (inputs.knownRepoKeys.has(rawToken))
|
|
284
|
+
return rawToken;
|
|
285
|
+
for (const segment of rawToken.split("/")) {
|
|
286
|
+
const stripped = segment
|
|
287
|
+
.replace(/-master@.*$/, "")
|
|
288
|
+
.replace(/@[^/]+$/, "");
|
|
289
|
+
if (stripped !== "" && inputs.knownRepoKeys.has(stripped)) {
|
|
290
|
+
return stripped;
|
|
291
|
+
}
|
|
292
|
+
const trailingMaster = stripped.replace(/-master$/, "");
|
|
293
|
+
if (trailingMaster !== stripped &&
|
|
294
|
+
inputs.knownRepoKeys.has(trailingMaster)) {
|
|
295
|
+
return trailingMaster;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Tokenize a markdown cell — mirrors the feature-grounding tokenizer.
|
|
302
|
+
* Matches backticked spans, bare CamelCase / kebab-case identifiers,
|
|
303
|
+
* and path-like tokens. Strips method call parens and array indexers.
|
|
304
|
+
*/
|
|
305
|
+
function tokenize(cell) {
|
|
306
|
+
const out = new Set();
|
|
307
|
+
for (const match of cell.matchAll(/`([^`]+)`/g)) {
|
|
308
|
+
const inner = match[1] ?? "";
|
|
309
|
+
addCandidatesFromToken(inner, out);
|
|
310
|
+
}
|
|
311
|
+
const stripped = cell.replace(/`[^`]+`/g, " ");
|
|
312
|
+
for (const match of stripped.matchAll(/[A-Za-z][A-Za-z0-9._@/-]*[A-Za-z0-9]/g)) {
|
|
313
|
+
addCandidatesFromToken(match[0], out);
|
|
314
|
+
}
|
|
315
|
+
return [...out];
|
|
316
|
+
}
|
|
317
|
+
function addCandidatesFromToken(token, out) {
|
|
318
|
+
if (token === "")
|
|
319
|
+
return;
|
|
320
|
+
if (token.includes("/")) {
|
|
321
|
+
out.add(token);
|
|
322
|
+
const first = token.split("/")[0] ?? "";
|
|
323
|
+
if (first !== "" && first !== token)
|
|
324
|
+
out.add(first);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const head = token.split(/[(.[]/)[0] ?? "";
|
|
328
|
+
if (head !== "")
|
|
329
|
+
out.add(head);
|
|
330
|
+
out.add(token);
|
|
331
|
+
}
|
|
332
|
+
/** Candidate tokens that came from backticked code spans only. */
|
|
333
|
+
function codeSpanTokens(cell) {
|
|
334
|
+
const out = new Set();
|
|
335
|
+
for (const match of cell.matchAll(/`([^`]+)`/g))
|
|
336
|
+
addCandidatesFromToken(match[1] ?? "", out);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
/** A letters-only word — ordinary prose unless it is written as code. */
|
|
340
|
+
const isPlainWord = (token) => /^[A-Za-z]+$/.test(token);
|
|
341
|
+
function normaliseToken(token) {
|
|
342
|
+
return token
|
|
343
|
+
.toLowerCase()
|
|
344
|
+
.replace(/\.(ts|tsx|js|jsx|java|kt|kts|py|go|rs|yaml|yml|json|md)$/i, "")
|
|
345
|
+
.replace(/\(\)$/, "")
|
|
346
|
+
.trim();
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=capability-spec.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// container_dependency member-tier builder — F19 (M-ADR 0189 §4.4/F17
|
|
2
|
+
// follow-on, schema 2.33.1).
|
|
3
|
+
//
|
|
4
|
+
// F17 made the aggregator's adjudication leg the ONLY source of
|
|
5
|
+
// container_dependency rows — populated exactly when an author
|
|
6
|
+
// mis-declared an internal container as `System_Ext`. An author who wrote
|
|
7
|
+
// the C4-correct thing instead — `Rel(container_a, container_b)` between
|
|
8
|
+
// two `Container(...)` nodes — got no row and no finding: the metamodel
|
|
9
|
+
// rewarded the wrong authoring and silently discarded the right one,
|
|
10
|
+
// inverting the incentive F17 was built to create.
|
|
11
|
+
//
|
|
12
|
+
// This builder harvests intra-SDD container→container `Rel(...)` lines:
|
|
13
|
+
// both endpoints must resolve to local containers (via the entry-id map
|
|
14
|
+
// the c4-containers builder fills), neither may be external, and the
|
|
15
|
+
// syndicated platform L1 is skipped for non-root SDDs (F14 — its Rel
|
|
16
|
+
// lines are the platform's statements). The adjudication leg then MERGES
|
|
17
|
+
// into a catalogue that already has content, rather than being its only
|
|
18
|
+
// source; dedup is by id, member rows first.
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { collectC4DiagramFiles } from "./c4-external-systems.js";
|
|
21
|
+
import { mergeEdgeLabel } from "./edge-label-merge.js";
|
|
22
|
+
import { techContradicts } from "./c4-containers.js";
|
|
23
|
+
const REL_RE = /^(?:Bi)?Rel(?:_[A-Za-z]+)?\s*\(\s*(\w+)\s*,\s*(\w+)\s*(?:,\s*"([^"]*)")?/;
|
|
24
|
+
export async function buildContainerDependency(inputs) {
|
|
25
|
+
if (inputs.containerIdByEntryId.size === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const isRoot = inputs.manifest.category === "platform-root";
|
|
28
|
+
const files = await collectC4DiagramFiles(inputs.sddPath);
|
|
29
|
+
const rows = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const rowsById = new Map();
|
|
32
|
+
const PERSON_DECL_RE = /Person(?:_Ext)?\s*\(\s*(\w+)\s*,/;
|
|
33
|
+
for (const { abs, syndicated } of files) {
|
|
34
|
+
if (syndicated && !isRoot)
|
|
35
|
+
continue;
|
|
36
|
+
const content = await readFile(abs, "utf-8");
|
|
37
|
+
// F26: per-file Person entries — the endpoint class that has no edge
|
|
38
|
+
// catalogue to a container.
|
|
39
|
+
const personEntries = new Set();
|
|
40
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
41
|
+
const pm = PERSON_DECL_RE.exec(raw.trim());
|
|
42
|
+
if (pm)
|
|
43
|
+
personEntries.add(pm[1] ?? "");
|
|
44
|
+
}
|
|
45
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
46
|
+
const m = REL_RE.exec(raw.trim());
|
|
47
|
+
if (!m)
|
|
48
|
+
continue;
|
|
49
|
+
const [, fromEntry, toEntry, label] = m;
|
|
50
|
+
// F20: resolve the container FIRST. Both maps are SDD-wide, and an
|
|
51
|
+
// entry-id that resolves to a local container IS a container —
|
|
52
|
+
// whatever a sibling diagram in the same SDD called it. The old
|
|
53
|
+
// external-guard-first order silently discarded a true edge whenever
|
|
54
|
+
// the id was also used for a System_Ext elsewhere in the SDD, which
|
|
55
|
+
// is precisely the shape shared load-bearing containers produce
|
|
56
|
+
// (measured: the estate's commonest collision class). The sibling's
|
|
57
|
+
// System_Ext usage is separately harvested and adjudicated; a
|
|
58
|
+
// converted candidate's rewritten edge dedups against the member row
|
|
59
|
+
// by id, so nothing is double-represented.
|
|
60
|
+
const from = inputs.containerIdByEntryId.get(fromEntry ?? "");
|
|
61
|
+
const to = inputs.containerIdByEntryId.get(toEntry ?? "");
|
|
62
|
+
if (from === undefined || to === undefined) {
|
|
63
|
+
// F26: a Rel the builder RESOLVES but cannot catalogue must say so
|
|
64
|
+
// — following F23's rename prescription turned exactly this
|
|
65
|
+
// silence into a two-edge delete. Classification is
|
|
66
|
+
// container-first (F20), then external, then Person (file-local);
|
|
67
|
+
// anything unknown stays silent (no confident classification, no
|
|
68
|
+
// guess). Pairs with a home elsewhere (one-external →
|
|
69
|
+
// system_external_dependency; person→system → persona_c4system)
|
|
70
|
+
// are not reported here.
|
|
71
|
+
const classify = (entry, resolved) => resolved !== undefined
|
|
72
|
+
? "container"
|
|
73
|
+
: inputs.externalIdByEntryId?.has(entry)
|
|
74
|
+
? "external"
|
|
75
|
+
: personEntries.has(entry)
|
|
76
|
+
? "person"
|
|
77
|
+
: "unknown";
|
|
78
|
+
const fromKind = classify(fromEntry ?? "", from);
|
|
79
|
+
const toKind = classify(toEntry ?? "", to);
|
|
80
|
+
const pair = `${fromKind}→${toKind}`;
|
|
81
|
+
// person↔container pairs are catalogued by persona_container as of
|
|
82
|
+
// schema 2.34.0 (or reported there when the persona doesn't
|
|
83
|
+
// resolve) — only the pairs with NO home remain findings here.
|
|
84
|
+
if (pair === "external→external") {
|
|
85
|
+
inputs.findings.add("convention", `container_dependency: Rel(${fromEntry}, ${toEntry}${label ? `, "${label}"` : ""}) resolves as ` +
|
|
86
|
+
`${pair} — no catalogue holds that pair; the relationship is recorded nowhere (F26)`);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (from === to)
|
|
91
|
+
continue;
|
|
92
|
+
const id = `${from}__${to}`;
|
|
93
|
+
const existing = rowsById.get(id);
|
|
94
|
+
if (existing !== undefined) {
|
|
95
|
+
mergeEdgeLabel(existing, label ?? "", "container_dependency", inputs.findings);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
seen.add(id);
|
|
99
|
+
rowsById.set(id, {
|
|
100
|
+
id,
|
|
101
|
+
from_container_id: from,
|
|
102
|
+
to_container_id: to,
|
|
103
|
+
interaction: label ?? "",
|
|
104
|
+
source_sdd_id: inputs.manifest.sddId,
|
|
105
|
+
});
|
|
106
|
+
rows.push(rowsById.get(id));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
rows.sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* F21 referent-collision check — corroborate on an INDEPENDENT evidence
|
|
114
|
+
* channel, never the one under guard. The F20-era version decided referent
|
|
115
|
+
* identity by comparing display names — the same evidence class the guard
|
|
116
|
+
* existed to distrust — so it was circular: the estate sweep that declared
|
|
117
|
+
* different-referent collisions absent USED this predicate and could not
|
|
118
|
+
* have found the counterexample it then declared inexercisable. Measured:
|
|
119
|
+
* nested names (a client layer and the server it proxies, names nesting)
|
|
120
|
+
* are DIFFERENT referents and stayed silent; acronym-vs-expansion is the
|
|
121
|
+
* SAME referent and fired. The predicate was inverted on the two
|
|
122
|
+
* name-shapes the estate actually uses.
|
|
123
|
+
*
|
|
124
|
+
* The independent channel is the technology string: ".NET 6 (in Linton)"
|
|
125
|
+
* against "Java" is a flat contradiction and needs no NLP. A dual-use
|
|
126
|
+
* entry-id (Container + System_Ext in one SDD) whose technologies
|
|
127
|
+
* contradict draws the finding REGARDLESS of what the names say — name
|
|
128
|
+
* agreement is precisely what makes the collision quiet today. Where no
|
|
129
|
+
* technology evidence exists on either side, silence: absence of evidence
|
|
130
|
+
* is not corroboration, and it is not contradiction either.
|
|
131
|
+
*/
|
|
132
|
+
export function reportEntryIdReferentCollisions(inputs) {
|
|
133
|
+
const techTokens = (s) => new Set(s
|
|
134
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
135
|
+
.toLowerCase()
|
|
136
|
+
.split(/[^a-z0-9.+#]+/)
|
|
137
|
+
.filter((t) => t !== ""));
|
|
138
|
+
for (const [entryId, containerName] of inputs.containerNameByEntryId) {
|
|
139
|
+
const externalName = inputs.externalNameByEntryId.get(entryId);
|
|
140
|
+
if (externalName === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
const containerTech = inputs.containerTechByEntryId.get(entryId) ?? "";
|
|
143
|
+
const externalTech = inputs.externalTechByEntryId.get(entryId) ?? "";
|
|
144
|
+
if (externalTech !== "" && techContradicts(containerTech, externalTech)) {
|
|
145
|
+
inputs.findings.add("convention", `c4 diagrams: entry-id "${entryId}" is declared Container ("${containerName}", "${containerTech}") and ` +
|
|
146
|
+
`System_Ext ("${externalName}", "${externalTech}") with contradictory technologies — ` +
|
|
147
|
+
`different referents wearing one id; the container interpretation wins for Rel edges — rename one entry-id to split them (F21)`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (externalTech !== "")
|
|
151
|
+
continue; // corroborating tech — same referent, silent
|
|
152
|
+
// F23: System_Ext carries no technology argument, so the primary
|
|
153
|
+
// dual-use case has NO independent evidence channel — measured: 0 of
|
|
154
|
+
// 109 external rows carry a technology. Weak corroboration is
|
|
155
|
+
// accepted (the external's description mentioning a container-tech
|
|
156
|
+
// token); otherwise the guard says what it cannot do, loudly. A
|
|
157
|
+
// dual-use entry-id buys nothing post-F20 (the container always wins
|
|
158
|
+
// for Rel edges) and risks a silent different-referent bind — the
|
|
159
|
+
// retirement is a rename, cheap because entry-ids are diagram-local
|
|
160
|
+
// and row identity derives from the display name.
|
|
161
|
+
const externalDesc = inputs.externalDescByEntryId.get(entryId) ?? "";
|
|
162
|
+
if (containerTech !== "" && externalDesc !== "") {
|
|
163
|
+
const ct = techTokens(containerTech);
|
|
164
|
+
const dt = techTokens(externalDesc);
|
|
165
|
+
let overlap = false;
|
|
166
|
+
for (const t of ct)
|
|
167
|
+
if (dt.has(t)) {
|
|
168
|
+
overlap = true;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (overlap)
|
|
172
|
+
continue; // description mentions the container's tech — weak corroboration
|
|
173
|
+
}
|
|
174
|
+
inputs.findings.add("convention", `c4 diagrams: entry-id "${entryId}" is declared Container ("${containerName}") and System_Ext ` +
|
|
175
|
+
`("${externalName}") with no independent evidence to adjudicate referent identity — System_Ext ` +
|
|
176
|
+
`carries no technology field; cannot corroborate, cannot contradict. Rename one entry-id to split ` +
|
|
177
|
+
`the namespaces — but check first whether the dual use is load-bearing: if Rel edges exist only via ` +
|
|
178
|
+
`the Container reading, a rename orphans them (they will surface as F26 findings); in that case ` +
|
|
179
|
+
`annotate the System_Ext description with the technology instead, so weak corroboration can silence ` +
|
|
180
|
+
`this honestly (F23/F26)`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=container-dependency.js.map
|