@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared walker for `feature-tree.md` files. Used by:
|
|
3
|
+
* - `buildFeatureGroups` — consumes H2/H3 events to emit `feature_groups.csv`.
|
|
4
|
+
* - `buildFeatures` (bullet branch, schema 2.2.0+) — consumes bullet
|
|
5
|
+
* events to emit `features.csv` rows with `source: "feature-tree"`.
|
|
6
|
+
*
|
|
7
|
+
* Both consumers need:
|
|
8
|
+
* - in-fence detection (skip headings/bullets inside ``` blocks),
|
|
9
|
+
* - the same H2/H3 slug derivation,
|
|
10
|
+
* - the same group-id format (`{productId}-group-{h2}[-{h3}]`),
|
|
11
|
+
* - the same candidate-paths order (`<root>/feature-tree.md` then
|
|
12
|
+
* `<root>/docs/platform/feature-tree.md`).
|
|
13
|
+
*
|
|
14
|
+
* Lifting these into one module keeps both builders honest about the
|
|
15
|
+
* exact shape of the file they read.
|
|
16
|
+
*/
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
const CANDIDATE_PATHS_REL = [
|
|
20
|
+
"feature-tree.md",
|
|
21
|
+
join("docs", "platform", "feature-tree.md"),
|
|
22
|
+
];
|
|
23
|
+
/** Resolve the first existing `feature-tree.md` candidate at the SDD root. */
|
|
24
|
+
export async function findFeatureTreeFile(sddPath) {
|
|
25
|
+
for (const rel of CANDIDATE_PATHS_REL) {
|
|
26
|
+
const full = join(sddPath, rel);
|
|
27
|
+
try {
|
|
28
|
+
await readFile(full, "utf-8");
|
|
29
|
+
return full;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const code = err.code;
|
|
33
|
+
if (code === "ENOENT")
|
|
34
|
+
continue;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Lowercase-hyphenated slug used by feature-group ids and bullet ids. */
|
|
41
|
+
export function slugify(text) {
|
|
42
|
+
return text
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/&/g, " and ")
|
|
45
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
46
|
+
.replace(/^-+|-+$/g, "");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a feature-group id from a product id and one or two heading
|
|
50
|
+
* slugs. Single-arg form returns the H2-level id; two-arg form returns
|
|
51
|
+
* the nested H3-level id. Matches `feature-groups.ts` row emission so
|
|
52
|
+
* the bullet parser's `parent_group_id` resolves cleanly.
|
|
53
|
+
*/
|
|
54
|
+
export function featureGroupId(productId, h2Slug, h3Slug) {
|
|
55
|
+
const base = `${productId}-group-${h2Slug}`;
|
|
56
|
+
return h3Slug !== undefined && h3Slug !== "" ? `${base}-${h3Slug}` : base;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Walk a `feature-tree.md` text body and emit headings + bullets in
|
|
60
|
+
* source order. Inside fenced code blocks (```), nothing is emitted.
|
|
61
|
+
*
|
|
62
|
+
* Heading rules (mirror `feature-groups.ts` for compatibility):
|
|
63
|
+
* - H2/H3 lines emit a HeadingEvent. Numeric prefixes (`1.`, `1.1`)
|
|
64
|
+
* and trailing `[Tag]` markers are stripped from the displayed
|
|
65
|
+
* text and from the slug.
|
|
66
|
+
* - An H3 without an enclosing H2 emits with `parentH2Slug: ""`.
|
|
67
|
+
*
|
|
68
|
+
* Bullet rules (canonical form per
|
|
69
|
+
* `reverse/templates/application-product/feature-tree.md`):
|
|
70
|
+
*
|
|
71
|
+
* - `{slug}` — {description} `[runway: {marker}]`
|
|
72
|
+
*
|
|
73
|
+
* - Required: leading `-` or `*`, slug-in-backticks.
|
|
74
|
+
* - Optional: em-dash + description (description may be empty).
|
|
75
|
+
* - Optional trailing `` `[runway: ...]` `` token; captured verbatim
|
|
76
|
+
* into `runway` and stripped from `description`.
|
|
77
|
+
* - Optional Markdown footnote markers `[^...]` anywhere on the
|
|
78
|
+
* line (`- \`{slug}\`[^x] — {description} \`[runway: ...]\`[^y]`).
|
|
79
|
+
* Markers are stripped before regex matching so they don't break
|
|
80
|
+
* bullet detection or runway-tail extraction. Authors typically
|
|
81
|
+
* place a single end-of-line marker pointing at the matching
|
|
82
|
+
* entry in `reverse-evidence/feature-grounding-evidence.yaml`.
|
|
83
|
+
*
|
|
84
|
+
* The walker does NOT enforce a description-length cap — that's a
|
|
85
|
+
* builder concern. Slugs not matching `[a-z0-9][a-z0-9-]*` are
|
|
86
|
+
* skipped (defensive: avoids accidentally claiming free-form
|
|
87
|
+
* backtick-wrapped prose like inline code refs).
|
|
88
|
+
*/
|
|
89
|
+
export function walkFeatureTree(text) {
|
|
90
|
+
const out = [];
|
|
91
|
+
let inFence = false;
|
|
92
|
+
let currentH2Slug = "";
|
|
93
|
+
let currentH3Slug = "";
|
|
94
|
+
let inDataRequirements = false;
|
|
95
|
+
// A bullet's description may wrap onto indented continuation lines. Reading one line per
|
|
96
|
+
// bullet cut it at the first break (a KPI list lost mid-sentence), so wrapped lines are
|
|
97
|
+
// joined into the bullet before it is parsed.
|
|
98
|
+
const lines = joinContinuations(text.split(/\r?\n/));
|
|
99
|
+
for (const rawLine of lines) {
|
|
100
|
+
const line = rawLine.trimEnd();
|
|
101
|
+
if (line.startsWith("```")) {
|
|
102
|
+
inFence = !inFence;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inFence)
|
|
106
|
+
continue;
|
|
107
|
+
const heading = matchHeading(line);
|
|
108
|
+
// A `## Data Requirements` section (M-ADR 0216 §4.3) is method-defined content inside the feature
|
|
109
|
+
// tree, not a feature group: skip the heading and everything under it until the next `##`.
|
|
110
|
+
// Without this, every adopter declaring tree demand got a spurious group (found by the first
|
|
111
|
+
// adopter to do so).
|
|
112
|
+
if (heading !== null && heading.level === 2)
|
|
113
|
+
inDataRequirements = /^Data Requirements\b/i.test(heading.text.trim());
|
|
114
|
+
if (inDataRequirements)
|
|
115
|
+
continue;
|
|
116
|
+
if (heading !== null) {
|
|
117
|
+
const slug = slugify(heading.text);
|
|
118
|
+
if (slug === "")
|
|
119
|
+
continue;
|
|
120
|
+
if (heading.level === 2) {
|
|
121
|
+
currentH2Slug = slug;
|
|
122
|
+
currentH3Slug = "";
|
|
123
|
+
out.push({
|
|
124
|
+
kind: "h2",
|
|
125
|
+
text: heading.text,
|
|
126
|
+
slug,
|
|
127
|
+
parentH2Slug: "",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
currentH3Slug = slug;
|
|
132
|
+
out.push({
|
|
133
|
+
kind: "h3",
|
|
134
|
+
text: heading.text,
|
|
135
|
+
slug,
|
|
136
|
+
parentH2Slug: currentH2Slug,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const bullet = matchBullet(line);
|
|
142
|
+
if (bullet !== null) {
|
|
143
|
+
out.push({
|
|
144
|
+
kind: "bullet",
|
|
145
|
+
slug: bullet.slug,
|
|
146
|
+
description: bullet.description,
|
|
147
|
+
runway: bullet.runway,
|
|
148
|
+
parentH2Slug: currentH2Slug,
|
|
149
|
+
parentH3Slug: currentH3Slug,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
const FEATURE_SERVICES_ROW_RE = /^\|\s*`([a-z0-9][a-z0-9-]*)`\s*\|\s*(.+?)\s*\|\s*$/;
|
|
156
|
+
// The Services column holds SERVICE ids, whose alphabet is services.csv's —
|
|
157
|
+
// which admits `_` (e.g. a service named for its repository). It is NOT the
|
|
158
|
+
// feature-slug alphabet above: slugs are a documented convention and become
|
|
159
|
+
// primary keys, so FEATURE_SERVICES_ROW_RE's slug group stays strict.
|
|
160
|
+
const BACKTICKED_SPAN_RE = /`([^`]*)`/g;
|
|
161
|
+
const SERVICE_ID_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
162
|
+
/**
|
|
163
|
+
* Parse the `## Feature Services` section of a `feature-tree.md` body (the
|
|
164
|
+
* application-product authoring surface for `feature_service`, per M-ADR
|
|
165
|
+
* 0157 — see WS0c of `catalogue-coverage-invariants.md`). Table shape:
|
|
166
|
+
*
|
|
167
|
+
* | Feature | Services |
|
|
168
|
+
* |---------|----------|
|
|
169
|
+
* | `{feature-slug}` | `{service-id}`, `{service-id}` |
|
|
170
|
+
*
|
|
171
|
+
* Returns one entry per table row (not one per feature-service pair) so the
|
|
172
|
+
* caller can resolve/validate each service id independently. A malformed or
|
|
173
|
+
* absent section yields an empty array — the caller decides whether that's
|
|
174
|
+
* a finding.
|
|
175
|
+
*/
|
|
176
|
+
export function parseFeatureServicesTable(text) {
|
|
177
|
+
const out = [];
|
|
178
|
+
let inSection = false;
|
|
179
|
+
let inTable = false;
|
|
180
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
181
|
+
const line = rawLine.trimEnd();
|
|
182
|
+
if (/^##\s+Feature Services\s*$/.test(line)) {
|
|
183
|
+
inSection = true;
|
|
184
|
+
inTable = false;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (inSection && /^##\s+/.test(line))
|
|
188
|
+
break;
|
|
189
|
+
if (!inSection)
|
|
190
|
+
continue;
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (trimmed.startsWith("| Feature") || /^\|[-\s|]+\|$/.test(trimmed)) {
|
|
193
|
+
inTable = true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!inTable)
|
|
197
|
+
continue;
|
|
198
|
+
if (trimmed === "")
|
|
199
|
+
continue;
|
|
200
|
+
if (!trimmed.startsWith("|")) {
|
|
201
|
+
inTable = false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const m = FEATURE_SERVICES_ROW_RE.exec(line);
|
|
205
|
+
if (m === null || m[1] === undefined || m[2] === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
const serviceIds = [];
|
|
208
|
+
const rejectedSpans = [];
|
|
209
|
+
for (const span of m[2].matchAll(BACKTICKED_SPAN_RE)) {
|
|
210
|
+
const value = span[1];
|
|
211
|
+
(SERVICE_ID_RE.test(value) ? serviceIds : rejectedSpans).push(value);
|
|
212
|
+
}
|
|
213
|
+
if (serviceIds.length === 0 && rejectedSpans.length === 0)
|
|
214
|
+
continue;
|
|
215
|
+
out.push(rejectedSpans.length > 0
|
|
216
|
+
? { featureSlug: m[1], serviceIds, rejectedSpans }
|
|
217
|
+
: { featureSlug: m[1], serviceIds });
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function matchHeading(line) {
|
|
222
|
+
const m = /^(#{2,3})\s+(.+?)\s*$/.exec(line);
|
|
223
|
+
if (!m || m[1] === undefined || m[2] === undefined)
|
|
224
|
+
return null;
|
|
225
|
+
const level = m[1].length === 2 ? 2 : 3;
|
|
226
|
+
const text = m[2]
|
|
227
|
+
.replace(/^[\d.]+\s*/, "")
|
|
228
|
+
.replace(/\s*\[.+?\]\s*$/, "")
|
|
229
|
+
.trim();
|
|
230
|
+
if (text === "")
|
|
231
|
+
return null;
|
|
232
|
+
return { level, text };
|
|
233
|
+
}
|
|
234
|
+
const BULLET_RE = /^\s*[-*]\s+`([a-z0-9][a-z0-9-]*)`(?:\s*[—–-]\s*(.*))?\s*$/;
|
|
235
|
+
const RUNWAY_TAIL_RE = /\s*`\[runway:\s*([^\]]+?)\s*\]`\s*$/;
|
|
236
|
+
const FOOTNOTE_MARKER_RE = /\s*\[\^[^\]]+\]/g;
|
|
237
|
+
/**
|
|
238
|
+
* Strip Markdown footnote markers (`[^...]`) from a line. Markers can
|
|
239
|
+
* appear anywhere — immediately after the slug, mid-description, or
|
|
240
|
+
* trailing after the runway tag. They are rendering aids that point
|
|
241
|
+
* authors at evidence files; they don't carry feature semantics, so
|
|
242
|
+
* strip-before-match keeps the bullet/runway regexes simple.
|
|
243
|
+
*/
|
|
244
|
+
function stripFootnoteMarkers(line) {
|
|
245
|
+
return line.replace(FOOTNOTE_MARKER_RE, "");
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Join a bullet's wrapped continuation lines onto it: an indented, non-blank line that is not
|
|
249
|
+
* itself a list item, heading, table row or fence, directly following a bullet (or another
|
|
250
|
+
* continuation). Fenced blocks are passed through untouched.
|
|
251
|
+
*/
|
|
252
|
+
function joinContinuations(lines) {
|
|
253
|
+
const out = [];
|
|
254
|
+
let inFence = false;
|
|
255
|
+
let joinable = false;
|
|
256
|
+
for (const raw of lines) {
|
|
257
|
+
const t = raw.trim();
|
|
258
|
+
if (t.startsWith("```")) {
|
|
259
|
+
inFence = !inFence;
|
|
260
|
+
joinable = false;
|
|
261
|
+
out.push(raw);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (inFence) {
|
|
265
|
+
out.push(raw);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const isContinuation = joinable &&
|
|
269
|
+
/^\s+\S/.test(raw) &&
|
|
270
|
+
!/^([-*+]\s|\d+[.)]\s|#|\||>)/.test(t);
|
|
271
|
+
if (isContinuation) {
|
|
272
|
+
out[out.length - 1] = `${out[out.length - 1].trimEnd()} ${t}`;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
joinable = /^\s*[-*+]\s/.test(raw);
|
|
276
|
+
out.push(raw);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
function matchBullet(line) {
|
|
281
|
+
const stripped = stripFootnoteMarkers(line);
|
|
282
|
+
const m = BULLET_RE.exec(stripped);
|
|
283
|
+
if (!m || m[1] === undefined)
|
|
284
|
+
return null;
|
|
285
|
+
const slug = m[1];
|
|
286
|
+
let rest = (m[2] ?? "").trim();
|
|
287
|
+
let runway = "";
|
|
288
|
+
const r = RUNWAY_TAIL_RE.exec(rest);
|
|
289
|
+
if (r !== null && r[1] !== undefined) {
|
|
290
|
+
runway = r[1];
|
|
291
|
+
rest = rest.slice(0, r.index).trim();
|
|
292
|
+
}
|
|
293
|
+
return { slug, description: rest, runway };
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=feature-tree-walker.js.map
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Features discoverer — emits `features.csv` rows from three sources:
|
|
2
|
+
//
|
|
3
|
+
// 1. Capability-scoped specs at `docs/domains/<d>/product/features/<folder>/*.md`.
|
|
4
|
+
// Folder→capability mapping uses (a) the traceability manifest's
|
|
5
|
+
// `capability.feature_directory` field — authoritative when a
|
|
6
|
+
// capability spec uses a folder name that differs from the
|
|
7
|
+
// canonical capability_id (e.g. capability_id "alerts-rules-engine"
|
|
8
|
+
// with feature_directory pointing at "features/alerts-rules/") —
|
|
9
|
+
// then (b) the folder name itself when it equals a known capability_id.
|
|
10
|
+
// id format: `{capability_id}-{feature-slug}`.
|
|
11
|
+
//
|
|
12
|
+
// 2. Application-product profile only: standalone specs at
|
|
13
|
+
// `features/<group>/<file>.md` under the SDD root.
|
|
14
|
+
// id format: `{productId}-{stem}`.
|
|
15
|
+
//
|
|
16
|
+
// 3. Application-product profile only (schema 2.2.0+): bullets in
|
|
17
|
+
// `feature-tree.md`. Canonical form:
|
|
18
|
+
//
|
|
19
|
+
// - `{slug}` — {description} `[runway: {marker}]`
|
|
20
|
+
//
|
|
21
|
+
// id format: `{productId}-{slug}`. Source path resolved via the
|
|
22
|
+
// shared `feature-tree-walker` (same candidates that
|
|
23
|
+
// buildFeatureGroups walks).
|
|
24
|
+
//
|
|
25
|
+
// Dedup ordering matters: walker (1) runs first (capability features),
|
|
26
|
+
// then walker (2) (standalone specs), then walker (3) (bullets). When
|
|
27
|
+
// a bullet's id is already in `seen` from a standalone spec the spec
|
|
28
|
+
// wins — divergence finding fires for the bullet. This matches the
|
|
29
|
+
// "standalone is more authoritative than bullet inventory" rule from
|
|
30
|
+
// plan extract-feature-tree-bullets.md §1.4.
|
|
31
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
32
|
+
import { basename, join, relative, sep } from "node:path";
|
|
33
|
+
import { featureGroupId, findFeatureTreeFile, walkFeatureTree, } from "./feature-tree-walker.js";
|
|
34
|
+
// ADR 0165: the standard, cross-SDD-comparable `features.maturity` vocabulary
|
|
35
|
+
// (canonical-schema §features). Feature-tree `[runway: X]` markers whose value
|
|
36
|
+
// is in this set ARE the maturity signal — wire them to `maturity` (comparable)
|
|
37
|
+
// while `runway` keeps the full marker (adopter-owned, incl. non-standard values
|
|
38
|
+
// like migration codes). Markers outside the set leave `maturity` empty.
|
|
39
|
+
const STANDARD_MATURITY = new Set(["working", "provisional", "unknown", "absent"]);
|
|
40
|
+
/** Standard-set maturity from a feature-tree runway marker, else "". */
|
|
41
|
+
function maturityFromRunway(runway) {
|
|
42
|
+
const v = runway.trim().toLowerCase();
|
|
43
|
+
return STANDARD_MATURITY.has(v) ? v : "";
|
|
44
|
+
}
|
|
45
|
+
const DOMAINS_DIR_REL = join("docs", "domains");
|
|
46
|
+
const DESCRIPTION_MAX = 500;
|
|
47
|
+
export async function buildFeatures(inputs) {
|
|
48
|
+
// Build folder→capability_id map from traceability feature_directory.
|
|
49
|
+
const folderToCap = new Map();
|
|
50
|
+
for (const m of inputs.manifests) {
|
|
51
|
+
const featDirs = m.capability.feature_directory;
|
|
52
|
+
if (!featDirs || featDirs.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
if (!inputs.knownCapabilityIds.has(m.capability.id))
|
|
55
|
+
continue;
|
|
56
|
+
for (const featDir of featDirs) {
|
|
57
|
+
const folder = basename(featDir.replace(/\/+$/, ""));
|
|
58
|
+
folderToCap.set(folder, m.capability.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const featurePaths = await collectFeatureSpecs(inputs.sddPath);
|
|
62
|
+
const rows = [];
|
|
63
|
+
const idByPath = new Map();
|
|
64
|
+
const idBySlug = new Map();
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const path of featurePaths) {
|
|
67
|
+
const relPath = relative(inputs.sddPath, path);
|
|
68
|
+
const folder = parentFolder(relPath);
|
|
69
|
+
const capId = folderToCap.get(folder)
|
|
70
|
+
?? (inputs.knownCapabilityIds.has(folder) ? folder : "");
|
|
71
|
+
if (capId === "") {
|
|
72
|
+
inputs.findings.add("orphan", `${relPath}: feature folder "${folder}" has no matching capability (no traceability feature_directory + folder is not a capability id)`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const stem = basename(relPath).replace(/\.md$/i, "");
|
|
76
|
+
const id = `${capId}-${stem}`;
|
|
77
|
+
if (seen.has(id)) {
|
|
78
|
+
inputs.findings.add("divergence", `${relPath}: feature id "${id}" already emitted by an earlier file`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.add(id);
|
|
82
|
+
const text = await readFile(path, "utf-8");
|
|
83
|
+
const description = extractH1Title(text);
|
|
84
|
+
rows.push({
|
|
85
|
+
id,
|
|
86
|
+
name: stem,
|
|
87
|
+
product_id: "",
|
|
88
|
+
capability_id: capId,
|
|
89
|
+
parent_group_id: "",
|
|
90
|
+
parent_feature_id: "",
|
|
91
|
+
description,
|
|
92
|
+
maturity: "",
|
|
93
|
+
status: "active",
|
|
94
|
+
source: "feature-spec",
|
|
95
|
+
runway: "",
|
|
96
|
+
source_sdd_id: "",
|
|
97
|
+
});
|
|
98
|
+
idByPath.set(relPath, id);
|
|
99
|
+
if (!idBySlug.has(stem))
|
|
100
|
+
idBySlug.set(stem, id);
|
|
101
|
+
}
|
|
102
|
+
// App-product profile: also walk `features/<group>/<file>.md` at SDD root.
|
|
103
|
+
// These features are owned by the SDD's product (not by a capability) —
|
|
104
|
+
// capability_id is left empty; product_id carries the link.
|
|
105
|
+
if (inputs.profile === "application-product") {
|
|
106
|
+
const appFeatures = await collectAppFeatureSpecs(inputs.sddPath);
|
|
107
|
+
for (const path of appFeatures) {
|
|
108
|
+
const relPath = relative(inputs.sddPath, path);
|
|
109
|
+
const stem = basename(relPath).replace(/\.md$/i, "");
|
|
110
|
+
const id = `${inputs.productId}-${stem}`;
|
|
111
|
+
if (seen.has(id)) {
|
|
112
|
+
inputs.findings.add("divergence", `${relPath}: feature id "${id}" already emitted by an earlier file`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
seen.add(id);
|
|
116
|
+
const text = await readFile(path, "utf-8");
|
|
117
|
+
const description = extractH1Title(text);
|
|
118
|
+
rows.push({
|
|
119
|
+
id,
|
|
120
|
+
name: stem,
|
|
121
|
+
product_id: inputs.productId,
|
|
122
|
+
capability_id: "",
|
|
123
|
+
parent_group_id: "",
|
|
124
|
+
parent_feature_id: "",
|
|
125
|
+
description,
|
|
126
|
+
maturity: "",
|
|
127
|
+
status: "active",
|
|
128
|
+
source: "feature-spec",
|
|
129
|
+
runway: "",
|
|
130
|
+
source_sdd_id: "",
|
|
131
|
+
});
|
|
132
|
+
idByPath.set(relPath, id);
|
|
133
|
+
if (!idBySlug.has(stem))
|
|
134
|
+
idBySlug.set(stem, id);
|
|
135
|
+
}
|
|
136
|
+
// Bullet branch — feature-tree.md.
|
|
137
|
+
await emitBulletRows({
|
|
138
|
+
sddPath: inputs.sddPath,
|
|
139
|
+
productId: inputs.productId,
|
|
140
|
+
findings: inputs.findings,
|
|
141
|
+
runwayVocabulary: inputs.runwayVocabulary ?? null,
|
|
142
|
+
seen,
|
|
143
|
+
rows,
|
|
144
|
+
idBySlug,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { rows, idByPath, idBySlug };
|
|
148
|
+
}
|
|
149
|
+
async function emitBulletRows(ctx) {
|
|
150
|
+
const featureTreePath = await findFeatureTreeFile(ctx.sddPath);
|
|
151
|
+
if (!featureTreePath)
|
|
152
|
+
return;
|
|
153
|
+
const text = await readFile(featureTreePath, "utf-8");
|
|
154
|
+
const events = walkFeatureTree(text);
|
|
155
|
+
const relPath = relative(ctx.sddPath, featureTreePath);
|
|
156
|
+
let bulletCount = 0;
|
|
157
|
+
for (const ev of events) {
|
|
158
|
+
if (ev.kind !== "bullet")
|
|
159
|
+
continue;
|
|
160
|
+
bulletCount++;
|
|
161
|
+
const id = `${ctx.productId}-${ev.slug}`;
|
|
162
|
+
if (ctx.seen.has(id)) {
|
|
163
|
+
ctx.findings.add("divergence", `${relPath}: feature-tree.md bullet "${ev.slug}" superseded by an earlier feature spec with id "${id}"`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
ctx.seen.add(id);
|
|
167
|
+
if (ev.runway !== "" &&
|
|
168
|
+
ctx.runwayVocabulary !== null &&
|
|
169
|
+
!ctx.runwayVocabulary.has(ev.runway)) {
|
|
170
|
+
ctx.findings.add("convention", `${relPath}: unrecognised runway value "${ev.runway}" in feature "${ev.slug}" — declare it in feature-runway-vocabulary.yaml`);
|
|
171
|
+
}
|
|
172
|
+
const parentGroupId = ev.parentH3Slug !== ""
|
|
173
|
+
? featureGroupId(ctx.productId, ev.parentH2Slug !== "" ? ev.parentH2Slug : ev.parentH3Slug, ev.parentH2Slug !== "" ? ev.parentH3Slug : undefined)
|
|
174
|
+
: ev.parentH2Slug !== ""
|
|
175
|
+
? featureGroupId(ctx.productId, ev.parentH2Slug)
|
|
176
|
+
: "";
|
|
177
|
+
const description = ev.description.length > DESCRIPTION_MAX
|
|
178
|
+
? ev.description.slice(0, DESCRIPTION_MAX)
|
|
179
|
+
: ev.description;
|
|
180
|
+
ctx.rows.push({
|
|
181
|
+
id,
|
|
182
|
+
name: ev.slug,
|
|
183
|
+
product_id: ctx.productId,
|
|
184
|
+
capability_id: "",
|
|
185
|
+
parent_group_id: parentGroupId,
|
|
186
|
+
parent_feature_id: "",
|
|
187
|
+
description,
|
|
188
|
+
maturity: maturityFromRunway(ev.runway),
|
|
189
|
+
status: "active",
|
|
190
|
+
source: "feature-tree",
|
|
191
|
+
runway: ev.runway,
|
|
192
|
+
source_sdd_id: "",
|
|
193
|
+
});
|
|
194
|
+
if (!ctx.idBySlug.has(ev.slug))
|
|
195
|
+
ctx.idBySlug.set(ev.slug, id);
|
|
196
|
+
}
|
|
197
|
+
if (bulletCount === 0) {
|
|
198
|
+
ctx.findings.add("coverage", `${relPath}: feature-tree.md found but no recognisable feature bullets — features.csv populated only from features/ specs`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const FEATURES_DIR_REL = "features";
|
|
202
|
+
async function collectAppFeatureSpecs(sddPath) {
|
|
203
|
+
const out = [];
|
|
204
|
+
const featuresDir = join(sddPath, FEATURES_DIR_REL);
|
|
205
|
+
let groups;
|
|
206
|
+
try {
|
|
207
|
+
groups = await readdir(featuresDir, { withFileTypes: true });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
for (const g of groups) {
|
|
213
|
+
if (!g.isDirectory())
|
|
214
|
+
continue;
|
|
215
|
+
const groupDir = join(featuresDir, g.name);
|
|
216
|
+
let entries;
|
|
217
|
+
try {
|
|
218
|
+
entries = await readdir(groupDir, { withFileTypes: true });
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const e of entries) {
|
|
224
|
+
if (!e.isFile())
|
|
225
|
+
continue;
|
|
226
|
+
if (!e.name.endsWith(".md"))
|
|
227
|
+
continue;
|
|
228
|
+
const lower = e.name.toLowerCase();
|
|
229
|
+
if (lower === "readme.md" || lower === "index.md")
|
|
230
|
+
continue;
|
|
231
|
+
out.push(join(groupDir, e.name));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out.sort();
|
|
235
|
+
}
|
|
236
|
+
async function collectFeatureSpecs(sddPath) {
|
|
237
|
+
const out = [];
|
|
238
|
+
const domainsDir = join(sddPath, DOMAINS_DIR_REL);
|
|
239
|
+
let domains;
|
|
240
|
+
try {
|
|
241
|
+
domains = await readdir(domainsDir, { withFileTypes: true });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
for (const d of domains) {
|
|
247
|
+
if (!d.isDirectory())
|
|
248
|
+
continue;
|
|
249
|
+
const featuresDir = join(domainsDir, d.name, "product", "features");
|
|
250
|
+
let folders;
|
|
251
|
+
try {
|
|
252
|
+
folders = await readdir(featuresDir, { withFileTypes: true });
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const f of folders) {
|
|
258
|
+
if (!f.isDirectory())
|
|
259
|
+
continue;
|
|
260
|
+
const folderPath = join(featuresDir, f.name);
|
|
261
|
+
let entries;
|
|
262
|
+
try {
|
|
263
|
+
entries = await readdir(folderPath, { withFileTypes: true });
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
for (const e of entries) {
|
|
269
|
+
if (!e.isFile())
|
|
270
|
+
continue;
|
|
271
|
+
if (!e.name.endsWith(".md"))
|
|
272
|
+
continue;
|
|
273
|
+
const lower = e.name.toLowerCase();
|
|
274
|
+
if (lower === "readme.md" || lower === "index.md")
|
|
275
|
+
continue;
|
|
276
|
+
out.push(join(folderPath, e.name));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out.sort();
|
|
281
|
+
}
|
|
282
|
+
function parentFolder(relPath) {
|
|
283
|
+
const parts = relPath.split(sep);
|
|
284
|
+
return parts[parts.length - 2] ?? "";
|
|
285
|
+
}
|
|
286
|
+
function extractH1Title(text) {
|
|
287
|
+
for (const line of text.split(/\r?\n/)) {
|
|
288
|
+
const t = line.trim();
|
|
289
|
+
if (t === "")
|
|
290
|
+
continue;
|
|
291
|
+
if (!t.startsWith("#"))
|
|
292
|
+
return "";
|
|
293
|
+
return t
|
|
294
|
+
.replace(/^#+\s*/, "")
|
|
295
|
+
.replace(/^(Feature Specification|Spec):\s*/i, "")
|
|
296
|
+
.trim();
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=features.js.map
|