@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,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-grounding extractor per ADR 0143 (schema 2.5.0).
|
|
3
|
+
*
|
|
4
|
+
* Walks `docs/domains/<dom>/design/<capability>/feature-grounding.md`
|
|
5
|
+
* (platform-profile) and `docs/design/integration/feature-grounding.md`
|
|
6
|
+
* (integration-product), parses the canonical markdown table, and
|
|
7
|
+
* emits four catalogue contributions:
|
|
8
|
+
*
|
|
9
|
+
* - `features.csv` rows with `source: feature-grounding`.
|
|
10
|
+
* - `capability_feature.csv` rows (1:1 with each feature row).
|
|
11
|
+
* - `feature_repo.csv` rows derived from file-path references in
|
|
12
|
+
* the Evidence column.
|
|
13
|
+
* - `feature_service.csv` rows derived from class names / file
|
|
14
|
+
* basenames / route fragments in API Routes / Backend Handlers
|
|
15
|
+
* / Evidence columns, resolved via `services.csv` + the optional
|
|
16
|
+
* per-SDD alias map.
|
|
17
|
+
*
|
|
18
|
+
* Resolution failures (unresolved capability, service, repo) emit
|
|
19
|
+
* findings but never break FK validation — only confidently-resolved
|
|
20
|
+
* rows are emitted.
|
|
21
|
+
*
|
|
22
|
+
* Precedence per ADR 0143 §4.4: feature-tree and feature-spec rows
|
|
23
|
+
* win on id collision; grounding rows are skipped with a divergence
|
|
24
|
+
* finding.
|
|
25
|
+
*/
|
|
26
|
+
import { readFile } from "node:fs/promises";
|
|
27
|
+
import { relative } from "node:path";
|
|
28
|
+
import { capabilityIdFromPath, findIntegrationGroundingFile, findPlatformGroundingFiles, parseGroundingTable, } from "./feature-grounding-walker.js";
|
|
29
|
+
const DESCRIPTION_MAX = 500;
|
|
30
|
+
/** Columns the service-name resolver scans for class names / route fragments. */
|
|
31
|
+
const SERVICE_RESOLUTION_COLUMNS = [
|
|
32
|
+
"ui_components",
|
|
33
|
+
"api_routes",
|
|
34
|
+
"backend_handlers",
|
|
35
|
+
"evidence",
|
|
36
|
+
];
|
|
37
|
+
/** Columns the repo-name resolver scans for file-path references. */
|
|
38
|
+
const REPO_RESOLUTION_COLUMNS = ["evidence", "ui_components"];
|
|
39
|
+
export async function buildFeatureGrounding(inputs) {
|
|
40
|
+
const featureRows = [];
|
|
41
|
+
const capabilityFeatureRows = [];
|
|
42
|
+
const featureRepoRows = [];
|
|
43
|
+
// M-ADR 0143 §4.4 (amended): rows whose Evidence cell is non-empty but resolves to
|
|
44
|
+
// ZERO repos. Counted for the summary finding below; each row also gets its own
|
|
45
|
+
// line. This measures evidence→repo RESOLUTION COVERAGE, not correctness.
|
|
46
|
+
let repoEvidenceRows = 0;
|
|
47
|
+
const repoZeroResolutionFeatureIds = [];
|
|
48
|
+
const featureServiceRows = [];
|
|
49
|
+
const seenFeatureRepo = new Set();
|
|
50
|
+
const seenFeatureService = new Set();
|
|
51
|
+
const seenFeatures = new Set(inputs.existingFeatureIds);
|
|
52
|
+
// Kept separate from `seenFeatures`, which grows during this walk. The two
|
|
53
|
+
// sets are what distinguish a BY-DESIGN cross-source collision from a REAL
|
|
54
|
+
// within-source one — see the split at the collision site below.
|
|
55
|
+
const fromEarlierSource = new Set(inputs.existingFeatureIds);
|
|
56
|
+
const files = [];
|
|
57
|
+
if (inputs.profile === "platform-profile") {
|
|
58
|
+
for (const absPath of await findPlatformGroundingFiles(inputs.sddPath)) {
|
|
59
|
+
const capId = capabilityIdFromPath(absPath, inputs.sddPath);
|
|
60
|
+
files.push({ absPath, capabilityId: capId });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (inputs.profile === "integration-product") {
|
|
64
|
+
const absPath = await findIntegrationGroundingFile(inputs.sddPath);
|
|
65
|
+
if (absPath !== null) {
|
|
66
|
+
// For integration-product, the integration itself is the
|
|
67
|
+
// capability surface; use the SDD's product id.
|
|
68
|
+
files.push({ absPath, capabilityId: inputs.productId });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const { absPath, capabilityId: pathCapabilityId } of files) {
|
|
72
|
+
const relPath = relative(inputs.sddPath, absPath);
|
|
73
|
+
if (pathCapabilityId === "") {
|
|
74
|
+
inputs.findings.add("convention", `${relPath}: capability id could not be resolved from file path`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Resolve a design-dir slug that is not itself a capability id via the
|
|
78
|
+
// slug → id alias map (spec / feature-directory basename → declared id).
|
|
79
|
+
const capabilityId = inputs.knownCapabilityIds.has(pathCapabilityId)
|
|
80
|
+
? pathCapabilityId
|
|
81
|
+
: (inputs.capabilitySlugAliases?.get(pathCapabilityId) ?? pathCapabilityId);
|
|
82
|
+
if (!inputs.knownCapabilityIds.has(capabilityId)) {
|
|
83
|
+
inputs.findings.add("convention", `${relPath}: capability "${pathCapabilityId}" from file path does not resolve against capabilities.csv`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const text = await readFile(absPath, "utf-8");
|
|
87
|
+
const rows = parseGroundingTable(text);
|
|
88
|
+
if (rows.length === 0) {
|
|
89
|
+
inputs.findings.add("coverage", `${relPath}: feature-grounding.md found but no recognisable Feature-to-Code Mapping table`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const slugsInThisFile = new Set();
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
if (slugsInThisFile.has(row.slug)) {
|
|
95
|
+
inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: duplicate feature slug "${row.slug}" in same file (first occurrence wins)`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
slugsInThisFile.add(row.slug);
|
|
99
|
+
const featureId = `${capabilityId}-${row.slug}`;
|
|
100
|
+
const featureRowCollides = seenFeatures.has(featureId);
|
|
101
|
+
// Feature row + capability_feature edge: emit only on first sight.
|
|
102
|
+
// feature-tree / feature-spec win the row identity when they declared
|
|
103
|
+
// it first — they carry runway / group / parent context that
|
|
104
|
+
// grounding doesn't, plus capability-spec.ts already emitted the
|
|
105
|
+
// capability_feature row on the spec source path.
|
|
106
|
+
//
|
|
107
|
+
// feature_repo and feature_service edges: emit unconditionally per
|
|
108
|
+
// ADR 0143 §2 (these are the source's unique contribution; nothing
|
|
109
|
+
// else carries per-row code-mapping). Pre-0.30.2 the collision skip
|
|
110
|
+
// dropped these edges too — bug fixed per
|
|
111
|
+
// `coordination/feature-grounding-collision-edges-coordination.md`.
|
|
112
|
+
if (featureRowCollides) {
|
|
113
|
+
// ONE CHECK, TWO MEANINGS — split them, because they are not the same
|
|
114
|
+
// event and only one of them is a defect.
|
|
115
|
+
//
|
|
116
|
+
// `seenFeatures` is seeded from earlier sources AND grows during this
|
|
117
|
+
// walk, so a hit means either:
|
|
118
|
+
// - an earlier SOURCE emitted this id. That is ADR 0143 §2's
|
|
119
|
+
// documented precedence rule working exactly as specified: the
|
|
120
|
+
// grounding pass re-declares ids that feature-tree/feature-spec
|
|
121
|
+
// already own. It happens for essentially every grounded feature,
|
|
122
|
+
// measured at 1,545 standing instances across two estates, and it
|
|
123
|
+
// is correct output. Reported as `inferred` (the category
|
|
124
|
+
// feature-contract.ts already uses for the identical
|
|
125
|
+
// both-paths-produced-this dedup shape, and one the gate does not
|
|
126
|
+
// act on).
|
|
127
|
+
// - an earlier FILE in THIS source pass emitted it — two
|
|
128
|
+
// feature-grounding files under one capability declaring the same
|
|
129
|
+
// slug. That is a real authoring collision and stays `divergence`,
|
|
130
|
+
// matching the within-one-file duplicate check above.
|
|
131
|
+
//
|
|
132
|
+
// Before this split the gate's contract ("fail on any new divergence
|
|
133
|
+
// line") could not work here: a real collision arrived inside a crowd
|
|
134
|
+
// of 1,545 accepted ones, so the whole category was unusable as a
|
|
135
|
+
// signal and unbaselineable as a backlog.
|
|
136
|
+
if (fromEarlierSource.has(featureId)) {
|
|
137
|
+
inputs.findings.add("inferred", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by an earlier source (feature-tree / feature-spec wins for the row; feature_repo + feature_service edges still emitted per ADR 0143 §2)`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
inputs.findings.add("divergence", `${relPath}:${row.lineNumber}: feature id "${featureId}" already emitted by another file in this same feature-grounding pass — two files under one capability declare the same slug (first occurrence wins)`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
seenFeatures.add(featureId);
|
|
145
|
+
featureRows.push({
|
|
146
|
+
id: featureId,
|
|
147
|
+
name: row.slug,
|
|
148
|
+
product_id: "",
|
|
149
|
+
capability_id: capabilityId,
|
|
150
|
+
parent_group_id: "",
|
|
151
|
+
parent_feature_id: "",
|
|
152
|
+
description: deriveDescription(row),
|
|
153
|
+
maturity: "",
|
|
154
|
+
status: "active",
|
|
155
|
+
source: "feature-grounding",
|
|
156
|
+
runway: "",
|
|
157
|
+
source_sdd_id: "",
|
|
158
|
+
});
|
|
159
|
+
const cfId = `${capabilityId}__${featureId}`;
|
|
160
|
+
capabilityFeatureRows.push({
|
|
161
|
+
id: cfId,
|
|
162
|
+
capability_id: capabilityId,
|
|
163
|
+
feature_id: featureId,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// feature_service edges — emit regardless of row collision.
|
|
167
|
+
const services = resolveServices(row, inputs);
|
|
168
|
+
for (const serviceId of services) {
|
|
169
|
+
const edgeId = `${featureId}__${serviceId}`;
|
|
170
|
+
if (seenFeatureService.has(edgeId))
|
|
171
|
+
continue;
|
|
172
|
+
seenFeatureService.add(edgeId);
|
|
173
|
+
featureServiceRows.push({
|
|
174
|
+
id: edgeId,
|
|
175
|
+
feature_id: featureId,
|
|
176
|
+
service_id: serviceId,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// feature_repo edges — emit regardless of row collision.
|
|
180
|
+
const repos = resolveRepos(row, inputs);
|
|
181
|
+
if ((row.cells.get("evidence") ?? "").trim() !== "") {
|
|
182
|
+
repoEvidenceRows++;
|
|
183
|
+
if (repos.length === 0) {
|
|
184
|
+
repoZeroResolutionFeatureIds.push(featureId);
|
|
185
|
+
inputs.findings.add("coverage", `${relPath}:${row.lineNumber}: feature "${featureId}" — Evidence 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)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const repoKey of repos) {
|
|
189
|
+
const edgeId = `${featureId}__${repoKey}`;
|
|
190
|
+
if (seenFeatureRepo.has(edgeId))
|
|
191
|
+
continue;
|
|
192
|
+
seenFeatureRepo.add(edgeId);
|
|
193
|
+
featureRepoRows.push({
|
|
194
|
+
id: edgeId,
|
|
195
|
+
feature_id: featureId,
|
|
196
|
+
repo_id: repoKey,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Summary emitted by the build after feature_repo is final (split against
|
|
202
|
+
// features left with NO anchor from any surface).
|
|
203
|
+
return {
|
|
204
|
+
featureRows,
|
|
205
|
+
capabilityFeatureRows,
|
|
206
|
+
featureRepoRows,
|
|
207
|
+
featureServiceRows,
|
|
208
|
+
repoEvidenceRows,
|
|
209
|
+
repoZeroResolutionFeatureIds,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Build the description column by joining non-feature cells, capped at
|
|
214
|
+
* 500 chars to match the feature-tree bullet convention.
|
|
215
|
+
*/
|
|
216
|
+
function deriveDescription(row) {
|
|
217
|
+
const parts = [];
|
|
218
|
+
for (const [key, value] of row.cells) {
|
|
219
|
+
if (value === "")
|
|
220
|
+
continue;
|
|
221
|
+
parts.push(`${prettyHeader(key)}: ${value}`);
|
|
222
|
+
}
|
|
223
|
+
const joined = parts.join(" • ");
|
|
224
|
+
return joined.length > DESCRIPTION_MAX
|
|
225
|
+
? joined.slice(0, DESCRIPTION_MAX)
|
|
226
|
+
: joined;
|
|
227
|
+
}
|
|
228
|
+
function prettyHeader(key) {
|
|
229
|
+
return key
|
|
230
|
+
.split("_")
|
|
231
|
+
.map((p) => (p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
232
|
+
.join(" ");
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolve service ids referenced in a row's cells. Strategy:
|
|
236
|
+
* - Tokenize backtick-quoted identifiers + CamelCase / kebab-case tokens.
|
|
237
|
+
* - Strip extensions (.ts/.java/.yaml/.kt/.py) and trailing parens.
|
|
238
|
+
* - Try exact match against services.id (lowercased).
|
|
239
|
+
* - Try alias map (case-sensitive — display names often carry case).
|
|
240
|
+
*/
|
|
241
|
+
function resolveServices(row, inputs) {
|
|
242
|
+
const out = new Set();
|
|
243
|
+
for (const col of SERVICE_RESOLUTION_COLUMNS) {
|
|
244
|
+
const cell = row.cells.get(col) ?? "";
|
|
245
|
+
if (cell === "")
|
|
246
|
+
continue;
|
|
247
|
+
for (const token of tokenize(cell)) {
|
|
248
|
+
const resolved = resolveServiceToken(token, inputs);
|
|
249
|
+
if (resolved !== null)
|
|
250
|
+
out.add(resolved);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return [...out].sort();
|
|
254
|
+
}
|
|
255
|
+
function resolveServiceToken(rawToken, inputs) {
|
|
256
|
+
// Try alias first (case-sensitive).
|
|
257
|
+
if (inputs.aliases !== null) {
|
|
258
|
+
const aliased = inputs.aliases.serviceAliases.get(rawToken);
|
|
259
|
+
if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
|
|
260
|
+
return aliased;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Normalise + try exact match.
|
|
264
|
+
const normalised = normaliseToken(rawToken);
|
|
265
|
+
if (normalised === "")
|
|
266
|
+
return null;
|
|
267
|
+
if (inputs.knownServiceIds.has(normalised))
|
|
268
|
+
return normalised;
|
|
269
|
+
// Try alias on normalised form too.
|
|
270
|
+
if (inputs.aliases !== null) {
|
|
271
|
+
const aliased = inputs.aliases.serviceAliases.get(normalised);
|
|
272
|
+
if (aliased !== undefined && inputs.knownServiceIds.has(aliased)) {
|
|
273
|
+
return aliased;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve repo ids from file-path references. Strategy:
|
|
280
|
+
* - Tokenize path-like spans (contain `/` or end in known extensions).
|
|
281
|
+
* - For each token: try longest-prefix match against
|
|
282
|
+
* `canonical_repo_key`s; e.g. `example-service-api-master@abc/src/...`
|
|
283
|
+
* → `example-service-api` (after stripping branch/sha suffix).
|
|
284
|
+
* - Also accept the alias map for display-name → canonical-key.
|
|
285
|
+
*/
|
|
286
|
+
function resolveRepos(row, inputs) {
|
|
287
|
+
const out = new Set();
|
|
288
|
+
for (const col of REPO_RESOLUTION_COLUMNS) {
|
|
289
|
+
const cell = row.cells.get(col) ?? "";
|
|
290
|
+
if (cell === "")
|
|
291
|
+
continue;
|
|
292
|
+
const coded = inputs.repoTokenPolicy === "identifier" ? codeSpanTokens(cell) : null;
|
|
293
|
+
for (const token of tokenize(cell)) {
|
|
294
|
+
const resolved = resolveRepoToken(token, inputs);
|
|
295
|
+
if (resolved === null)
|
|
296
|
+
continue;
|
|
297
|
+
if (coded !== null && isPlainWord(token) && !coded.has(token) && inputs.aliases?.repoAliases.get(token) !== resolved)
|
|
298
|
+
continue;
|
|
299
|
+
out.add(resolved);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return [...out].sort();
|
|
303
|
+
}
|
|
304
|
+
function resolveRepoToken(rawToken, inputs) {
|
|
305
|
+
// Try alias first.
|
|
306
|
+
if (inputs.aliases !== null) {
|
|
307
|
+
const aliased = inputs.aliases.repoAliases.get(rawToken);
|
|
308
|
+
if (aliased !== undefined && inputs.knownRepoKeys.has(aliased)) {
|
|
309
|
+
return aliased;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Direct match on the full token (rare).
|
|
313
|
+
if (inputs.knownRepoKeys.has(rawToken))
|
|
314
|
+
return rawToken;
|
|
315
|
+
// Walk every path segment and try to recover a repo key. Each
|
|
316
|
+
// segment may carry a `-master@<sha>` or `@<sha>` suffix; strip
|
|
317
|
+
// those before matching.
|
|
318
|
+
for (const segment of rawToken.split("/")) {
|
|
319
|
+
const stripped = segment
|
|
320
|
+
.replace(/-master@.*$/, "")
|
|
321
|
+
.replace(/@[^/]+$/, "");
|
|
322
|
+
if (stripped !== "" && inputs.knownRepoKeys.has(stripped)) {
|
|
323
|
+
return stripped;
|
|
324
|
+
}
|
|
325
|
+
// Also try the segment with the `-master` literal removed (no @sha).
|
|
326
|
+
const trailingMaster = stripped.replace(/-master$/, "");
|
|
327
|
+
if (trailingMaster !== stripped &&
|
|
328
|
+
inputs.knownRepoKeys.has(trailingMaster)) {
|
|
329
|
+
return trailingMaster;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Extract candidate identifier tokens from a markdown cell. Matches:
|
|
336
|
+
* - Backticked spans: `` `AuthHandler.handleLoginRequest()` ``
|
|
337
|
+
* - Bare identifiers: `ExampleOauthService`
|
|
338
|
+
* - kebab-case identifiers: `example-service-api`
|
|
339
|
+
* - Path-like tokens with at least one `/`.
|
|
340
|
+
* Strips leading method names (e.g. `AuthHandler.foo()` → `AuthHandler`).
|
|
341
|
+
*/
|
|
342
|
+
function tokenize(cell) {
|
|
343
|
+
const out = new Set();
|
|
344
|
+
// Backticked spans.
|
|
345
|
+
for (const match of cell.matchAll(/`([^`]+)`/g)) {
|
|
346
|
+
const inner = match[1] ?? "";
|
|
347
|
+
addCandidatesFromToken(inner, out);
|
|
348
|
+
}
|
|
349
|
+
// Bare identifiers outside backticks — strip backtick spans first.
|
|
350
|
+
const stripped = cell.replace(/`[^`]+`/g, " ");
|
|
351
|
+
for (const match of stripped.matchAll(/[A-Za-z][A-Za-z0-9._@/-]*[A-Za-z0-9]/g)) {
|
|
352
|
+
addCandidatesFromToken(match[0], out);
|
|
353
|
+
}
|
|
354
|
+
return [...out];
|
|
355
|
+
}
|
|
356
|
+
function addCandidatesFromToken(token, out) {
|
|
357
|
+
if (token === "")
|
|
358
|
+
return;
|
|
359
|
+
// Path token: take first segment as a candidate AND keep the full path.
|
|
360
|
+
if (token.includes("/")) {
|
|
361
|
+
out.add(token);
|
|
362
|
+
const first = token.split("/")[0] ?? "";
|
|
363
|
+
if (first !== "" && first !== token)
|
|
364
|
+
out.add(first);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Method/class form: split on `.`, contribute each segment except (), [].
|
|
368
|
+
const head = token.split(/[(.[]/)[0] ?? "";
|
|
369
|
+
if (head !== "")
|
|
370
|
+
out.add(head);
|
|
371
|
+
// Also keep the full token for alias-map matching on exact display
|
|
372
|
+
// strings like `AuthHandler.handleLoginRequest`.
|
|
373
|
+
out.add(token);
|
|
374
|
+
}
|
|
375
|
+
/** Candidate tokens that came from backticked code spans only. */
|
|
376
|
+
function codeSpanTokens(cell) {
|
|
377
|
+
const out = new Set();
|
|
378
|
+
for (const match of cell.matchAll(/`([^`]+)`/g))
|
|
379
|
+
addCandidatesFromToken(match[1] ?? "", out);
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
/** A letters-only word — ordinary prose unless it is written as code. */
|
|
383
|
+
const isPlainWord = (token) => /^[A-Za-z]+$/.test(token);
|
|
384
|
+
function normaliseToken(token) {
|
|
385
|
+
return token
|
|
386
|
+
.toLowerCase()
|
|
387
|
+
.replace(/\.(ts|tsx|js|jsx|java|kt|kts|py|go|rs|yaml|yml|json|md)$/i, "")
|
|
388
|
+
.replace(/\(\)$/, "")
|
|
389
|
+
.trim();
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=feature-grounding.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// FeatureGroups discoverer — emits `feature_groups.csv` rows from
|
|
2
|
+
// hierarchical `feature-tree.md` files.
|
|
3
|
+
//
|
|
4
|
+
// Per Q7.2 resolution (adopt-canonical-catalogue-builder.md §7.2),
|
|
5
|
+
// FeatureGroups are first-class entities. This builder consumes the
|
|
6
|
+
// shared `feature-tree-walker` and emits one row per H2/H3 heading
|
|
7
|
+
// outside fenced code blocks.
|
|
8
|
+
//
|
|
9
|
+
// ASCII-tree representations inside fenced code blocks are NOT parsed
|
|
10
|
+
// — those are an adopter authoring style that doesn't map cleanly to
|
|
11
|
+
// catalogue entities. Adopters wanting first-class groups format their
|
|
12
|
+
// feature trees with proper H2/H3 headings.
|
|
13
|
+
//
|
|
14
|
+
// Source paths: see `findFeatureTreeFile` in feature-tree-walker.ts.
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { relative } from "node:path";
|
|
17
|
+
import { featureGroupId, findFeatureTreeFile, walkFeatureTree, } from "./feature-tree-walker.js";
|
|
18
|
+
export async function buildFeatureGroups(inputs) {
|
|
19
|
+
const featureTreePath = await findFeatureTreeFile(inputs.sddPath);
|
|
20
|
+
if (!featureTreePath) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const text = await readFile(featureTreePath, "utf-8");
|
|
24
|
+
const events = walkFeatureTree(text);
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (const ev of events) {
|
|
27
|
+
if (ev.kind === "h2") {
|
|
28
|
+
rows.push({
|
|
29
|
+
id: featureGroupId(inputs.productId, ev.slug),
|
|
30
|
+
name: ev.text,
|
|
31
|
+
product_id: inputs.productId,
|
|
32
|
+
capability_id: "",
|
|
33
|
+
parent_group_id: "",
|
|
34
|
+
description: "",
|
|
35
|
+
source: "feature-tree-h2",
|
|
36
|
+
source_sdd_id: "",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (ev.kind === "h3") {
|
|
40
|
+
if (ev.parentH2Slug === "") {
|
|
41
|
+
// H3 with no enclosing H2 — orphan; surface as finding but emit anyway.
|
|
42
|
+
inputs.findings.add("convention", `${relative(inputs.sddPath, featureTreePath)}: H3 "${ev.text}" has no enclosing H2`);
|
|
43
|
+
}
|
|
44
|
+
const id = ev.parentH2Slug !== ""
|
|
45
|
+
? featureGroupId(inputs.productId, ev.parentH2Slug, ev.slug)
|
|
46
|
+
: featureGroupId(inputs.productId, ev.slug);
|
|
47
|
+
rows.push({
|
|
48
|
+
id,
|
|
49
|
+
name: ev.text,
|
|
50
|
+
product_id: inputs.productId,
|
|
51
|
+
capability_id: "",
|
|
52
|
+
parent_group_id: ev.parentH2Slug !== ""
|
|
53
|
+
? featureGroupId(inputs.productId, ev.parentH2Slug)
|
|
54
|
+
: "",
|
|
55
|
+
description: "",
|
|
56
|
+
source: "feature-tree-h3",
|
|
57
|
+
source_sdd_id: "",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=feature-groups.js.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// feature_subscription_channels builder — emits one row per
|
|
2
|
+
// `subscription_channels:` entry in any `reverse-evidence/feature-grounding-evidence.yaml`
|
|
3
|
+
// file in the SDD. Added in schema 2.9.0.
|
|
4
|
+
//
|
|
5
|
+
// Per resolved decision §1 of `content/plans/v0-9-0-cohort-sweep-derived-bundle.md`
|
|
6
|
+
// (see also `docs/method/methods/reverse/rubrics/reverse-application-evidence-quality.md`
|
|
7
|
+
// per-feature slot vocabulary): `subscription_channels:` is a peer of
|
|
8
|
+
// `graphql_operations:` in the per-feature evidence shape — `graphql_operations:`
|
|
9
|
+
// carries the operation names declared in the GraphQL schema; this builder
|
|
10
|
+
// captures the channel/topic layer (AppSync WebSocket, Pekko WS, MQTT, SSE,
|
|
11
|
+
// etc.) that physically delivers push events.
|
|
12
|
+
//
|
|
13
|
+
// Input shape (per the rubric):
|
|
14
|
+
//
|
|
15
|
+
// features:
|
|
16
|
+
// - feature_slug: <slug>
|
|
17
|
+
// subscription_channels:
|
|
18
|
+
// - name: <channel-identifier>
|
|
19
|
+
// transport: appsync-websocket | pekko-ws | mqtt | sse | webhook-push | other
|
|
20
|
+
// evidence: <sub-repo>/<path>:<line>
|
|
21
|
+
// payload_ref: <event-name> # optional
|
|
22
|
+
//
|
|
23
|
+
// Feature-id resolution: the canonical builder emits feature rows with ids
|
|
24
|
+
// shaped `{product_id|capability_id}-{feature_slug}`. Adopters' evidence
|
|
25
|
+
// yamls carry only the bare `feature_slug:`. This builder accepts a set of
|
|
26
|
+
// known feature ids (`existingFeatureIds`) and probes each candidate
|
|
27
|
+
// resolution (`{productId}-{slug}` and `{slug}` and `<capability>-{slug}`
|
|
28
|
+
// for any capability id ending in `-{slug}`). When no candidate resolves,
|
|
29
|
+
// the row is dropped and an `orphan` finding is emitted so reviewers can
|
|
30
|
+
// reconcile.
|
|
31
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
32
|
+
import { join, relative, sep } from "node:path";
|
|
33
|
+
import { parse as parseYaml } from "yaml";
|
|
34
|
+
const VALID_TRANSPORTS = new Set([
|
|
35
|
+
"appsync-websocket",
|
|
36
|
+
"pekko-ws",
|
|
37
|
+
"mqtt",
|
|
38
|
+
"sse",
|
|
39
|
+
"webhook-push",
|
|
40
|
+
"other",
|
|
41
|
+
]);
|
|
42
|
+
export async function buildFeatureSubscriptionChannels(inputs) {
|
|
43
|
+
const rows = [];
|
|
44
|
+
const seenIds = new Set();
|
|
45
|
+
const evidenceFiles = await collectEvidenceFiles(inputs.sddPath);
|
|
46
|
+
for (const file of evidenceFiles) {
|
|
47
|
+
const relPath = relative(inputs.sddPath, file);
|
|
48
|
+
// The per-design-dir `feature-grounding-evidence.yaml` is this builder's
|
|
49
|
+
// dedicated contract — read/parse failures are its findings. The
|
|
50
|
+
// per-domain `<domain>-feature-grounding.yaml` files (ADR 0164) are
|
|
51
|
+
// primarily authored for other tooling and only opportunistically scanned
|
|
52
|
+
// for a `feature_subscription_channels:` key; they may not be strict YAML,
|
|
53
|
+
// so their read/parse failures are skipped silently (not this builder's
|
|
54
|
+
// authority to validate).
|
|
55
|
+
const isCanonical = file.substring(file.lastIndexOf(sep) + 1) === "feature-grounding-evidence.yaml";
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
text = await readFile(file, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
if (isCanonical) {
|
|
62
|
+
inputs.findings.add("convention", `${relPath}: cannot read evidence file`);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = parseYaml(text);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (isCanonical) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
inputs.findings.add("convention", `${relPath}: invalid YAML — ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!parsed || typeof parsed !== "object")
|
|
78
|
+
continue;
|
|
79
|
+
// Per-feature subscription_channels live under `features:` (per-design-dir
|
|
80
|
+
// layout) or a top-level `feature_subscription_channels:` list (per-domain
|
|
81
|
+
// layout, ADR 0164). Both are lists of { feature_slug, subscription_channels }.
|
|
82
|
+
const obj = parsed;
|
|
83
|
+
const featureEntries = [];
|
|
84
|
+
if (Array.isArray(obj["features"]))
|
|
85
|
+
featureEntries.push(...obj["features"]);
|
|
86
|
+
if (Array.isArray(obj["feature_subscription_channels"])) {
|
|
87
|
+
featureEntries.push(...obj["feature_subscription_channels"]);
|
|
88
|
+
}
|
|
89
|
+
if (featureEntries.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
for (const f of featureEntries) {
|
|
92
|
+
if (!f || typeof f !== "object")
|
|
93
|
+
continue;
|
|
94
|
+
const feat = f;
|
|
95
|
+
const slug = String(feat["feature_slug"] ?? "").trim();
|
|
96
|
+
if (slug === "")
|
|
97
|
+
continue;
|
|
98
|
+
const channels = feat["subscription_channels"];
|
|
99
|
+
if (!Array.isArray(channels) || channels.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
const featureId = resolveFeatureId(slug, inputs.productId, inputs.existingFeatureIds);
|
|
102
|
+
if (featureId === "") {
|
|
103
|
+
inputs.findings.add("orphan", `${relPath}: feature_slug="${slug}" carries subscription_channels but does not resolve to any catalogue feature id`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const ch of channels) {
|
|
107
|
+
if (!ch || typeof ch !== "object")
|
|
108
|
+
continue;
|
|
109
|
+
const channelObj = ch;
|
|
110
|
+
const channelName = String(channelObj["name"] ?? "").trim();
|
|
111
|
+
if (channelName === "") {
|
|
112
|
+
inputs.findings.add("convention", `${relPath}: subscription_channels entry under feature_slug="${slug}" missing required "name" field`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const transport = String(channelObj["transport"] ?? "").trim();
|
|
116
|
+
if (transport === "") {
|
|
117
|
+
inputs.findings.add("convention", `${relPath}: subscription_channels entry "${channelName}" under feature_slug="${slug}" missing required "transport" field`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!VALID_TRANSPORTS.has(transport)) {
|
|
121
|
+
inputs.findings.add("convention", `${relPath}: subscription_channels entry "${channelName}" under feature_slug="${slug}" has unknown transport="${transport}" (expected one of ${[...VALID_TRANSPORTS].join(", ")})`);
|
|
122
|
+
// Don't drop — emit the row with the literal transport. The
|
|
123
|
+
// schema accepts any string; this finding lets reviewers
|
|
124
|
+
// notice the deviation.
|
|
125
|
+
}
|
|
126
|
+
const evidence = typeof channelObj["evidence"] === "string"
|
|
127
|
+
? channelObj["evidence"].trim()
|
|
128
|
+
: "";
|
|
129
|
+
const payloadRef = typeof channelObj["payload_ref"] === "string"
|
|
130
|
+
? channelObj["payload_ref"].trim()
|
|
131
|
+
: "";
|
|
132
|
+
const rowId = `${featureId}__${channelName}`;
|
|
133
|
+
if (seenIds.has(rowId)) {
|
|
134
|
+
inputs.findings.add("divergence", `${relPath}: duplicate subscription_channels row id="${rowId}" — same (feature_id, channel_name) already emitted`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
seenIds.add(rowId);
|
|
138
|
+
rows.push({
|
|
139
|
+
id: rowId,
|
|
140
|
+
feature_id: featureId,
|
|
141
|
+
channel_name: channelName,
|
|
142
|
+
transport,
|
|
143
|
+
evidence,
|
|
144
|
+
payload_ref: payloadRef,
|
|
145
|
+
source_sdd_id: "",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a bare `feature_slug` from evidence yaml to a canonical feature id
|
|
154
|
+
* present in `existingFeatureIds`. Candidates probed in order:
|
|
155
|
+
* 1. `{productId}-{slug}` — typical for application-product / integration-product features.
|
|
156
|
+
* 2. The slug as-is — when the feature id is just the slug (legacy / capability-spec source).
|
|
157
|
+
* 3. Any existing id ending in `-{slug}` — covers `{capability-id}-{slug}` for capability-spec sourced features.
|
|
158
|
+
*
|
|
159
|
+
* Returns `""` when no candidate resolves.
|
|
160
|
+
*/
|
|
161
|
+
function resolveFeatureId(slug, productId, existing) {
|
|
162
|
+
if (productId !== "") {
|
|
163
|
+
const candidate = `${productId}-${slug}`;
|
|
164
|
+
if (existing.has(candidate))
|
|
165
|
+
return candidate;
|
|
166
|
+
}
|
|
167
|
+
if (existing.has(slug))
|
|
168
|
+
return slug;
|
|
169
|
+
for (const id of existing) {
|
|
170
|
+
if (id.endsWith(`-${slug}`))
|
|
171
|
+
return id;
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
async function collectEvidenceFiles(sddPath) {
|
|
176
|
+
const out = [];
|
|
177
|
+
await walk(sddPath, sddPath, out);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
async function walk(root, current, out) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const e of entries) {
|
|
189
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name === "source" || e.name === "catalogues" || e.name === "dist") {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const abs = join(current, e.name);
|
|
193
|
+
if (e.isDirectory()) {
|
|
194
|
+
// Check whether this is the reverse-evidence dir; if so, look for the file directly.
|
|
195
|
+
if (e.name === "reverse-evidence") {
|
|
196
|
+
// Two reverse-evidence layouts are read (ADR 0164):
|
|
197
|
+
// - per-design-dir: `reverse-evidence/feature-grounding-evidence.yaml`
|
|
198
|
+
// (features: → per-feature subscription_channels:).
|
|
199
|
+
// - per-domain: `reverse-evidence/<domain>-feature-grounding.yaml`
|
|
200
|
+
// at the SDD root (top-level feature_subscription_channels: list).
|
|
201
|
+
let reEntries;
|
|
202
|
+
try {
|
|
203
|
+
reEntries = await readdir(abs, { withFileTypes: true });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for (const re of reEntries) {
|
|
209
|
+
if (!re.isFile())
|
|
210
|
+
continue;
|
|
211
|
+
if (re.name === "feature-grounding-evidence.yaml" ||
|
|
212
|
+
re.name.endsWith("-feature-grounding.yaml")) {
|
|
213
|
+
out.push(join(abs, re.name));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Bound recursion depth via a heuristic on path component count from root
|
|
219
|
+
// to avoid pathological deep trees; reverse-evidence lives at most a few
|
|
220
|
+
// levels deep in adopters we've seen.
|
|
221
|
+
const rel = relative(root, abs);
|
|
222
|
+
if (rel.split(sep).length > 8)
|
|
223
|
+
continue;
|
|
224
|
+
await walk(root, abs, out);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=feature-subscription-channels.js.map
|