@sdd-method/maintain-cli 0.8.0 → 0.10.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/dist/lib/agent-harness.js +2 -2
- package/dist/lib/behaviour-store/archive.js +28 -1
- package/dist/lib/behaviour-store/schema.js +85 -0
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/render.js +1 -1
- package/dist/lib/briefing/variables.js +1 -1
- package/dist/lib/bundle.js +1 -1
- package/dist/lib/catalogue/aggregate.js +7 -7
- package/dist/lib/catalogue/application-archetype/vocabulary.js +2 -2
- package/dist/lib/catalogue/archetype/maps/android.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +2 -2
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +2 -2
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +3 -3
- package/dist/lib/catalogue/archetype/resolver.js +4 -4
- package/dist/lib/catalogue/archetype/wireframe.js +1 -1
- package/dist/lib/catalogue/build.js +36 -33
- package/dist/lib/catalogue/builders/adr-supersedes.js +3 -3
- package/dist/lib/catalogue/builders/adrs.js +3 -3
- package/dist/lib/catalogue/builders/c4-containers.js +2 -2
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +1 -1
- package/dist/lib/catalogue/builders/c4-external-systems.js +2 -2
- package/dist/lib/catalogue/builders/c4-systems.js +1 -1
- package/dist/lib/catalogue/builders/capabilities.js +1 -1
- package/dist/lib/catalogue/builders/capability-domain-model.js +1 -1
- package/dist/lib/catalogue/builders/capability-spec-walker.js +2 -2
- package/dist/lib/catalogue/builders/capability-spec.js +4 -4
- package/dist/lib/catalogue/builders/contracts.js +3 -3
- package/dist/lib/catalogue/builders/data-store-mapping.js +2 -2
- package/dist/lib/catalogue/builders/domain-models.js +2 -2
- package/dist/lib/catalogue/builders/domains.js +3 -3
- package/dist/lib/catalogue/builders/entities-manifest.js +8 -8
- package/dist/lib/catalogue/builders/entities-mermaid.js +4 -4
- package/dist/lib/catalogue/builders/entity-persistence.js +1 -1
- package/dist/lib/catalogue/builders/entity-slug.js +3 -3
- package/dist/lib/catalogue/builders/event-catalog-walker.js +6 -6
- package/dist/lib/catalogue/builders/event-edges.js +6 -6
- package/dist/lib/catalogue/builders/events.js +3 -3
- package/dist/lib/catalogue/builders/evidence-class.js +1 -1
- package/dist/lib/catalogue/builders/feature-domain-model.js +1 -1
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +2 -2
- package/dist/lib/catalogue/builders/feature-grounding.js +5 -5
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +3 -3
- package/dist/lib/catalogue/builders/features.js +1 -1
- package/dist/lib/catalogue/builders/glossary-xref.js +4 -4
- package/dist/lib/catalogue/builders/integrations.js +4 -4
- package/dist/lib/catalogue/builders/persona-c4system.js +1 -1
- package/dist/lib/catalogue/builders/product-edges.js +1 -1
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +2 -2
- package/dist/lib/catalogue/builders/products.js +5 -5
- package/dist/lib/catalogue/builders/repo-edges.js +12 -12
- package/dist/lib/catalogue/builders/repositories.js +11 -7
- package/dist/lib/catalogue/builders/requirements.js +110 -11
- package/dist/lib/catalogue/builders/screen-tree.js +6 -6
- package/dist/lib/catalogue/builders/screens.js +18 -18
- package/dist/lib/catalogue/builders/service-contract-consumes.js +1 -1
- package/dist/lib/catalogue/builders/service-entity-tables.js +3 -3
- package/dist/lib/catalogue/builders/services.js +5 -5
- package/dist/lib/catalogue/builders/system-external-dependency.js +2 -2
- package/dist/lib/catalogue/canonical-schema.yaml +138 -2
- package/dist/lib/catalogue/coverage-render.js +15 -15
- package/dist/lib/catalogue/csv-writer.js +1 -1
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +1 -1
- package/dist/lib/catalogue/findings.js +1 -1
- package/dist/lib/catalogue/fk-validator.js +2 -2
- package/dist/lib/catalogue/manifest-loader.js +3 -3
- package/dist/lib/catalogue/rubric-coverage-map.yaml +28 -0
- package/dist/lib/catalogue/schema-compat.js +2 -2
- package/dist/lib/catalogue/schema-loader.js +1 -1
- package/dist/lib/catalogue/screen-validation-loader.js +2 -2
- package/dist/lib/catalogue/service-architecture-parser.js +1 -1
- package/dist/lib/catalogue/traceability-loader.js +6 -4
- package/dist/lib/ci-binding/module.js +4 -2
- package/dist/lib/generate-briefing/index.js +2 -2
- package/dist/lib/hooks-seed/entries.js +1 -1
- package/dist/lib/metamodel/tql-subset-parser.js +280 -0
- package/dist/lib/pack/builder.js +2 -2
- package/dist/lib/pack/index.js +1 -1
- package/dist/lib/pack/leakage.js +1 -1
- package/dist/lib/pack/publish-transform.js +2 -2
- package/dist/lib/pack/schema.js +2 -2
- package/dist/lib/pack/seed.js +2 -2
- package/dist/lib/pack/validate.js +1 -1
- package/dist/lib/repo-kind.js +4 -12
- package/dist/lib/scaffold/archetype-behaviour.js +173 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/sync/adr-transitions.js +4 -2
- package/dist/lib/sync/briefing-merge.js +2 -2
- package/dist/lib/sync/bundle-cache.js +4 -4
- package/dist/lib/sync/index.js +39 -12
- package/dist/lib/sync/settings-merge.js +1 -1
- package/dist/lib/sync/yaml-block-merge.js +1 -1
- package/dist/lib/validate/archetype-citations.js +169 -0
- package/dist/lib/validate/behaviour-store.js +161 -6
- package/dist/lib/validate/dispatch.js +8 -1
- package/dist/maintain-program.js +2 -2
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/list.js +1 -1
- package/dist/verbs/pack-build.js +2 -2
- package/dist/verbs/render.js +1 -1
- package/dist/verify-program.js +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
- package/scripts/validate-app-dependencies.sh +1 -1
- package/scripts/validate-integration-docs.sh +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// glossary_xref + entity_glossary edge builders — emit cross-domain glossary
|
|
2
2
|
// relationships from `docs/platform/reference/glossary-alignment-manifest.yaml`.
|
|
3
3
|
//
|
|
4
|
-
// Per ADR 0158 (Glossary Cross-Reference Multi-Source Ingestion, schema 2.12.0),
|
|
4
|
+
// Per M-ADR 0158 (Glossary Cross-Reference Multi-Source Ingestion, schema 2.12.0),
|
|
5
5
|
// the builder ingests from FIVE walkers:
|
|
6
6
|
//
|
|
7
7
|
// 1. Forward declared pair-form (existing).
|
|
@@ -42,7 +42,7 @@ import { join } from "node:path";
|
|
|
42
42
|
import { parse as parseYaml } from "yaml";
|
|
43
43
|
const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
|
|
44
44
|
export async function buildGlossaryXref(inputs) {
|
|
45
|
-
// Profile gate per ADR 0158 §amendment: the manifest is platform-owned;
|
|
45
|
+
// Profile gate per M-ADR 0158 §amendment: the manifest is platform-owned;
|
|
46
46
|
// sibling SDDs that syndicate it for documentation accessibility should
|
|
47
47
|
// not re-emit rows. Mirrors the entities-manifest profile gate.
|
|
48
48
|
if (inputs.profile !== "platform-profile") {
|
|
@@ -292,7 +292,7 @@ export async function buildGlossaryXref(inputs) {
|
|
|
292
292
|
status: winner,
|
|
293
293
|
});
|
|
294
294
|
}
|
|
295
|
-
inputs.findings.add("divergence", `${MANIFEST_REL_PATH}: entity ${name} in domain ${domain} has conflicting statuses across models (${existingStatus} vs ${status}) — keeping ${winner} (per ADR 0158)`);
|
|
295
|
+
inputs.findings.add("divergence", `${MANIFEST_REL_PATH}: entity ${name} in domain ${domain} has conflicting statuses across models (${existingStatus} vs ${status}) — keeping ${winner} (per M-ADR 0158)`);
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
298
298
|
const row = {
|
|
@@ -321,7 +321,7 @@ export async function buildGlossaryXref(inputs) {
|
|
|
321
321
|
|| "cross_domain_terms" in obj
|
|
322
322
|
|| "models" in obj;
|
|
323
323
|
if (!hasAnyKnownKey) {
|
|
324
|
-
inputs.findings.add("convention", `${MANIFEST_REL_PATH}: present but no recognised top-level keys (aliases/conflicts/cross_domain_terms/models) — emitting 0 xref rows (per ADR 0158)`);
|
|
324
|
+
inputs.findings.add("convention", `${MANIFEST_REL_PATH}: present but no recognised top-level keys (aliases/conflicts/cross_domain_terms/models) — emitting 0 xref rows (per M-ADR 0158)`);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
return { glossaryXref: xrefRows, entityGlossary: entityRows };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Integrations discoverer — emits `integrations.csv` rows from an
|
|
2
2
|
// integration-product SDD's `integration-manifest.yaml`.
|
|
3
3
|
//
|
|
4
|
-
// Per ADR 0134 federation: each integration-provider SDD owns one row
|
|
4
|
+
// Per M-ADR 0134 federation: each integration-provider SDD owns one row
|
|
5
5
|
// in the granular integrations.csv. The aggregator merges siblings.
|
|
6
6
|
//
|
|
7
|
-
// Per ADR 0144 (schema 2.6.0): if the manifest declares an optional
|
|
7
|
+
// Per M-ADR 0144 (schema 2.6.0): if the manifest declares an optional
|
|
8
8
|
// `sub_integrations:` array (multi-provider monolith pattern), emit
|
|
9
9
|
// one additional row per entry with parent_product_id set to the
|
|
10
10
|
// SDD's integrationId. The umbrella SDD row stays unchanged.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// doc_path = SDD's doc_path
|
|
30
30
|
// repo_count = 0 (repo ownership stays with the umbrella)
|
|
31
31
|
//
|
|
32
|
-
// domain_id is always empty per ADR 0136.
|
|
32
|
+
// domain_id is always empty per M-ADR 0136.
|
|
33
33
|
import { readFile } from "node:fs/promises";
|
|
34
34
|
import { join } from "node:path";
|
|
35
35
|
import { parse as parseYaml } from "yaml";
|
|
@@ -111,7 +111,7 @@ export async function buildIntegrations(inputs) {
|
|
|
111
111
|
source_sdd_id: "",
|
|
112
112
|
},
|
|
113
113
|
];
|
|
114
|
-
// Per ADR 0144 (schema 2.6.0): emit one row per sub_integration with
|
|
114
|
+
// Per M-ADR 0144 (schema 2.6.0): emit one row per sub_integration with
|
|
115
115
|
// parent_product_id = the umbrella SDD's id.
|
|
116
116
|
const subs = parsed.data.sub_integrations ?? [];
|
|
117
117
|
const seenSubIds = new Set([id]); // umbrella id is reserved
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// persona_c4system edge builder — emits one row per `Person(...)` actor on
|
|
2
|
-
// the L1 context diagram that maps to a known persona (per ADR 0155). This
|
|
2
|
+
// the L1 context diagram that maps to a known persona (per M-ADR 0155). This
|
|
3
3
|
// captures the C4 "Person uses System" relationship; the C4 Person
|
|
4
4
|
// abstraction reuses the existing Persona entity rather than a parallel one.
|
|
5
5
|
//
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// referenced columns are external FKs (capability_id and service_id
|
|
8
8
|
// are owned by the platform-root SDD, not by the consuming app SDD).
|
|
9
9
|
// The per-SDD builder logs `[finding] external` for each unresolved
|
|
10
|
-
// reference; the aggregator validates after merge per ADR 0134 §4.3.
|
|
10
|
+
// reference; the aggregator validates after merge per M-ADR 0134 §4.3.
|
|
11
11
|
//
|
|
12
12
|
// Profile gating: only application-product SDDs read this file. Other
|
|
13
13
|
// profiles emit zero rows (no convention defined).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// product_platform_dependencies builder — emits per-SDD rows recording a
|
|
2
2
|
// product's consumption of a platform-side (capability, service) pair.
|
|
3
3
|
//
|
|
4
|
-
// Multi-source ingestion (ADR 0157 parity principle; same shape as ADR 0156
|
|
4
|
+
// Multi-source ingestion (M-ADR 0157 parity principle; same shape as M-ADR 0156
|
|
5
5
|
// events ingestion). Two equal-status authoring sources, merged by PK:
|
|
6
6
|
//
|
|
7
7
|
// 1. FORWARD — `app-platform-dependencies.yaml` at the SDD root, the same
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// service (M-ADR 0192), `(product_id, target_capability_id, contract_path)`.
|
|
32
32
|
// The aggregator subsequently joins synchronous rows against platform-side
|
|
33
33
|
// `capability_service` to derive `cross_sdd_platform_capability` edges (per
|
|
34
|
-
// ADR 0134, federation); async rows resolve via the capability leg (no
|
|
34
|
+
// M-ADR 0134, federation); async rows resolve via the capability leg (no
|
|
35
35
|
// realiser service to join on) and skip the realiser-join.
|
|
36
36
|
//
|
|
37
37
|
// Cross-SDD references (target_capability_id, target_service_id) are
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Products discoverer — emits the SDD-as-Product row from the manifest.
|
|
3
3
|
*
|
|
4
|
-
* Per ADR 0134 + catalogue-metamodel.md: every SDD is one Product. The
|
|
4
|
+
* Per M-ADR 0134 + catalogue-metamodel.md: every SDD is one Product. The
|
|
5
5
|
* canonical builder emits ONE products.csv row per SDD — the SDD itself.
|
|
6
6
|
* Sibling SDDs (apps, integrations) get their own products.csv rows in
|
|
7
7
|
* their own builds; the aggregator merges by primary key.
|
|
8
8
|
*
|
|
9
|
-
* domain_id selection (per ADR 0136):
|
|
9
|
+
* domain_id selection (per M-ADR 0136):
|
|
10
10
|
* - For platform-profile SDDs: manifest.domainId when declared, else
|
|
11
11
|
* a sensible default for platform-profile categories (where a real
|
|
12
12
|
* `platform` domain exists locally).
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { PACKAGING_DEFAULT } from "../manifest-loader.js";
|
|
28
28
|
/**
|
|
29
29
|
* Default domain_id per category when the manifest doesn't declare one.
|
|
30
|
-
* Per ADR 0136 only platform-profile categories carry a default — app
|
|
30
|
+
* Per M-ADR 0136 only platform-profile categories carry a default — app
|
|
31
31
|
* and integration products legitimately have an empty domain_id.
|
|
32
32
|
*/
|
|
33
33
|
const DEFAULT_DOMAIN_BY_CATEGORY = {
|
|
@@ -49,7 +49,7 @@ export function buildProducts(inputs) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
-
// Per ADR 0136: app/integration/support products do not have a
|
|
52
|
+
// Per M-ADR 0136: app/integration/support products do not have a
|
|
53
53
|
// primary domain. Even if the manifest declares one (typically the
|
|
54
54
|
// umbrella name like "applications" or "integrations"), clear it
|
|
55
55
|
// here so the products.domain_id FK reference does not dangle
|
|
@@ -58,7 +58,7 @@ export function buildProducts(inputs) {
|
|
|
58
58
|
// so the override is traceable.
|
|
59
59
|
domainId = "";
|
|
60
60
|
if (m.domainId !== undefined && m.domainId.length > 0) {
|
|
61
|
-
inputs.findings.add("inferred", `products: domain_id cleared (manifest declared "${m.domainId}") for profile=${m.profile} per ADR 0136 — app/integration/support products do not have a primary domain`);
|
|
61
|
+
inputs.findings.add("inferred", `products: domain_id cleared (manifest declared "${m.domainId}") for profile=${m.profile} per M-ADR 0136 — app/integration/support products do not have a primary domain`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
// Lifecycle (schema 2.22.0): owner-declared in the manifest, default
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Repo-edge builders — repo_capability, repo_contract, feature_repo.
|
|
2
2
|
//
|
|
3
|
-
// Per ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
|
|
3
|
+
// Per M-ADR 0157 (Forward-Declared / Reverse-Derived Parity for Edge Catalogues):
|
|
4
4
|
// both edges ingest from forward-method declared sources AND reverse-method
|
|
5
5
|
// derived sources, merged by primary-key deduplication. The two paths are
|
|
6
6
|
// equal-status per the parity principle; neither displaces the other.
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
// services.primary_repo_id`. A service exposing a contract is implemented
|
|
33
33
|
// by a repo (its primary_repo_id) — the repo "owns" the contract by
|
|
34
34
|
// transitive inference. Works universally because primary_repo_id and
|
|
35
|
-
// service_contract are core-schema populated. Added per ADR 0157 §4.2
|
|
35
|
+
// service_contract are core-schema populated. Added per M-ADR 0157 §4.2
|
|
36
36
|
// amendment after an early-adopter 0/30 source_repo_id outcome.
|
|
37
37
|
// repo_contract Path Y — `contracts.source_repo_id` denormalisation
|
|
38
|
-
// (from OpenAPI `x-source-repo` hints per ADR 0110, or whatever
|
|
38
|
+
// (from OpenAPI `x-source-repo` hints per M-ADR 0110, or whatever
|
|
39
39
|
// populates the column). Augments Path X when adopters carry the hint.
|
|
40
40
|
// repo_capability Path X (universal) — `capability_service ⋈
|
|
41
41
|
// services.primary_repo_id`. Works in both forward and reverse method
|
|
42
42
|
// shapes because primary_repo_id is core-schema and capability_service
|
|
43
43
|
// is heavily authored.
|
|
44
|
-
// repo_capability Path Y (augments via ADR 0143 feature-grounding) —
|
|
44
|
+
// repo_capability Path Y (augments via M-ADR 0143 feature-grounding) —
|
|
45
45
|
// `capability_feature ⋈ feature_repo`. Adopters who have adopted the
|
|
46
46
|
// feature-grounding extractor get many additional rows.
|
|
47
47
|
//
|
|
48
|
-
// feature_repo: ADR 0143's feature-grounding extractor populates this in
|
|
48
|
+
// feature_repo: M-ADR 0143's feature-grounding extractor populates this in
|
|
49
49
|
// build.ts (10a). The forward declared `contributes_to.artifact_type:
|
|
50
50
|
// feature-set` path is documented in the metamodel but unused by any
|
|
51
51
|
// observed adopter — left empty here pending first-adopter authoring.
|
|
@@ -68,7 +68,7 @@ import { normaliseAdditionalContextEntry, REPO_CONFIG_CONTRACT_KINDS, } from "./
|
|
|
68
68
|
* identity-DEPENDENT, and so is this. A first review of D8 flagged the opposite
|
|
69
69
|
* and was wrong.
|
|
70
70
|
*
|
|
71
|
-
* Caller supplies `existingIds` so this only ever ADDS: the ADR 0143
|
|
71
|
+
* Caller supplies `existingIds` so this only ever ADDS: the M-ADR 0143
|
|
72
72
|
* feature-grounding extractor and the capability-spec walker own their rows and
|
|
73
73
|
* are authoritative where they overlap.
|
|
74
74
|
*/
|
|
@@ -109,7 +109,7 @@ export function deriveFeatureRepoFromServices(inputs) {
|
|
|
109
109
|
export function buildRepoEdges(inputs) {
|
|
110
110
|
const repoCapability = collectRepoCapability(inputs);
|
|
111
111
|
const repoContract = collectRepoContract(inputs);
|
|
112
|
-
// feature_repo is populated later in build.ts by the ADR 0143
|
|
112
|
+
// feature_repo is populated later in build.ts by the M-ADR 0143
|
|
113
113
|
// feature-grounding extractor; this builder doesn't contribute.
|
|
114
114
|
const featureRepo = [];
|
|
115
115
|
return { repoCapability, repoContract, featureRepo };
|
|
@@ -222,9 +222,9 @@ function collectRepoCapability(inputs) {
|
|
|
222
222
|
missingPrimaryRepoSvcs++;
|
|
223
223
|
}
|
|
224
224
|
if (missingPrimaryRepoSvcs > 0) {
|
|
225
|
-
inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.3)`);
|
|
225
|
+
inputs.findings.add("orphan", `repo_capability: ${missingPrimaryRepoSvcs} capability_service rows reference services with empty primary_repo_id — reverse-method Path X (capability_service ⋈ services.primary_repo_id) skipped those edges (per M-ADR 0157 §4.3)`);
|
|
226
226
|
}
|
|
227
|
-
// Reverse path Y — capability_feature ⋈ feature_repo (transitive via ADR 0143).
|
|
227
|
+
// Reverse path Y — capability_feature ⋈ feature_repo (transitive via M-ADR 0143).
|
|
228
228
|
const featuresToRepos = new Map();
|
|
229
229
|
for (const fr of featureRepoRows) {
|
|
230
230
|
const fid = String(fr["feature_id"] ?? "");
|
|
@@ -299,7 +299,7 @@ function collectRepoContract(inputs) {
|
|
|
299
299
|
}
|
|
300
300
|
// Reverse path X (universal) — service_contract ⋈ services.primary_repo_id.
|
|
301
301
|
// A service exposing a contract is implemented by a repo; the repo "owns"
|
|
302
|
-
// the contract by transitive inference. Added per ADR 0157 §4.2 amendment
|
|
302
|
+
// the contract by transitive inference. Added per M-ADR 0157 §4.2 amendment
|
|
303
303
|
// (an early adopter's source_repo_id was 0/30 populated, so
|
|
304
304
|
// a derivation that depends only on it leaves them empty).
|
|
305
305
|
const repoBySvcId = new Map();
|
|
@@ -323,10 +323,10 @@ function collectRepoContract(inputs) {
|
|
|
323
323
|
emit(repoId, ctrId);
|
|
324
324
|
}
|
|
325
325
|
if (missingPrimaryRepoSvcs > 0) {
|
|
326
|
-
inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per ADR 0157 §4.2)`);
|
|
326
|
+
inputs.findings.add("orphan", `repo_contract: ${missingPrimaryRepoSvcs} service_contract rows reference services with empty primary_repo_id — reverse-method Path X (service_contract ⋈ services.primary_repo_id) skipped those edges (per M-ADR 0157 §4.2)`);
|
|
327
327
|
}
|
|
328
328
|
// Reverse path Y — contracts.source_repo_id denormalisation.
|
|
329
|
-
// Adopters who carry OpenAPI x-source-repo hints (per ADR 0110) or whose
|
|
329
|
+
// Adopters who carry OpenAPI x-source-repo hints (per M-ADR 0110) or whose
|
|
330
330
|
// contracts builder otherwise populates source_repo_id get one row per
|
|
331
331
|
// (source_repo_id, contract.id) for free.
|
|
332
332
|
for (const c of contractRows) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Repositories discoverer — emits `repositories.csv` rows from
|
|
2
2
|
// `orchestration/repo-config/<domain>/<repo>.yaml` (recursive).
|
|
3
3
|
//
|
|
4
|
-
// Per ADR 0134 + Q7.1 resolution: `service_type` is constrained to the
|
|
4
|
+
// Per M-ADR 0134 + Q7.1 resolution: `service_type` is constrained to the
|
|
5
5
|
// canonical `enums.service_type` set (api_service, worker, gateway,
|
|
6
6
|
// frontend, mobile, library, infrastructure, documentation). Unknown
|
|
7
7
|
// values log a `[finding] convention` and are kept verbatim — the FK
|
|
@@ -173,13 +173,13 @@ export async function buildRepositories(inputs) {
|
|
|
173
173
|
inputs.findings.add("convention", `${cfg.relPath}: missing required \`domain\` field`);
|
|
174
174
|
}
|
|
175
175
|
else if (!inputs.knownDomainIds.has(domain)) {
|
|
176
|
-
// Per ADR 0136: leave domain_id empty when the declared domain
|
|
176
|
+
// Per M-ADR 0136: leave domain_id empty when the declared domain
|
|
177
177
|
// doesn't resolve locally (typical for app/integration SDDs whose
|
|
178
178
|
// repo-config declares "applications"/"integrations").
|
|
179
179
|
//
|
|
180
180
|
// NOT `orphan`, and the argument is about TIER rather than a relabelling
|
|
181
181
|
// of the same claim. A locally-unresolvable domain is two different
|
|
182
|
-
// things and THIS TIER CANNOT TELL THEM APART: ADR 0136 externality (the
|
|
182
|
+
// things and THIS TIER CANNOT TELL THEM APART: M-ADR 0136 externality (the
|
|
183
183
|
// domain is owned by a sibling SDD — correct, expected, nothing to fix)
|
|
184
184
|
// or a genuinely dangling domain name (a typo). The discriminator lives
|
|
185
185
|
// at estate scope, not here. Measured across two estates: 137
|
|
@@ -191,7 +191,7 @@ export async function buildRepositories(inputs) {
|
|
|
191
191
|
// §4.2 names. `inferred` records the builder's documented resolution
|
|
192
192
|
// (empty the column, keep the repository) without claiming a defect this
|
|
193
193
|
// build cannot see.
|
|
194
|
-
inputs.findings.add("inferred", `${cfg.relPath}: domain "${domain}" has no docs/domains/${domain}/ directory — domain_id left empty per ADR 0136. Whether that is external ownership or a dangling name is decidable only at estate scope`);
|
|
194
|
+
inputs.findings.add("inferred", `${cfg.relPath}: domain "${domain}" has no docs/domains/${domain}/ directory — domain_id left empty per M-ADR 0136. Whether that is external ownership or a dangling name is decidable only at estate scope`);
|
|
195
195
|
resolvedDomain = "";
|
|
196
196
|
}
|
|
197
197
|
const loc = locByRepo.get(repoId);
|
|
@@ -252,19 +252,23 @@ async function loadRepoConfigs(sddPath, findings) {
|
|
|
252
252
|
raw = parseYaml(text);
|
|
253
253
|
}
|
|
254
254
|
catch (err) {
|
|
255
|
-
findings.add("convention", `failed to parse YAML at ${relative(sddPath, path)}: ${err.message}
|
|
255
|
+
findings.add("convention", `failed to parse YAML at ${relative(sddPath, path)}: ${err.message} — repository dropped`, "error", relative(sddPath, path));
|
|
256
256
|
continue;
|
|
257
257
|
}
|
|
258
258
|
if (!raw || typeof raw !== "object") {
|
|
259
|
-
findings.add("convention", `${relative(sddPath, path)}: not an object —
|
|
259
|
+
findings.add("convention", `${relative(sddPath, path)}: not an object — repository dropped`, "error", relative(sddPath, path));
|
|
260
260
|
continue;
|
|
261
261
|
}
|
|
262
|
+
// A repo-config that cannot be read takes its repository, services and
|
|
263
|
+
// capability edges out of the catalogue with it. That is an error, not
|
|
264
|
+
// advice: as a warning it passed validation while the rows vanished, and
|
|
265
|
+
// only a downstream aggregate's FK check noticed.
|
|
262
266
|
const parsed = RepoConfigSchema.safeParse(raw);
|
|
263
267
|
if (!parsed.success) {
|
|
264
268
|
const issues = parsed.error.issues
|
|
265
269
|
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
266
270
|
.join("; ");
|
|
267
|
-
findings.add("convention", `${relative(sddPath, path)}: schema violation — ${issues}
|
|
271
|
+
findings.add("convention", `${relative(sddPath, path)}: schema violation — ${issues} — repository dropped`, "error", relative(sddPath, path));
|
|
268
272
|
continue;
|
|
269
273
|
}
|
|
270
274
|
const repoFull = parsed.data.repo;
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
// directory name, which is the catalogue feature id; plus one per requirement an open,
|
|
9
9
|
// agreed change ADDS (stage agreed).
|
|
10
10
|
// requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED or RENAMED a
|
|
11
|
-
// requirement that is current now
|
|
11
|
+
// requirement that is current now, under any name a later RENAMED gave it. A change carrying
|
|
12
|
+
// baseline-evidence.md is a baseline. Statuses follow renames the same way.
|
|
13
|
+
// changes each change in the store, open or archived (M-ADR 0223 §4.2): its directory name, its
|
|
14
|
+
// undated name, kind, status (archived, or its agreement record's) and verdict.
|
|
15
|
+
// change_work_order per (change, repository): the record's work order, agreed or completed.
|
|
16
|
+
// change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
|
|
17
|
+
// the undated name so the row survives archive.
|
|
12
18
|
// requirement_status per (requirement, repository): observed from a baseline's `**Observed in:**` line;
|
|
13
19
|
// declared-shipped from an archived change whose agreement record's verdict is PASS, through
|
|
14
20
|
// its work orders' `implements` lists; agreed from an open change whose record is agreed.
|
|
@@ -20,7 +26,7 @@
|
|
|
20
26
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
21
27
|
import { join } from "node:path";
|
|
22
28
|
import { parse as parseYaml } from "yaml";
|
|
23
|
-
import { observedIn, orderArchives, parseRequirements } from "../../behaviour-store/archive.js";
|
|
29
|
+
import { observedIn, orderArchives, parseRequirements, renamePairs } from "../../behaviour-store/archive.js";
|
|
24
30
|
export { observedIn, parseRequirements };
|
|
25
31
|
const STORE = "openspec";
|
|
26
32
|
const AGREEMENTS = join("docs", "agreements");
|
|
@@ -62,6 +68,17 @@ async function deltaOf(changeDir) {
|
|
|
62
68
|
}
|
|
63
69
|
return out;
|
|
64
70
|
}
|
|
71
|
+
async function renamesOf(changeDir) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const feature of await dirs(join(changeDir, "specs"))) {
|
|
74
|
+
const text = await readText(join(changeDir, "specs", feature, "spec.md"));
|
|
75
|
+
if (text === null)
|
|
76
|
+
continue;
|
|
77
|
+
for (const r of renamePairs(text))
|
|
78
|
+
out.push({ feature, ...r });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
65
82
|
async function agreementRecord(sddPath, change) {
|
|
66
83
|
const text = await readText(join(sddPath, AGREEMENTS, `${change}.yaml`));
|
|
67
84
|
if (text === null)
|
|
@@ -82,10 +99,33 @@ export async function archiveOrder(sddPath, names, findings) {
|
|
|
82
99
|
}
|
|
83
100
|
return order;
|
|
84
101
|
}
|
|
102
|
+
const CHANGE_STATUSES = new Set(["proposed", "agreed", "superseded", "withdrawn"]);
|
|
103
|
+
/**
|
|
104
|
+
* The work-item ids a proposal's `## Work Items` section lists (M-ADR 0223 §4.2): one per bullet, the backticked
|
|
105
|
+
* id if there is one, else the bullet's first word. HTML comments (the template's guidance) are ignored.
|
|
106
|
+
*/
|
|
107
|
+
export function parseWorkItems(proposal) {
|
|
108
|
+
const text = proposal.replace(/<!--[\s\S]*?-->/g, "");
|
|
109
|
+
const m = /^##\s+Work Items\s*$/im.exec(text);
|
|
110
|
+
if (!m)
|
|
111
|
+
return [];
|
|
112
|
+
const rest = text.slice(m.index + m[0].length);
|
|
113
|
+
const end = /^##\s/m.exec(rest);
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const line of (end ? rest.slice(0, end.index) : rest).split("\n")) {
|
|
116
|
+
const b = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
117
|
+
if (!b)
|
|
118
|
+
continue;
|
|
119
|
+
const id = (/`([^`]+)`/.exec(b[1])?.[1] ?? b[1].split(/\s+/)[0] ?? "").replace(/[:,;]+$/, "").trim();
|
|
120
|
+
if (id && !ids.includes(id))
|
|
121
|
+
ids.push(id);
|
|
122
|
+
}
|
|
123
|
+
return ids;
|
|
124
|
+
}
|
|
85
125
|
const rel = (sddPath, p) => p.slice(sddPath.length + 1).split("\\").join("/");
|
|
86
126
|
export async function buildRequirements(inputs) {
|
|
87
127
|
const { sddPath, findings } = inputs;
|
|
88
|
-
const empty = { requirements: [], lineage: [], status: [] };
|
|
128
|
+
const empty = { requirements: [], lineage: [], status: [], changes: [], workOrders: [], workItems: [] };
|
|
89
129
|
const store = join(sddPath, STORE);
|
|
90
130
|
if (!(await isDir(store)))
|
|
91
131
|
return empty;
|
|
@@ -115,6 +155,28 @@ export async function buildRequirements(inputs) {
|
|
|
115
155
|
const events = [];
|
|
116
156
|
const lineage = [];
|
|
117
157
|
const unknownRepos = new Set();
|
|
158
|
+
const changes = [];
|
|
159
|
+
const workOrders = [];
|
|
160
|
+
const workItems = [];
|
|
161
|
+
// The change itself, its work orders and its work items (M-ADR 0223 §4.2).
|
|
162
|
+
const recordChange = async (id, name, kind, status, archivedOn, changeDir, record) => {
|
|
163
|
+
const verdict = record?.verdict?.result === "PASS" || record?.verdict?.result === "FAIL" ? record.verdict.result : "";
|
|
164
|
+
changes.push({ id, name, change_kind: kind, status, archived_on: archivedOn, verdict, source_sdd_id: "", platform_sdd_id: "" });
|
|
165
|
+
for (const wo of record?.work_orders ?? []) {
|
|
166
|
+
if (!wo.repo)
|
|
167
|
+
continue;
|
|
168
|
+
if (!inputs.knownRepoIds.has(wo.repo)) {
|
|
169
|
+
unknownRepos.add(wo.repo);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const woStatus = wo.status === "agreed" || wo.status === "completed" ? wo.status : verdict === "PASS" && status === "archived" ? "completed" : "agreed";
|
|
173
|
+
workOrders.push({ id: `${id}__${wo.repo}`, change_id: id, repo_id: wo.repo, status: woStatus, source_sdd_id: "", platform_sdd_id: "" });
|
|
174
|
+
}
|
|
175
|
+
const proposal = await readText(join(changeDir, "proposal.md"));
|
|
176
|
+
for (const item of proposal === null ? [] : parseWorkItems(proposal)) {
|
|
177
|
+
workItems.push({ id: `${name}__${item}`, change_id: id, change_name: name, work_item_id: item, source_sdd_id: "", platform_sdd_id: "" });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
118
180
|
const pushEvent = (e) => {
|
|
119
181
|
if (!inputs.knownRepoIds.has(e.repo)) {
|
|
120
182
|
unknownRepos.add(e.repo);
|
|
@@ -123,21 +185,25 @@ export async function buildRequirements(inputs) {
|
|
|
123
185
|
if (requirements.has(e.requirementId))
|
|
124
186
|
events.push(e);
|
|
125
187
|
};
|
|
126
|
-
// 2. Archived changes, in archive order.
|
|
127
|
-
|
|
188
|
+
// 2. Archived changes, in archive order. A requirement is named as it was when each change archived, so its
|
|
189
|
+
// rows are carried forward through every later RENAMED (and one in the same change) to the name it has now;
|
|
190
|
+
// otherwise a rename would read as a requirement born at the rename, with no baseline.
|
|
191
|
+
const archived = [];
|
|
192
|
+
const renames = [];
|
|
193
|
+
const archives = await archiveOrder(sddPath, await dirs(join(store, "changes", "archive")), findings);
|
|
194
|
+
for (const [index, dir] of archives.entries()) {
|
|
128
195
|
const changeDir = join(store, "changes", "archive", dir);
|
|
196
|
+
renames.push(await renamesOf(changeDir));
|
|
129
197
|
const change = dir.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
130
198
|
const archivedOn = /^(\d{4}-\d{2}-\d{2})-/.exec(dir)?.[1] ?? "";
|
|
131
199
|
const evidence = await readText(join(changeDir, "baseline-evidence.md"));
|
|
132
200
|
const kind = evidence !== null ? "baseline" : "change";
|
|
201
|
+
await recordChange(dir, change, kind, "archived", archivedOn, changeDir, await agreementRecord(sddPath, change));
|
|
133
202
|
const delta = await deltaOf(changeDir);
|
|
134
203
|
for (const d of delta) {
|
|
135
204
|
if (d.operation === "REMOVED")
|
|
136
205
|
continue;
|
|
137
|
-
|
|
138
|
-
if (!requirements.has(requirementId))
|
|
139
|
-
continue;
|
|
140
|
-
lineage.push({ id: `${requirementId}__${dir}`, requirement_id: requirementId, change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" });
|
|
206
|
+
archived.push({ index, feature: d.feature, name: d.name, lineage: { change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" } });
|
|
141
207
|
}
|
|
142
208
|
if (evidence !== null) {
|
|
143
209
|
const repos = observedIn(evidence);
|
|
@@ -147,7 +213,7 @@ export async function buildRequirements(inputs) {
|
|
|
147
213
|
for (const repo of repos)
|
|
148
214
|
for (const d of delta)
|
|
149
215
|
if (d.operation !== "REMOVED")
|
|
150
|
-
|
|
216
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo, status: "observed", basis: dir } });
|
|
151
217
|
continue;
|
|
152
218
|
}
|
|
153
219
|
const record = await agreementRecord(sddPath, change);
|
|
@@ -162,13 +228,43 @@ export async function buildRequirements(inputs) {
|
|
|
162
228
|
findings.add("convention", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", which the change's delta does not name`);
|
|
163
229
|
}
|
|
164
230
|
for (const d of match)
|
|
165
|
-
|
|
231
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo: wo.repo, status: "declared-shipped", basis: dir } });
|
|
166
232
|
}
|
|
167
233
|
}
|
|
168
234
|
}
|
|
235
|
+
const current = (feature, name, from) => {
|
|
236
|
+
let n = name;
|
|
237
|
+
for (let j = from; j < renames.length; j++)
|
|
238
|
+
for (const r of renames[j])
|
|
239
|
+
if (r.feature === feature && r.from === n)
|
|
240
|
+
n = r.to;
|
|
241
|
+
return `${feature}::${n}`;
|
|
242
|
+
};
|
|
243
|
+
// One lineage row per (requirement, change): a change that renames and modifies a requirement records MODIFIED,
|
|
244
|
+
// the behaviour change, and its delta still shows the rename.
|
|
245
|
+
const lineageById = new Map();
|
|
246
|
+
for (const a of archived) {
|
|
247
|
+
const requirementId = current(a.feature, a.name, a.index);
|
|
248
|
+
if (a.event)
|
|
249
|
+
pushEvent({ requirementId, ...a.event });
|
|
250
|
+
else if (a.lineage && requirements.has(requirementId)) {
|
|
251
|
+
const id = `${requirementId}__${a.lineage["change_id"]}`;
|
|
252
|
+
if (lineageById.get(id)?.["operation"] === "MODIFIED")
|
|
253
|
+
continue;
|
|
254
|
+
lineageById.set(id, { id, requirement_id: requirementId, ...a.lineage });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lineage.push(...lineageById.values());
|
|
169
258
|
// 3. Open changes whose agreement record is agreed.
|
|
170
259
|
for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
|
|
171
260
|
const record = await agreementRecord(sddPath, dir);
|
|
261
|
+
const openKind = (await readText(join(store, "changes", dir, "baseline-evidence.md"))) !== null ? "baseline" : "change";
|
|
262
|
+
let openStatus = record?.status ?? "proposed";
|
|
263
|
+
if (!CHANGE_STATUSES.has(openStatus)) {
|
|
264
|
+
findings.add("convention", `${AGREEMENTS}/${dir}.yaml: status "${openStatus}" is not a change status; catalogued as proposed`);
|
|
265
|
+
openStatus = "proposed";
|
|
266
|
+
}
|
|
267
|
+
await recordChange(dir, dir, openKind, openStatus, "", join(store, "changes", dir), record);
|
|
172
268
|
if (record?.status !== "agreed")
|
|
173
269
|
continue;
|
|
174
270
|
const delta = await deltaOf(join(store, "changes", dir));
|
|
@@ -203,6 +299,9 @@ export async function buildRequirements(inputs) {
|
|
|
203
299
|
requirements: [...requirements.values()].sort(byId),
|
|
204
300
|
lineage,
|
|
205
301
|
status: [...status.values()].sort(byId),
|
|
302
|
+
changes: changes.sort(byId),
|
|
303
|
+
workOrders: workOrders.sort(byId),
|
|
304
|
+
workItems: workItems.sort(byId),
|
|
206
305
|
};
|
|
207
306
|
}
|
|
208
307
|
//# sourceMappingURL=requirements.js.map
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// Screen composition-tree analysis for openui-lang blocks (per ADR 0168).
|
|
1
|
+
// Screen composition-tree analysis for openui-lang blocks (per M-ADR 0168).
|
|
2
2
|
//
|
|
3
3
|
// The screens builder (screens.ts) lowers a ```openui-lang block into catalogue
|
|
4
|
-
// rows by flat token-scanning. ADR 0168 adds a *fidelity floor*: the block must
|
|
4
|
+
// rows by flat token-scanning. M-ADR 0168 adds a *fidelity floor*: the block must
|
|
5
5
|
// be a faithful tree, not a flat / empty / mis-rooted "widget bag". That needs
|
|
6
6
|
// the structural shape the flat scan throws away — so this module parses the
|
|
7
7
|
// block into a tree and computes the metrics the depth findings reason about.
|
|
8
8
|
//
|
|
9
9
|
// Pure functions, no I/O. The metrics reason about TREE SHAPE only — never about
|
|
10
|
-
// any component vocabulary (adopter-agnostic by construction, ADR 0168 §4.5).
|
|
10
|
+
// any component vocabulary (adopter-agnostic by construction, M-ADR 0168 §4.5).
|
|
11
11
|
//
|
|
12
|
-
// Grammar recap (ADR 0166 / openui-lang-grammar-reference.md): one
|
|
12
|
+
// Grammar recap (M-ADR 0166 / openui-lang-grammar-reference.md): one
|
|
13
13
|
// `identifier = Expression` statement per line; the first statement is `root`;
|
|
14
14
|
// composition is by identifier reference inside list literals. A component is a
|
|
15
15
|
// Capitalised call head (`Name(`); local references are lowercase ids that are
|
|
@@ -73,7 +73,7 @@ function parseArgsActions(rhs) {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Strip openui-lang inline comments: from the first *unquoted* `#` to
|
|
76
|
-
* end-of-line, per line (ADR 0168 §4.4, defect g). A `Capitalised(` token inside
|
|
76
|
+
* end-of-line, per line (M-ADR 0168 §4.4, defect g). A `Capitalised(` token inside
|
|
77
77
|
* a comment must not be lexed as a component. `#` inside a string literal is
|
|
78
78
|
* preserved.
|
|
79
79
|
*/
|
|
@@ -113,7 +113,7 @@ function parseStatements(cleanBlock) {
|
|
|
113
113
|
const rhs = m[2];
|
|
114
114
|
// A component statement's RHS is a call (`Text("Q4")`) or a reference — never a
|
|
115
115
|
// bare string literal. A line like `action="basket-updated"` (the block-level
|
|
116
|
-
// screen_action messaging contract, ADR 0166) matches the statement shape with
|
|
116
|
+
// screen_action messaging contract, M-ADR 0166) matches the statement shape with
|
|
117
117
|
// id=`action`; skip it so it isn't lexed as a phantom component (and so a node
|
|
118
118
|
// carrying an `action=` attr doesn't pick it up as a spurious child).
|
|
119
119
|
if (/^["']/.test(rhs))
|