@documentation-robotics/cli 0.3.0 → 0.4.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/README.md +10 -14
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/cards/build.js +21 -1
- package/dist/analyzers/cards/build.js.map +2 -2
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +76 -4
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +12 -6
- package/dist/analyzers/cbm-responses.js.map +2 -2
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +6 -2
- package/dist/analyzers/edge-relationships.js.map +2 -2
- package/dist/analyzers/extraction-plan.js +541 -0
- package/dist/analyzers/extraction-plan.js.map +7 -0
- package/dist/analyzers/index-job.js +229 -0
- package/dist/analyzers/index-job.js.map +7 -0
- package/dist/cli.js +12 -10
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +120 -18
- package/dist/coding-agents/drbot-mcp.js.map +2 -2
- package/dist/commands/add.js +4 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +35 -35
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +16 -8
- package/dist/commands/changeset-verify.js.map +2 -2
- package/dist/commands/changeset.js +41 -11
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +35 -31
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +2 -12
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +3 -3
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +14 -13
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +28 -120
- package/dist/commands/upgrade.js.map +2 -2
- package/dist/commands/validate.js +19 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +30 -12
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/annotation-store.js +15 -5
- package/dist/core/annotation-store.js.map +2 -2
- package/dist/core/catalog-migration.js +2 -1
- package/dist/core/catalog-migration.js.map +2 -2
- package/dist/core/changeset-base-index.js +7 -0
- package/dist/core/changeset-base-index.js.map +2 -2
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-verify.js +356 -26
- package/dist/core/changeset-verify.js.map +3 -3
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/migration-registry.js +13 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/project-init.js +53 -0
- package/dist/core/project-init.js.map +7 -0
- package/dist/core/relationship-staging.js +94 -1
- package/dist/core/relationship-staging.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +83 -3
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staging-area.js +85 -8
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/upgrade-plan.js +117 -0
- package/dist/core/upgrade-plan.js.map +7 -0
- package/dist/generated/relationship-index.js +124 -15
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
- package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +7 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
- package/dist/integrations/claude_code/docs/README.md +20 -1
- package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
- package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
- package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
- package/dist/mcp/changeset-only.js +14 -3
- package/dist/mcp/changeset-only.js.map +2 -2
- package/dist/mcp/project-root.js +85 -0
- package/dist/mcp/project-root.js.map +7 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/server-instructions.js.map +7 -0
- package/dist/mcp/strict-schema.js +52 -0
- package/dist/mcp/strict-schema.js.map +7 -0
- package/dist/mcp/tool-registry.js +39 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
- package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
- package/dist/mcp/tools/changeset-commit.js +224 -0
- package/dist/mcp/tools/changeset-commit.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +133 -57
- package/dist/mcp/tools/changeset-editing.js.map +3 -3
- package/dist/mcp/tools/changeset-lifecycle.js +1 -1
- package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +12 -6
- package/dist/mcp/tools/changeset-verify.js.map +2 -2
- package/dist/mcp/tools/codebase-extract.js +836 -0
- package/dist/mcp/tools/codebase-extract.js.map +7 -0
- package/dist/mcp/tools/codebase-index.js +125 -0
- package/dist/mcp/tools/codebase-index.js.map +7 -0
- package/dist/mcp/tools/codebase-shared.js +265 -0
- package/dist/mcp/tools/codebase-shared.js.map +7 -0
- package/dist/mcp/tools/codebase-verify.js +48 -0
- package/dist/mcp/tools/codebase-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +15 -0
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +189 -73
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-update.js +16 -12
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/model-validate.js +46 -4
- package/dist/mcp/tools/model-validate.js.map +2 -2
- package/dist/mcp/tools/project-status-collectors.js +381 -0
- package/dist/mcp/tools/project-status-collectors.js.map +7 -0
- package/dist/mcp/tools/project-status.js +237 -0
- package/dist/mcp/tools/project-status.js.map +7 -0
- package/dist/mcp/tools/project-tools.js +505 -0
- package/dist/mcp/tools/project-tools.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +89 -20
- package/dist/mcp/tools/relationship-tools.js.map +2 -2
- package/dist/mcp/tools/review-open.js +210 -0
- package/dist/mcp/tools/review-open.js.map +7 -0
- package/dist/mcp/tools/shared.js +10 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +31 -2
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/schemas/bundled/analyzers/cbm.json +22 -10
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +1 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +1 -1
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +3 -3
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +145 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +508 -237
- package/dist/server/server.js.map +3 -3
- package/dist/server/viewer-registry.js +264 -0
- package/dist/server/viewer-registry.js.map +7 -0
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/source-reference.js +58 -27
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
- package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +1 -1
- package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
- package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { MappingLoader } from "../analyzers/mapping-loader.js";
|
|
2
|
+
import { relationshipFor } from "../analyzers/edge-relationships.js";
|
|
3
|
+
import { tier0RulesFrom } from "../analyzers/tier0-rules.js";
|
|
4
|
+
import { getValidRelationships } from "../generated/relationship-index.js";
|
|
5
|
+
const NEVER_OBSERVABLE_LAYERS = /* @__PURE__ */ new Set(["motivation", "business", "product"]), NO_TEST_CARDS = /\.[cm]?[jt]sx?$/, ROUTE_SYMBOL = /^(?:__route__[A-Z]+__\/|[A-Z]+ \/)/, BARREL_FILE = /(^|\/)(index\.[cm]?[jt]sx?|__init__\.py|mod\.rs)$/, CODE_FACTS = {
|
|
6
|
+
"api.operation": ["http_method", "http_path"]
|
|
7
|
+
}, EDGE_LABELS = {
|
|
8
|
+
DEPLOYS: { oneEndOf: ["Resource", "Image"], why: "links infrastructure resources and images" },
|
|
9
|
+
INFRA_MAPS: { oneEndOf: ["Resource"], why: "links infrastructure resources" },
|
|
10
|
+
DEPENDS_ON: { oneEndOf: ["Package", "Image", "Resource"], why: "links packages, images and resources" },
|
|
11
|
+
SPAWNS: { oneEndOf: ["Process"], why: "links code to the processes it launches" }
|
|
12
|
+
}, EDGE_ENDPOINTS = {
|
|
13
|
+
DEFINES_METHOD: {
|
|
14
|
+
to: (element) => {
|
|
15
|
+
const symbols = sourceLocations(element).map((l) => l.symbol).filter((s) => !!s);
|
|
16
|
+
return symbols.length === 0 || symbols.some((s) => s.includes("."));
|
|
17
|
+
},
|
|
18
|
+
why: "links a class to its methods only, and this function is not a method"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function catalogHasTriple(id) {
|
|
22
|
+
const parts = id.split(".");
|
|
23
|
+
return parts.length !== 5 ? !1 : getValidRelationships(`${parts[0]}.${parts[1]}`, parts[2], `${parts[3]}.${parts[4]}`).length > 0;
|
|
24
|
+
}
|
|
25
|
+
function sourceLocations(element) {
|
|
26
|
+
return (element.sourceReference?.locations ?? []).filter((l) => typeof l?.file == "string" && l.file.length > 0).map((l) => ({ file: l.file.replace(/\\/g, "/").replace(/^\.\//, ""), ...l.symbol ? { symbol: l.symbol } : {} }));
|
|
27
|
+
}
|
|
28
|
+
class ExtractionCoverage {
|
|
29
|
+
analyzer;
|
|
30
|
+
producers = /* @__PURE__ */ new Map();
|
|
31
|
+
/** Layers a Tier-0 rule narrows to with `layer.*`. */
|
|
32
|
+
wildcardLayers = /* @__PURE__ */ new Map();
|
|
33
|
+
/** Attributes the node mapping fills, by element type. */
|
|
34
|
+
filled = /* @__PURE__ */ new Map();
|
|
35
|
+
edgeMappings;
|
|
36
|
+
hasTriple;
|
|
37
|
+
tripleCache = /* @__PURE__ */ new Map();
|
|
38
|
+
constructor(input) {
|
|
39
|
+
this.analyzer = input.analyzer, this.hasTriple = input.hasTriple ?? catalogHasTriple;
|
|
40
|
+
const add = (type, producer) => {
|
|
41
|
+
const list = this.producers.get(type) ?? [];
|
|
42
|
+
list.push(producer), this.producers.set(type, list);
|
|
43
|
+
}, fill = (type, attributes) => {
|
|
44
|
+
const set = this.filled.get(type) ?? /* @__PURE__ */ new Set();
|
|
45
|
+
for (const a of attributes)
|
|
46
|
+
set.add(a);
|
|
47
|
+
this.filled.set(type, set);
|
|
48
|
+
}, labels = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const m of input.nodeMappings) {
|
|
50
|
+
if (!m.dr_layer || !m.dr_element_type)
|
|
51
|
+
continue;
|
|
52
|
+
labels.add(m.cbm_label);
|
|
53
|
+
const producer = { via: `node mapping ${m.cbm_label}`, label: m.cbm_label }, mapped = m, attributes = [
|
|
54
|
+
...(mapped.attribute_mappings ?? []).map((a) => a.dr_attribute).filter((a) => !!a),
|
|
55
|
+
...Object.keys(m.dr_element_fields ?? {})
|
|
56
|
+
];
|
|
57
|
+
for (const type of [m.dr_element_type, mapped.dr_element_type_promoted].filter((t) => !!t))
|
|
58
|
+
add(`${m.dr_layer}.${type}`, producer), fill(`${m.dr_layer}.${type}`, attributes);
|
|
59
|
+
}
|
|
60
|
+
for (const rule of input.rules) {
|
|
61
|
+
if (rule.action.kind === "reject")
|
|
62
|
+
continue;
|
|
63
|
+
for (const label of rule.labels)
|
|
64
|
+
labels.add(label);
|
|
65
|
+
const producer = { via: `Tier-0 rule ${rule.name}`, ...rule.when.file ? { file: rule.when.file } : {} };
|
|
66
|
+
if (rule.action.kind === "decide")
|
|
67
|
+
add(`${rule.action.layer}.${rule.action.type}`, producer);
|
|
68
|
+
else
|
|
69
|
+
for (const c of rule.action.candidates)
|
|
70
|
+
c.type ? add(`${c.layer}.${c.type}`, producer) : this.wildcardLayers.set(c.layer, [...this.wildcardLayers.get(c.layer) ?? [], producer]);
|
|
71
|
+
}
|
|
72
|
+
for (const [type, facts] of Object.entries(CODE_FACTS))
|
|
73
|
+
fill(type, facts);
|
|
74
|
+
this.edgeMappings = input.edgeMappings.filter((m) => m.usage === "verification_and_mapping" && (!EDGE_LABELS[m.cbm_edge] || EDGE_LABELS[m.cbm_edge].oneEndOf.some((l) => labels.has(l))));
|
|
75
|
+
}
|
|
76
|
+
/** Element types extraction can produce at all (`layer.type`), sorted. */
|
|
77
|
+
observableTypes() {
|
|
78
|
+
return [...this.producers.keys()].sort();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether extraction could produce this element. `codeFiles` are the files the build was
|
|
82
|
+
* extracted from; they stand in for the element's own source file when it names none.
|
|
83
|
+
*/
|
|
84
|
+
element(element, codeFiles = []) {
|
|
85
|
+
const type = `${element.layer}.${element.type}`;
|
|
86
|
+
if (NEVER_OBSERVABLE_LAYERS.has(element.layer))
|
|
87
|
+
return { observable: !1, reason: `extraction does not read intent (${element.layer}) from code` };
|
|
88
|
+
const producers = [...this.producers.get(type) ?? [], ...this.wildcardLayers.get(element.layer) ?? []];
|
|
89
|
+
if (producers.length === 0)
|
|
90
|
+
return { observable: !1, reason: `the ${this.analyzer} mapping produces no ${type} elements` };
|
|
91
|
+
const locations = sourceLocations(element), files = locations.length > 0 ? locations.map((l) => l.file) : codeFiles;
|
|
92
|
+
if (element.layer === "testing" && files.length > 0 && files.every((f) => NO_TEST_CARDS.test(f)))
|
|
93
|
+
return {
|
|
94
|
+
observable: !1,
|
|
95
|
+
reason: "JavaScript and TypeScript test cases are anonymous callbacks and never become extraction cards"
|
|
96
|
+
};
|
|
97
|
+
const unlocated = isUnlocatedOperation(element, locations);
|
|
98
|
+
if (producers.filter((p) => !(p.file && locations.length > 0 && !locations.some((l) => p.file.test(l.file)) || p.label === "Route" && (unlocated || isReExport(element, locations)))).length === 0) {
|
|
99
|
+
const routeOnly = producers.every((p) => p.label === "Route");
|
|
100
|
+
return {
|
|
101
|
+
observable: !1,
|
|
102
|
+
...routeOnly && unlocated ? { unlocated: !0 } : {},
|
|
103
|
+
reason: routeOnly ? unlocated ? `extraction produces ${type} only from HTTP routes, and this one has no method, path or source reference to find it by. To make it verifiable, give it http_method and http_path (for a route), or sourceFile and sourceSymbol (the handler that serves it)` : `extraction produces ${type} only from HTTP routes, and this one is a re-export from ${locations.map((l) => l.file).join(", ")} with no method and path` : `no ${this.analyzer} rule producing ${type} applies to ${locations.map((l) => l.file).join(", ")}`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { observable: !0 };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether extraction writes this field of an element of this type: the name and type, the
|
|
110
|
+
* attributes the node mapping fills, and the code facts it reads (an operation's method and
|
|
111
|
+
* path). Never the description, which comes from a docstring at most, or other attributes.
|
|
112
|
+
*/
|
|
113
|
+
field(element, field) {
|
|
114
|
+
return field === "name" || field === "type" ? !0 : field.startsWith("attributes.") ? this.filled.get(`${element.layer}.${element.type}`)?.has(field.slice(11)) ?? !1 : !1;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether some mapped code edge between these two elements becomes this relationship: by their
|
|
118
|
+
* types, as extraction decides it, and by what the edge links (see EDGE_LABELS, EDGE_ENDPOINTS).
|
|
119
|
+
*/
|
|
120
|
+
relationship(source, predicate, target) {
|
|
121
|
+
const sourceType = `${source.layer}.${source.type}`, targetType = `${target.layer}.${target.type}`, key = `${sourceType}|${predicate}|${targetType}`;
|
|
122
|
+
let edges = this.tripleCache.get(key);
|
|
123
|
+
if (edges === void 0) {
|
|
124
|
+
edges = [];
|
|
125
|
+
for (const m of this.edgeMappings)
|
|
126
|
+
for (const inverted of [!1, !0]) {
|
|
127
|
+
const [from, to] = inverted ? [targetType, sourceType] : [sourceType, targetType], r = relationshipFor(m, from, to, this.hasTriple);
|
|
128
|
+
r && r.source === sourceType && r.destination === targetType && r.predicate === predicate && edges.push({ edge: m.cbm_edge, inverted });
|
|
129
|
+
}
|
|
130
|
+
this.tripleCache.set(key, edges);
|
|
131
|
+
}
|
|
132
|
+
return edges.length === 0 ? { observable: !1, reason: `the ${this.analyzer} edge mapping turns no code edge into ${sourceType} ${predicate} ${targetType}` } : edges.filter(({ edge, inverted }) => {
|
|
133
|
+
const check = EDGE_ENDPOINTS[edge]?.to;
|
|
134
|
+
return !check || check(inverted ? source : target);
|
|
135
|
+
}).length === 0 ? {
|
|
136
|
+
observable: !1,
|
|
137
|
+
reason: edges.map(({ edge }) => `${edge} ${EDGE_ENDPOINTS[edge]?.why ?? "does not apply"}`).join("; ")
|
|
138
|
+
} : { observable: !0 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isReExport(element, locations) {
|
|
142
|
+
const attrs = element.attributes ?? {};
|
|
143
|
+
return attrs.http_method || attrs.http_path || attrs.method || attrs.path || locations.length === 0 ? !1 : locations.every((l) => BARREL_FILE.test(l.file) && !(l.symbol && ROUTE_SYMBOL.test(l.symbol)));
|
|
144
|
+
}
|
|
145
|
+
function isUnlocatedOperation(element, locations) {
|
|
146
|
+
const attrs = element.attributes ?? {};
|
|
147
|
+
return attrs.http_method || attrs.http_path || attrs.method || attrs.path ? !1 : locations.length === 0;
|
|
148
|
+
}
|
|
149
|
+
async function loadExtractionCoverage(analyzer) {
|
|
150
|
+
try {
|
|
151
|
+
const mapper = await MappingLoader.load(analyzer), nodeMappings = mapper.getNodeLabels().map((label) => mapper.getNodeMapping(label)).filter((m) => !!m), edgeMappings = mapper.getEdgeTypes().map((type) => mapper.getEdgeMapping(type)).filter((m) => !!m);
|
|
152
|
+
return new ExtractionCoverage({ analyzer, nodeMappings, edgeMappings, rules: tier0RulesFrom(mapper.getHeuristics()) });
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
ExtractionCoverage,
|
|
159
|
+
loadExtractionCoverage
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=extraction-observability.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/extraction-observability.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Extraction observability \u2014 which planned elements, relationships and fields code extraction can\n * see.\n *\n * `changeset_verify` compares a plan with an as-built changeset. When the as-built side came from\n * `codebase_extract`, a planned element extraction can never produce (a requirement, a re-export,\n * a TypeScript test case) is not \"missing\": it is unverifiable, and stays out of the verdict.\n *\n * What extraction can produce is read from the analyzer's own mapping (spec/analyzers/<name>/):\n *\n * - the node mapping: each CBM label becomes one element type (and a promoted type), and fills some\n * of its attributes;\n * - the Tier-0 rules: each decides or narrows to element types, some only for files matching a\n * pattern;\n * - the edge mapping: each mapped CBM edge becomes a relationship between two element types,\n * through the relationship catalog and the edge's from_types and to_types, the way\n * `relationshipFor` decides it during extraction.\n *\n * Facts the mapping files don't state are kept here, each with its reason:\n *\n * - a Route is an HTTP route: an operation sourced to a barrel file (a package entry that\n * re-exports) with no method and path is not one, and an operation with no method, no path and\n * no source reference gives extraction nothing to find it by;\n * - some CBM edges link labels no node mapping or Tier-0 rule produces (DEPLOYS links\n * infrastructure resources), so they make no relationship at all;\n * - DEFINES_METHOD links a class to its methods, not a module to its functions;\n * - the card builder makes no cards for JavaScript and TypeScript test cases (anonymous `it()`\n * callbacks), so the testing layer is not observable in those files (docs/EXTRACTION_STRATEGY.md,\n * \"Known gap\");\n * - extraction writes an operation's method and path (`http_method`, `http_path`) besides the\n * node mapping's fields.\n *\n * Intent (motivation, business, product) is never read from code.\n */\n\nimport { MappingLoader } from \"../analyzers/mapping-loader.js\";\nimport { relationshipFor } from \"../analyzers/edge-relationships.js\";\nimport { tier0RulesFrom, type Tier0Rule } from \"../analyzers/tier0-rules.js\";\nimport type { AnalyzerEdgeMapping, AnalyzerNodeMapping } from \"../analyzers/types.js\";\nimport { getValidRelationships } from \"../generated/relationship-index.js\";\nimport type { SourceReference } from \"../types/source-reference.js\";\n\n/** Layers that state intent; no code says why the system exists. */\nconst NEVER_OBSERVABLE_LAYERS = new Set([\"motivation\", \"business\", \"product\"]);\n\n/** Files the card builder makes no test cards for: JS/TS test cases are anonymous callbacks. */\nconst NO_TEST_CARDS = /\\.[cm]?[jt]sx?$/;\n\n/** A Route's symbol: `GET /owners/{id}` or CBM's `__route__GET__/owners/{id}`. */\nconst ROUTE_SYMBOL = /^(?:__route__[A-Z]+__\\/|[A-Z]+ \\/)/;\n\n/** Barrel files: a package entry that re-exports what other files define. */\nconst BARREL_FILE = /(^|\\/)(index\\.[cm]?[jt]sx?|__init__\\.py|mod\\.rs)$/;\n\n/** Attributes extraction writes from the code beyond the node mapping (see deriveAttributes). */\nconst CODE_FACTS: Record<string, string[]> = {\n \"api.operation\": [\"http_method\", \"http_path\"],\n};\n\n/**\n * CBM edges whose ends are particular labels. An edge one of whose ends must be a label no node\n * mapping or Tier-0 rule produces makes no relationship: its ends never become elements.\n */\nconst EDGE_LABELS: Record<string, { oneEndOf: string[]; why: string }> = {\n DEPLOYS: { oneEndOf: [\"Resource\", \"Image\"], why: \"links infrastructure resources and images\" },\n INFRA_MAPS: { oneEndOf: [\"Resource\"], why: \"links infrastructure resources\" },\n DEPENDS_ON: { oneEndOf: [\"Package\", \"Image\", \"Resource\"], why: \"links packages, images and resources\" },\n SPAWNS: { oneEndOf: [\"Process\"], why: \"links code to the processes it launches\" },\n};\n\n/**\n * CBM edges that link only some symbols of their element types, by which end (in CBM's direction)\n * must be what. DEFINES_METHOD runs from a class to its methods: a module-level function, whose\n * symbol names no owner (`computeNodeDegrees`, not `OwnerController.initCreationForm`), has none.\n */\nconst EDGE_ENDPOINTS: Record<string, { to?: (element: ObservedElement) => boolean; why: string }> = {\n DEFINES_METHOD: {\n to: (element) => {\n const symbols = sourceLocations(element)\n .map((l) => l.symbol)\n .filter((s): s is string => !!s);\n return symbols.length === 0 || symbols.some((s) => s.includes(\".\"));\n },\n why: \"links a class to its methods only, and this function is not a method\",\n },\n};\n\n/** What an element looks like to the observability check. */\nexport interface ObservedElement {\n layer: string;\n type: string;\n attributes?: Record<string, unknown>;\n sourceReference?: SourceReference;\n}\n\nexport interface ObservabilityVerdict {\n observable: boolean;\n /** Why not, when it is not. */\n reason?: string;\n /**\n * Not observable only for want of a method, path or source reference to find it by (an operation\n * planned with no source fields): the build may still have it under another name.\n */\n unlocated?: true;\n}\n\n/** One way extraction produces an element type. */\ninterface Producer {\n /** `node mapping <Label>` or `Tier-0 rule <name>`. */\n via: string;\n /** For a node mapping: the CBM label. */\n label?: string;\n /** For a Tier-0 rule with a file condition: the files it applies to. */\n file?: RegExp;\n}\n\nexport interface ExtractionCoverageInput {\n analyzer: string;\n nodeMappings: AnalyzerNodeMapping[];\n edgeMappings: AnalyzerEdgeMapping[];\n rules: Tier0Rule[];\n /** Catalog check by triple id (`layer.type.predicate.layer.type`); defaults to the generated index. */\n hasTriple?: (id: string) => boolean;\n}\n\nfunction catalogHasTriple(id: string): boolean {\n const parts = id.split(\".\");\n if (parts.length !== 5) return false;\n return getValidRelationships(`${parts[0]}.${parts[1]}`, parts[2], `${parts[3]}.${parts[4]}`).length > 0;\n}\n\nfunction sourceLocations(element: ObservedElement): Array<{ file: string; symbol?: string }> {\n return (element.sourceReference?.locations ?? [])\n .filter((l) => typeof l?.file === \"string\" && l.file.length > 0)\n .map((l) => ({ file: l.file.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\"), ...(l.symbol ? { symbol: l.symbol } : {}) }));\n}\n\n/** What an analyzer's extraction can produce, by element type, relationship triple and field. */\nexport class ExtractionCoverage {\n readonly analyzer: string;\n private readonly producers = new Map<string, Producer[]>();\n /** Layers a Tier-0 rule narrows to with `layer.*`. */\n private readonly wildcardLayers = new Map<string, Producer[]>();\n /** Attributes the node mapping fills, by element type. */\n private readonly filled = new Map<string, Set<string>>();\n private readonly edgeMappings: AnalyzerEdgeMapping[];\n private readonly hasTriple: (id: string) => boolean;\n private readonly tripleCache = new Map<string, Array<{ edge: string; inverted: boolean }>>();\n\n constructor(input: ExtractionCoverageInput) {\n this.analyzer = input.analyzer;\n this.hasTriple = input.hasTriple ?? catalogHasTriple;\n const add = (type: string, producer: Producer) => {\n const list = this.producers.get(type) ?? [];\n list.push(producer);\n this.producers.set(type, list);\n };\n const fill = (type: string, attributes: string[]) => {\n const set = this.filled.get(type) ?? new Set<string>();\n for (const a of attributes) set.add(a);\n this.filled.set(type, set);\n };\n // Labels whose symbols can become elements, by the node mapping or a Tier-0 rule.\n const labels = new Set<string>();\n for (const m of input.nodeMappings) {\n if (!m.dr_layer || !m.dr_element_type) continue;\n labels.add(m.cbm_label);\n const producer = { via: `node mapping ${m.cbm_label}`, label: m.cbm_label };\n const mapped = m as AnalyzerNodeMapping & {\n dr_element_type_promoted?: string;\n attribute_mappings?: Array<{ dr_attribute?: string }>;\n };\n const attributes = [\n ...(mapped.attribute_mappings ?? []).map((a) => a.dr_attribute).filter((a): a is string => !!a),\n ...Object.keys(m.dr_element_fields ?? {}),\n ];\n for (const type of [m.dr_element_type, mapped.dr_element_type_promoted].filter((t): t is string => !!t)) {\n add(`${m.dr_layer}.${type}`, producer);\n fill(`${m.dr_layer}.${type}`, attributes);\n }\n }\n for (const rule of input.rules) {\n if (rule.action.kind === \"reject\") continue;\n for (const label of rule.labels) labels.add(label);\n const producer: Producer = { via: `Tier-0 rule ${rule.name}`, ...(rule.when.file ? { file: rule.when.file } : {}) };\n if (rule.action.kind === \"decide\") add(`${rule.action.layer}.${rule.action.type}`, producer);\n else {\n for (const c of rule.action.candidates) {\n if (c.type) add(`${c.layer}.${c.type}`, producer);\n else this.wildcardLayers.set(c.layer, [...(this.wildcardLayers.get(c.layer) ?? []), producer]);\n }\n }\n }\n for (const [type, facts] of Object.entries(CODE_FACTS)) fill(type, facts);\n this.edgeMappings = input.edgeMappings.filter(\n (m) => m.usage === \"verification_and_mapping\" && (!EDGE_LABELS[m.cbm_edge] || EDGE_LABELS[m.cbm_edge].oneEndOf.some((l) => labels.has(l)))\n );\n }\n\n /** Element types extraction can produce at all (`layer.type`), sorted. */\n observableTypes(): string[] {\n return [...this.producers.keys()].sort();\n }\n\n /**\n * Whether extraction could produce this element. `codeFiles` are the files the build was\n * extracted from; they stand in for the element's own source file when it names none.\n */\n element(element: ObservedElement, codeFiles: string[] = []): ObservabilityVerdict {\n const type = `${element.layer}.${element.type}`;\n if (NEVER_OBSERVABLE_LAYERS.has(element.layer)) {\n return { observable: false, reason: `extraction does not read intent (${element.layer}) from code` };\n }\n const producers = [...(this.producers.get(type) ?? []), ...(this.wildcardLayers.get(element.layer) ?? [])];\n if (producers.length === 0) {\n return { observable: false, reason: `the ${this.analyzer} mapping produces no ${type} elements` };\n }\n\n const locations = sourceLocations(element);\n const files = locations.length > 0 ? locations.map((l) => l.file) : codeFiles;\n if (element.layer === \"testing\" && files.length > 0 && files.every((f) => NO_TEST_CARDS.test(f))) {\n return {\n observable: false,\n reason: \"JavaScript and TypeScript test cases are anonymous callbacks and never become extraction cards\",\n };\n }\n\n const unlocated = isUnlocatedOperation(element, locations);\n const possible = producers.filter((p) => {\n if (p.file && locations.length > 0 && !locations.some((l) => p.file!.test(l.file))) return false;\n if (p.label === \"Route\" && (unlocated || isReExport(element, locations))) return false;\n return true;\n });\n if (possible.length === 0) {\n const routeOnly = producers.every((p) => p.label === \"Route\");\n return {\n observable: false,\n ...(routeOnly && unlocated ? { unlocated: true as const } : {}),\n reason: !routeOnly\n ? `no ${this.analyzer} rule producing ${type} applies to ${locations.map((l) => l.file).join(\", \")}`\n : unlocated\n ? `extraction produces ${type} only from HTTP routes, and this one has no method, path or source reference to find ` +\n \"it by. To make it verifiable, give it http_method and http_path (for a route), or sourceFile and sourceSymbol \" +\n \"(the handler that serves it)\"\n : `extraction produces ${type} only from HTTP routes, and this one is a re-export from ${locations.map((l) => l.file).join(\", \")} with no method and path`,\n };\n }\n return { observable: true };\n }\n\n /**\n * Whether extraction writes this field of an element of this type: the name and type, the\n * attributes the node mapping fills, and the code facts it reads (an operation's method and\n * path). Never the description, which comes from a docstring at most, or other attributes.\n */\n field(element: ObservedElement, field: string): boolean {\n if (field === \"name\" || field === \"type\") return true;\n if (!field.startsWith(\"attributes.\")) return false;\n return this.filled.get(`${element.layer}.${element.type}`)?.has(field.slice(\"attributes.\".length)) ?? false;\n }\n\n /**\n * Whether some mapped code edge between these two elements becomes this relationship: by their\n * types, as extraction decides it, and by what the edge links (see EDGE_LABELS, EDGE_ENDPOINTS).\n */\n relationship(source: ObservedElement, predicate: string, target: ObservedElement): ObservabilityVerdict {\n const sourceType = `${source.layer}.${source.type}`;\n const targetType = `${target.layer}.${target.type}`;\n const key = `${sourceType}|${predicate}|${targetType}`;\n let edges = this.tripleCache.get(key);\n if (edges === undefined) {\n // Each producing edge, with the CBM edge's own ends: `inverted` when it runs target \u2192 source.\n edges = [];\n for (const m of this.edgeMappings) {\n for (const inverted of [false, true]) {\n const [from, to] = inverted ? [targetType, sourceType] : [sourceType, targetType];\n const r = relationshipFor(m, from, to, this.hasTriple);\n if (r && r.source === sourceType && r.destination === targetType && r.predicate === predicate) {\n edges.push({ edge: m.cbm_edge, inverted });\n }\n }\n }\n this.tripleCache.set(key, edges);\n }\n if (edges.length === 0) {\n return { observable: false, reason: `the ${this.analyzer} edge mapping turns no code edge into ${sourceType} ${predicate} ${targetType}` };\n }\n const possible = edges.filter(({ edge, inverted }) => {\n const check = EDGE_ENDPOINTS[edge]?.to;\n return !check || check(inverted ? source : target);\n });\n if (possible.length === 0) {\n return {\n observable: false,\n reason: edges.map(({ edge }) => `${edge} ${EDGE_ENDPOINTS[edge]?.why ?? \"does not apply\"}`).join(\"; \"),\n };\n }\n return { observable: true };\n }\n}\n\n/**\n * A re-export, not a route: no method and path, sourced to a barrel file (a package entry), under a\n * name that isn't route-shaped. Without that evidence an operation may be a route (a handler symbol\n * such as `OwnerController.showVisits` names the code that serves it).\n */\nfunction isReExport(element: ObservedElement, locations: Array<{ file: string; symbol?: string }>): boolean {\n const attrs = element.attributes ?? {};\n if (attrs.http_method || attrs.http_path || attrs.method || attrs.path) return false;\n if (locations.length === 0) return false;\n return locations.every((l) => BARREL_FILE.test(l.file) && !(l.symbol && ROUTE_SYMBOL.test(l.symbol)));\n}\n\n/**\n * An operation planned with nothing extraction could find it by: no method or path, and no source\n * reference (so no handler or route symbol either). Extraction finds operations only as routes, by\n * method and path or by the handler's file and symbol, so it can't show such a one either way.\n */\nfunction isUnlocatedOperation(element: ObservedElement, locations: Array<{ file: string; symbol?: string }>): boolean {\n const attrs = element.attributes ?? {};\n if (attrs.http_method || attrs.http_path || attrs.method || attrs.path) return false;\n return locations.length === 0;\n}\n\n/** The coverage of an analyzer's bundled mapping, or null when it can't be read. */\nexport async function loadExtractionCoverage(analyzer: string): Promise<ExtractionCoverage | null> {\n try {\n const mapper = await MappingLoader.load(analyzer);\n const nodeMappings = mapper\n .getNodeLabels()\n .map((label) => mapper.getNodeMapping(label))\n .filter((m): m is AnalyzerNodeMapping => !!m);\n const edgeMappings = mapper\n .getEdgeTypes()\n .map((type) => mapper.getEdgeMapping(type))\n .filter((m): m is AnalyzerEdgeMapping => !!m);\n return new ExtractionCoverage({ analyzer, nodeMappings, edgeMappings, rules: tier0RulesFrom(mapper.getHeuristics()) });\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -139,6 +139,7 @@ class MigrationRegistry {
|
|
|
139
139
|
fromVersion: "0.9.0",
|
|
140
140
|
toVersion: "0.10.0",
|
|
141
141
|
description: "Data Model objectschema.required type change, element type renames and relationship catalog rewrites (Spec v0.10.0)",
|
|
142
|
+
rewritesCatalog: !0,
|
|
142
143
|
apply: async (model) => {
|
|
143
144
|
const required = await migrateObjectSchemaRequired(model), catalog = await applyCatalogMigrations(model), errors = [...required.errors, ...catalog.errors];
|
|
144
145
|
return {
|
|
@@ -153,6 +154,7 @@ class MigrationRegistry {
|
|
|
153
154
|
fromVersion: "0.10.0",
|
|
154
155
|
toVersion: "0.11.0",
|
|
155
156
|
description: "Relationship catalog rewrites from the layer rules pass (Spec v0.11.0)",
|
|
157
|
+
rewritesCatalog: !0,
|
|
156
158
|
apply: async (model) => {
|
|
157
159
|
const catalog = await applyCatalogMigrations(model);
|
|
158
160
|
return {
|
|
@@ -163,6 +165,15 @@ class MigrationRegistry {
|
|
|
163
165
|
details: catalog.details
|
|
164
166
|
};
|
|
165
167
|
}
|
|
168
|
+
}), this.migrations.push({
|
|
169
|
+
fromVersion: "0.11.0",
|
|
170
|
+
toVersion: "0.12.0",
|
|
171
|
+
description: "Twelve test links in the testing layer (Spec v0.12.0)",
|
|
172
|
+
apply: async () => ({
|
|
173
|
+
migrationsApplied: 1,
|
|
174
|
+
filesModified: 0,
|
|
175
|
+
description: "Spec version updated to 0.12.0 (twelve test links in the testing layer)"
|
|
176
|
+
})
|
|
166
177
|
});
|
|
167
178
|
}
|
|
168
179
|
/**
|
|
@@ -246,7 +257,8 @@ class MigrationRegistry {
|
|
|
246
257
|
migrations: path.map((m) => ({
|
|
247
258
|
from: m.fromVersion,
|
|
248
259
|
to: m.toVersion,
|
|
249
|
-
description: m.description
|
|
260
|
+
description: m.description,
|
|
261
|
+
...m.rewritesCatalog ? { rewritesCatalog: !0 } : {}
|
|
250
262
|
}))
|
|
251
263
|
};
|
|
252
264
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/migration-registry.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Migration Registry - Manages version-based model migrations\n *\n * Handles sequential migrations from one spec version to another,\n * supporting the evolution of the architecture model specification.\n */\n\nimport type { Model } from \"./model.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { applyCatalogMigrations } from \"./catalog-migration.js\";\n\n/**\n * Represents a single version migration\n */\nexport interface Migration {\n fromVersion: string;\n toVersion: string;\n description: string;\n apply: (model: Model) => Promise<MigrationResult>;\n}\n\n/**\n * Result of applying a migration\n */\nexport interface MigrationResult {\n migrationsApplied: number;\n filesModified: number;\n description: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\n/**\n * Migration Registry managing all available migrations\n */\nexport class MigrationRegistry {\n private migrations: Migration[] = [];\n\n constructor() {\n this.registerMigrations();\n }\n\n /**\n * Register all available migrations\n */\n private registerMigrations(): void {\n // Migration from v0.5.0 to v0.6.0: Relationship Taxonomy\n this.migrations.push({\n fromVersion: \"0.5.0\",\n toVersion: \"0.6.0\",\n description: \"Relationship Taxonomy (Spec v0.6.0)\",\n apply: async () => {\n // This migration is opt-in - the new relationship taxonomy\n // is fully backward compatible with existing models\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.6.0 (Relationship Taxonomy now available)\",\n };\n },\n });\n\n // Migration from v0.6.0 to v0.7.0: Layer Schema Relationship Metadata\n this.migrations.push({\n fromVersion: \"0.6.0\",\n toVersion: \"0.7.0\",\n description: \"Layer Schema Relationship Metadata (Spec v0.7.0)\",\n apply: async () => {\n // This migration updates layer schemas with relationship metadata sections\n // - All 12 layer schemas now include layerMetadata, intraLayerRelationships, crossLayerRelationships\n // - Deprecated link-registry.json (removed in v0.8.0)\n // - Terminology change: \"cross-layer links\" \u2192 \"cross-layer relationships\"\n // Fully backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.7.0 (Layer schemas now include relationship metadata sections)\",\n };\n },\n });\n\n // Migration from v0.7.0 to v0.7.1: Source Code Reference Infrastructure\n this.migrations.push({\n fromVersion: \"0.7.0\",\n toVersion: \"0.7.1\",\n description: \"Source Code Reference Infrastructure (Spec v0.7.1)\",\n apply: async () => {\n // This migration adds source code reference infrastructure\n // - Common schema definitions in spec/schemas/common/\n // - Source references integrated across 10 layers\n // - Backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.7.1 (Source Code Reference Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.7.1 to v0.8.0: JSON Schema-Only Spec Architecture\n this.migrations.push({\n fromVersion: \"0.7.1\",\n toVersion: \"0.8.0\",\n description: \"JSON Schema-Only Spec Architecture (Spec v0.8.0)\",\n apply: async () => {\n // Spec architecture refactored to JSON Schema-only model:\n // - Node type definitions moved to spec/schemas/nodes/{layer}/*.node.schema.json\n // - Relationship definitions moved to spec/schemas/relationships/{layer}/*.relationship.schema.json\n // - Predicates relocated to spec/schemas/base/predicates.json\n // This is a spec architecture change only - model data files are fully backward compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.0 (JSON Schema-Only Spec Architecture)\",\n };\n },\n });\n\n // Migration from v0.8.0 to v0.8.1: Expanded Relationship Coverage and Node Improvements\n this.migrations.push({\n fromVersion: \"0.8.0\",\n toVersion: \"0.8.1\",\n description: \"Expanded Relationship Coverage and Node Improvements (Spec v0.8.1)\",\n apply: async () => {\n // Spec enhancements only - no model data format changes:\n // - 733 new relationship schemas added across all 12 layers\n // - Node type descriptions improved for all 12 layers\n // - Enum-masquerader node types consolidated into parent attributes\n // - Existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.1 (Expanded Relationship Coverage and Node Improvements)\",\n };\n },\n });\n\n // Migration from v0.8.1 to v0.8.2: UUID/Path identifier separation\n this.migrations.push({\n fromVersion: \"0.8.1\",\n toVersion: \"0.8.2\",\n description: \"UUID/Path identifier separation (Spec v0.8.2)\",\n apply: async () => {\n // The path field is now required on all spec nodes (spec-node.schema.json).\n // Existing elements are migrated automatically on load in model.ts (loadLayer):\n // - Elements with slug-format id: slug moved to path, deterministic UUID assigned to id\n // - Elements with UUID id but no path: path derived from layer_id.type.kebab(name)\n // No manual data file changes needed; migration runs transparently on next load/save.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.2 (UUID/Path identifier separation)\",\n };\n },\n });\n\n // Migration from v0.8.2 to v0.8.3: Inter-layer Relationship Schemas and APM Node Types\n this.migrations.push({\n fromVersion: \"0.8.2\",\n toVersion: \"0.8.3\",\n description: \"Inter-layer Relationship Schemas and APM Node Types (Spec v0.8.3)\",\n apply: async () => {\n // Major expansion of inter-layer relationship coverage and APM layer:\n // - 495 new inter-layer relationship schemas across all 12 layers\n // - Total relationship count increased from 969 to 1,447\n // - 2 new APM node types: apm.alert and apm.dashboard\n // - Backward compatible - existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.3 (Inter-layer Relationship Schemas and APM Node Types)\",\n };\n },\n });\n\n // Migration from v0.8.3 to v0.8.4: Analyzer Infrastructure (CBM Mapping)\n this.migrations.push({\n fromVersion: \"0.8.3\",\n toVersion: \"0.8.4\",\n description: \"Analyzer Infrastructure (Spec v0.8.4)\",\n apply: async () => {\n // Introduces spec/analyzers/ with codebase-memory (CBM) mapping:\n // - New analyzer adapter foundation and full analyzer subcommands\n // - dr analyzer verify with changeset awareness\n // - Claude Code integration commands (/dr-verify, /dr-map, dr-extractor)\n // - No model data format changes; existing models are fully compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.4 (Analyzer Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.8.4 to v0.9.0: Product Layer Introduction\n this.migrations.push({\n fromVersion: \"0.8.4\",\n toVersion: \"0.9.0\",\n description: \"Product Layer Introduction (Spec v0.9.0)\",\n apply: async (model: Model) => {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n\n const modelDir = `${model.rootPath}/documentation-robotics/model`;\n let filesModified = 0;\n\n try {\n // Verify model directory exists\n await fs.access(modelDir);\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT means the directory doesn't exist - nothing to migrate\n if (err.code === \"ENOENT\") {\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced; no model directories to migrate)\",\n };\n }\n // Re-throw other errors (e.g., EACCES for permission denied)\n throw error;\n }\n\n // Rename existing layer directories: shift from 03-12 to 04-13\n // Must do this in reverse order (12\u219213, 11\u219212, ..., 03\u219204) to avoid conflicts\n const renameMap = [\n [\"12_testing\", \"13_testing\"],\n [\"11_apm\", \"12_apm\"],\n [\"10_navigation\", \"11_navigation\"],\n [\"09_ux\", \"10_ux\"],\n [\"08_data-store\", \"09_data-store\"],\n [\"07_data-model\", \"08_data-model\"],\n [\"06_api\", \"07_api\"],\n [\"05_technology\", \"06_technology\"],\n [\"04_application\", \"05_application\"],\n [\"03_security\", \"04_security\"],\n ];\n\n const renameErrors: string[] = [];\n\n for (const [oldName, newName] of renameMap) {\n const oldPath = pathModule.join(modelDir, oldName);\n const newPath = pathModule.join(modelDir, newName);\n\n try {\n await fs.rename(oldPath, newPath);\n filesModified++;\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT is expected when a directory hasn't been created yet\n if (err.code === \"ENOENT\") {\n continue;\n }\n // Track non-ENOENT errors to report them in the result\n renameErrors.push(\n `Failed to rename ${oldName} to ${newName}: ${getErrorMessage(error)}`\n );\n }\n }\n\n // If there were any rename failures, include them in the error field\n if (renameErrors.length > 0) {\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n error: renameErrors.join(\"; \"),\n };\n }\n\n // Create new product layer directory (it will be empty for now)\n // recursive: true handles EEXIST, so no catch needed\n const productPath = pathModule.join(modelDir, \"03_product\");\n await fs.mkdir(productPath, { recursive: true });\n\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n };\n },\n });\n\n // Migration from v0.9.0 to v0.10.0: objectschema.required type change and relationship catalog rewrites\n this.migrations.push({\n fromVersion: \"0.9.0\",\n toVersion: \"0.10.0\",\n description:\n \"Data Model objectschema.required type change, element type renames and relationship catalog rewrites (Spec v0.10.0)\",\n apply: async (model: Model) => {\n const required = await migrateObjectSchemaRequired(model);\n\n // Element type renames, then relationship rewrites in relationships.yaml and in staged\n // changesets: shared with `dr upgrade` on a model already at the current version\n const catalog = await applyCatalogMigrations(model);\n const errors = [...required.errors, ...catalog.errors];\n\n return {\n migrationsApplied: 1,\n filesModified: required.filesModified + catalog.filesModified,\n description: `Spec version updated to 0.10.0 (objectschema.required converted from string to array; ${catalog.summaries.join(\"; \")})`,\n ...(errors.length > 0 && { error: errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n\n // Migration from v0.10.0 to v0.11.0: the layer rules pass retired relationship types and\n // predicates; CLI 0.2.0 stamped models 0.10.0 with the catalog from before it\n this.migrations.push({\n fromVersion: \"0.10.0\",\n toVersion: \"0.11.0\",\n description: \"Relationship catalog rewrites from the layer rules pass (Spec v0.11.0)\",\n apply: async (model: Model) => {\n const catalog = await applyCatalogMigrations(model);\n return {\n migrationsApplied: 1,\n filesModified: catalog.filesModified,\n description: `Spec version updated to 0.11.0 (${catalog.summaries.join(\"; \")})`,\n ...(catalog.errors.length > 0 && { error: catalog.errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n }\n\n /**\n * Get the latest available specification version\n */\n getLatestVersion(): string {\n if (this.migrations.length === 0) {\n return \"0.7.0\";\n }\n\n // Return the highest toVersion from all migrations using proper version comparison\n const versions = this.migrations.map((m) => m.toVersion);\n return versions.reduce((max, current) => {\n return this.compareVersions(current, max) > 0 ? current : max;\n });\n }\n\n /**\n * Get the sequence of migrations needed from one version to another\n */\n getMigrationPath(fromVersion: string, toVersion?: string): Migration[] {\n const targetVersion = toVersion || this.getLatestVersion();\n\n if (this.compareVersions(fromVersion, targetVersion) >= 0) {\n return [];\n }\n\n const path: Migration[] = [];\n let current = fromVersion;\n\n // Sort migrations by fromVersion\n const sortedMigrations = [...this.migrations].sort((a, b) =>\n this.compareVersions(a.fromVersion, b.fromVersion)\n );\n\n for (const migration of sortedMigrations) {\n // Check if this migration is in our path\n if (\n this.compareVersions(current, migration.fromVersion) >= 0 &&\n this.compareVersions(current, migration.toVersion) < 0 &&\n this.compareVersions(migration.toVersion, targetVersion) <= 0\n ) {\n path.push(migration);\n current = migration.toVersion;\n }\n }\n\n return path;\n }\n\n /**\n * Check if a model requires migration\n */\n requiresMigration(currentVersion: string): boolean {\n const path = this.getMigrationPath(currentVersion);\n return path.length > 0;\n }\n\n /**\n * Apply all migrations in sequence\n */\n async applyMigrations(\n model: Model,\n options: {\n fromVersion: string;\n toVersion?: string;\n dryRun?: boolean;\n validate?: boolean;\n }\n ): Promise<ApplyMigrationsResult> {\n const path = this.getMigrationPath(options.fromVersion, options.toVersion);\n const targetVersion = options.toVersion || this.getLatestVersion();\n\n if (path.length === 0) {\n return {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n }\n\n const results: ApplyMigrationsResult = {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n\n for (const migration of path) {\n if (!options.dryRun) {\n try {\n const result = await migration.apply(model);\n\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n changes: result,\n });\n\n results.totalChanges += result.migrationsApplied;\n\n // Only update model's spec version if migration succeeded (no error field)\n if (!result.error) {\n model.manifest.specVersion = migration.toVersion;\n } else {\n // If migration has errors, throw to prevent partial success state\n throw new Error(result.error);\n }\n } catch (error) {\n throw new Error(\n `Migration ${migration.fromVersion} \u2192 ${migration.toVersion} failed: ${\n getErrorMessage(error)\n }`\n );\n }\n } else {\n // Dry run - just record what would happen\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n dryRun: true,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Get a summary of migrations that would be applied\n */\n getMigrationSummary(\n fromVersion: string,\n toVersion?: string\n ): {\n currentVersion: string;\n targetVersion: string;\n migrationsNeeded: number;\n migrations: Array<{\n from: string;\n to: string;\n description: string;\n }>;\n } {\n const path = this.getMigrationPath(fromVersion, toVersion);\n const targetVersion = toVersion || this.getLatestVersion();\n\n return {\n currentVersion: fromVersion,\n targetVersion,\n migrationsNeeded: path.length,\n migrations: path.map((m) => ({\n from: m.fromVersion,\n to: m.toVersion,\n description: m.description,\n })),\n };\n }\n\n /**\n * Compare two version strings\n * Returns: -1 if a < b, 0 if a == b, 1 if a > b\n */\n private compareVersions(a: string, b: string): number {\n const aParts = a.split(\".\").map(Number);\n const bParts = b.split(\".\").map(Number);\n\n const maxLength = Math.max(aParts.length, bParts.length);\n\n for (let i = 0; i < maxLength; i++) {\n const aPart = aParts[i] || 0;\n const bPart = bParts[i] || 0;\n\n if (aPart < bPart) return -1;\n if (aPart > bPart) return 1;\n }\n\n return 0;\n }\n}\n\n/**\n * Result of applying multiple migrations\n */\nexport interface ApplyMigrationsResult {\n applied: Array<{\n from: string;\n to: string;\n description: string;\n changes?: MigrationResult;\n dryRun?: boolean;\n }>;\n currentVersion: string;\n targetVersion: string;\n totalChanges: number;\n}\n\n/**\n * Spec v0.10.0: data-model.objectschema `required` changed from a comma-separated string to an\n * array of property names. Converts every string value in 08_data-model/*.yaml, and in the\n * data-model layer when the model has it loaded: `dr upgrade` saves the loaded layers after\n * migrating, which would otherwise write the old strings back over the converted files.\n */\nasync function migrateObjectSchemaRequired(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const result = await migrateObjectSchemaRequiredFiles(model);\n migrateLoadedObjectSchemaRequired(model);\n return result;\n}\n\n/** A comma-separated `required` string as an array of property names; null for any other value */\nfunction requiredAsArray(required: unknown): string[] | null {\n if (!required || typeof required !== \"string\") {\n return null;\n }\n return required\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n}\n\nfunction migrateLoadedObjectSchemaRequired(model: Model): void {\n // The committed layer: getLayer() would merge in an active changeset's staged changes\n const layer = model.layers.get(\"data-model\");\n if (!layer) {\n return;\n }\n for (const element of layer.listElements()) {\n if (element.spec_node_id !== \"data-model.objectschema\") {\n continue;\n }\n const required = requiredAsArray(element.attributes.required);\n if (required) {\n element.attributes = { ...element.attributes, required };\n layer.updateElement(element);\n }\n }\n}\n\nasync function migrateObjectSchemaRequiredFiles(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n const YAML = await import(\"yaml\");\n\n const dataModelDir = pathModule.join(model.rootPath, \"documentation-robotics/model/08_data-model\");\n let filesModified = 0;\n\n try {\n await fs.access(dataModelDir);\n } catch (error) {\n // ENOENT means the directory doesn't exist - nothing to migrate\n if ((error as { code?: string }).code === \"ENOENT\") {\n return { filesModified: 0, errors: [] };\n }\n throw error;\n }\n\n const files = await fs.readdir(dataModelDir);\n const errors: string[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".yaml\") && !file.endsWith(\".yml\")) {\n continue;\n }\n\n const filePath = pathModule.join(dataModelDir, file);\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n let modified = false;\n const doc = YAML.parse(content);\n\n if (!doc || typeof doc !== \"object\") {\n continue;\n }\n\n for (const [, element] of Object.entries(doc)) {\n if (\n !element ||\n typeof element !== \"object\" ||\n (element as Record<string, unknown>).spec_node_id !== \"data-model.objectschema\"\n ) {\n continue;\n }\n\n const attributes = (element as Record<string, unknown>).attributes as\n | Record<string, unknown>\n | undefined;\n\n if (attributes && typeof attributes === \"object\") {\n const required = requiredAsArray(attributes.required);\n if (required) {\n attributes.required = required;\n modified = true;\n }\n }\n }\n\n if (modified) {\n await fs.writeFile(filePath, YAML.stringify(doc), \"utf-8\");\n filesModified++;\n }\n } catch (error) {\n errors.push(`Failed to migrate ${file}: ${getErrorMessage(error)}`);\n }\n }\n\n return { filesModified, errors };\n}\n"],
|
|
5
|
-
"mappings": "AAQA,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["/**\n * Migration Registry - Manages version-based model migrations\n *\n * Handles sequential migrations from one spec version to another,\n * supporting the evolution of the architecture model specification.\n */\n\nimport type { Model } from \"./model.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { applyCatalogMigrations } from \"./catalog-migration.js\";\n\n/**\n * Represents a single version migration\n */\nexport interface Migration {\n fromVersion: string;\n toVersion: string;\n description: string;\n /** True when the step runs the catalog migration (element type renames, relationship rewrites) */\n rewritesCatalog?: boolean;\n apply: (model: Model) => Promise<MigrationResult>;\n}\n\n/**\n * Result of applying a migration\n */\nexport interface MigrationResult {\n migrationsApplied: number;\n filesModified: number;\n description: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\n/**\n * Migration Registry managing all available migrations\n */\nexport class MigrationRegistry {\n private migrations: Migration[] = [];\n\n constructor() {\n this.registerMigrations();\n }\n\n /**\n * Register all available migrations\n */\n private registerMigrations(): void {\n // Migration from v0.5.0 to v0.6.0: Relationship Taxonomy\n this.migrations.push({\n fromVersion: \"0.5.0\",\n toVersion: \"0.6.0\",\n description: \"Relationship Taxonomy (Spec v0.6.0)\",\n apply: async () => {\n // This migration is opt-in - the new relationship taxonomy\n // is fully backward compatible with existing models\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.6.0 (Relationship Taxonomy now available)\",\n };\n },\n });\n\n // Migration from v0.6.0 to v0.7.0: Layer Schema Relationship Metadata\n this.migrations.push({\n fromVersion: \"0.6.0\",\n toVersion: \"0.7.0\",\n description: \"Layer Schema Relationship Metadata (Spec v0.7.0)\",\n apply: async () => {\n // This migration updates layer schemas with relationship metadata sections\n // - All 12 layer schemas now include layerMetadata, intraLayerRelationships, crossLayerRelationships\n // - Deprecated link-registry.json (removed in v0.8.0)\n // - Terminology change: \"cross-layer links\" \u2192 \"cross-layer relationships\"\n // Fully backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.7.0 (Layer schemas now include relationship metadata sections)\",\n };\n },\n });\n\n // Migration from v0.7.0 to v0.7.1: Source Code Reference Infrastructure\n this.migrations.push({\n fromVersion: \"0.7.0\",\n toVersion: \"0.7.1\",\n description: \"Source Code Reference Infrastructure (Spec v0.7.1)\",\n apply: async () => {\n // This migration adds source code reference infrastructure\n // - Common schema definitions in spec/schemas/common/\n // - Source references integrated across 10 layers\n // - Backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.7.1 (Source Code Reference Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.7.1 to v0.8.0: JSON Schema-Only Spec Architecture\n this.migrations.push({\n fromVersion: \"0.7.1\",\n toVersion: \"0.8.0\",\n description: \"JSON Schema-Only Spec Architecture (Spec v0.8.0)\",\n apply: async () => {\n // Spec architecture refactored to JSON Schema-only model:\n // - Node type definitions moved to spec/schemas/nodes/{layer}/*.node.schema.json\n // - Relationship definitions moved to spec/schemas/relationships/{layer}/*.relationship.schema.json\n // - Predicates relocated to spec/schemas/base/predicates.json\n // This is a spec architecture change only - model data files are fully backward compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.0 (JSON Schema-Only Spec Architecture)\",\n };\n },\n });\n\n // Migration from v0.8.0 to v0.8.1: Expanded Relationship Coverage and Node Improvements\n this.migrations.push({\n fromVersion: \"0.8.0\",\n toVersion: \"0.8.1\",\n description: \"Expanded Relationship Coverage and Node Improvements (Spec v0.8.1)\",\n apply: async () => {\n // Spec enhancements only - no model data format changes:\n // - 733 new relationship schemas added across all 12 layers\n // - Node type descriptions improved for all 12 layers\n // - Enum-masquerader node types consolidated into parent attributes\n // - Existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.1 (Expanded Relationship Coverage and Node Improvements)\",\n };\n },\n });\n\n // Migration from v0.8.1 to v0.8.2: UUID/Path identifier separation\n this.migrations.push({\n fromVersion: \"0.8.1\",\n toVersion: \"0.8.2\",\n description: \"UUID/Path identifier separation (Spec v0.8.2)\",\n apply: async () => {\n // The path field is now required on all spec nodes (spec-node.schema.json).\n // Existing elements are migrated automatically on load in model.ts (loadLayer):\n // - Elements with slug-format id: slug moved to path, deterministic UUID assigned to id\n // - Elements with UUID id but no path: path derived from layer_id.type.kebab(name)\n // No manual data file changes needed; migration runs transparently on next load/save.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.2 (UUID/Path identifier separation)\",\n };\n },\n });\n\n // Migration from v0.8.2 to v0.8.3: Inter-layer Relationship Schemas and APM Node Types\n this.migrations.push({\n fromVersion: \"0.8.2\",\n toVersion: \"0.8.3\",\n description: \"Inter-layer Relationship Schemas and APM Node Types (Spec v0.8.3)\",\n apply: async () => {\n // Major expansion of inter-layer relationship coverage and APM layer:\n // - 495 new inter-layer relationship schemas across all 12 layers\n // - Total relationship count increased from 969 to 1,447\n // - 2 new APM node types: apm.alert and apm.dashboard\n // - Backward compatible - existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.3 (Inter-layer Relationship Schemas and APM Node Types)\",\n };\n },\n });\n\n // Migration from v0.8.3 to v0.8.4: Analyzer Infrastructure (CBM Mapping)\n this.migrations.push({\n fromVersion: \"0.8.3\",\n toVersion: \"0.8.4\",\n description: \"Analyzer Infrastructure (Spec v0.8.4)\",\n apply: async () => {\n // Introduces spec/analyzers/ with codebase-memory (CBM) mapping:\n // - New analyzer adapter foundation and full analyzer subcommands\n // - dr analyzer verify with changeset awareness\n // - Claude Code integration commands (/dr-verify, /dr-map, dr-extractor)\n // - No model data format changes; existing models are fully compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.4 (Analyzer Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.8.4 to v0.9.0: Product Layer Introduction\n this.migrations.push({\n fromVersion: \"0.8.4\",\n toVersion: \"0.9.0\",\n description: \"Product Layer Introduction (Spec v0.9.0)\",\n apply: async (model: Model) => {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n\n const modelDir = `${model.rootPath}/documentation-robotics/model`;\n let filesModified = 0;\n\n try {\n // Verify model directory exists\n await fs.access(modelDir);\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT means the directory doesn't exist - nothing to migrate\n if (err.code === \"ENOENT\") {\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced; no model directories to migrate)\",\n };\n }\n // Re-throw other errors (e.g., EACCES for permission denied)\n throw error;\n }\n\n // Rename existing layer directories: shift from 03-12 to 04-13\n // Must do this in reverse order (12\u219213, 11\u219212, ..., 03\u219204) to avoid conflicts\n const renameMap = [\n [\"12_testing\", \"13_testing\"],\n [\"11_apm\", \"12_apm\"],\n [\"10_navigation\", \"11_navigation\"],\n [\"09_ux\", \"10_ux\"],\n [\"08_data-store\", \"09_data-store\"],\n [\"07_data-model\", \"08_data-model\"],\n [\"06_api\", \"07_api\"],\n [\"05_technology\", \"06_technology\"],\n [\"04_application\", \"05_application\"],\n [\"03_security\", \"04_security\"],\n ];\n\n const renameErrors: string[] = [];\n\n for (const [oldName, newName] of renameMap) {\n const oldPath = pathModule.join(modelDir, oldName);\n const newPath = pathModule.join(modelDir, newName);\n\n try {\n await fs.rename(oldPath, newPath);\n filesModified++;\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT is expected when a directory hasn't been created yet\n if (err.code === \"ENOENT\") {\n continue;\n }\n // Track non-ENOENT errors to report them in the result\n renameErrors.push(\n `Failed to rename ${oldName} to ${newName}: ${getErrorMessage(error)}`\n );\n }\n }\n\n // If there were any rename failures, include them in the error field\n if (renameErrors.length > 0) {\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n error: renameErrors.join(\"; \"),\n };\n }\n\n // Create new product layer directory (it will be empty for now)\n // recursive: true handles EEXIST, so no catch needed\n const productPath = pathModule.join(modelDir, \"03_product\");\n await fs.mkdir(productPath, { recursive: true });\n\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n };\n },\n });\n\n // Migration from v0.9.0 to v0.10.0: objectschema.required type change and relationship catalog rewrites\n this.migrations.push({\n fromVersion: \"0.9.0\",\n toVersion: \"0.10.0\",\n description:\n \"Data Model objectschema.required type change, element type renames and relationship catalog rewrites (Spec v0.10.0)\",\n rewritesCatalog: true,\n apply: async (model: Model) => {\n const required = await migrateObjectSchemaRequired(model);\n\n // Element type renames, then relationship rewrites in relationships.yaml and in staged\n // changesets: shared with `dr upgrade` on a model already at the current version\n const catalog = await applyCatalogMigrations(model);\n const errors = [...required.errors, ...catalog.errors];\n\n return {\n migrationsApplied: 1,\n filesModified: required.filesModified + catalog.filesModified,\n description: `Spec version updated to 0.10.0 (objectschema.required converted from string to array; ${catalog.summaries.join(\"; \")})`,\n ...(errors.length > 0 && { error: errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n\n // Migration from v0.10.0 to v0.11.0: the layer rules pass retired relationship types and\n // predicates; CLI 0.2.0 stamped models 0.10.0 with the catalog from before it\n this.migrations.push({\n fromVersion: \"0.10.0\",\n toVersion: \"0.11.0\",\n description: \"Relationship catalog rewrites from the layer rules pass (Spec v0.11.0)\",\n rewritesCatalog: true,\n apply: async (model: Model) => {\n const catalog = await applyCatalogMigrations(model);\n return {\n migrationsApplied: 1,\n filesModified: catalog.filesModified,\n description: `Spec version updated to 0.11.0 (${catalog.summaries.join(\"; \")})`,\n ...(catalog.errors.length > 0 && { error: catalog.errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n\n // Migration from v0.11.0 to v0.12.0: twelve test links added to the testing layer\n this.migrations.push({\n fromVersion: \"0.11.0\",\n toVersion: \"0.12.0\",\n description: \"Twelve test links in the testing layer (Spec v0.12.0)\",\n apply: async () => {\n // Additive only: twelve new relationship types (a test case tests an object schema, access\n // pattern, application component and nine more; a coverage target covers a library\n // component). Nothing is retired or renamed, so existing models need no change.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.12.0 (twelve test links in the testing layer)\",\n };\n },\n });\n }\n\n /**\n * Get the latest available specification version\n */\n getLatestVersion(): string {\n if (this.migrations.length === 0) {\n return \"0.7.0\";\n }\n\n // Return the highest toVersion from all migrations using proper version comparison\n const versions = this.migrations.map((m) => m.toVersion);\n return versions.reduce((max, current) => {\n return this.compareVersions(current, max) > 0 ? current : max;\n });\n }\n\n /**\n * Get the sequence of migrations needed from one version to another\n */\n getMigrationPath(fromVersion: string, toVersion?: string): Migration[] {\n const targetVersion = toVersion || this.getLatestVersion();\n\n if (this.compareVersions(fromVersion, targetVersion) >= 0) {\n return [];\n }\n\n const path: Migration[] = [];\n let current = fromVersion;\n\n // Sort migrations by fromVersion\n const sortedMigrations = [...this.migrations].sort((a, b) =>\n this.compareVersions(a.fromVersion, b.fromVersion)\n );\n\n for (const migration of sortedMigrations) {\n // Check if this migration is in our path\n if (\n this.compareVersions(current, migration.fromVersion) >= 0 &&\n this.compareVersions(current, migration.toVersion) < 0 &&\n this.compareVersions(migration.toVersion, targetVersion) <= 0\n ) {\n path.push(migration);\n current = migration.toVersion;\n }\n }\n\n return path;\n }\n\n /**\n * Check if a model requires migration\n */\n requiresMigration(currentVersion: string): boolean {\n const path = this.getMigrationPath(currentVersion);\n return path.length > 0;\n }\n\n /**\n * Apply all migrations in sequence\n */\n async applyMigrations(\n model: Model,\n options: {\n fromVersion: string;\n toVersion?: string;\n dryRun?: boolean;\n validate?: boolean;\n }\n ): Promise<ApplyMigrationsResult> {\n const path = this.getMigrationPath(options.fromVersion, options.toVersion);\n const targetVersion = options.toVersion || this.getLatestVersion();\n\n if (path.length === 0) {\n return {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n }\n\n const results: ApplyMigrationsResult = {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n\n for (const migration of path) {\n if (!options.dryRun) {\n try {\n const result = await migration.apply(model);\n\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n changes: result,\n });\n\n results.totalChanges += result.migrationsApplied;\n\n // Only update model's spec version if migration succeeded (no error field)\n if (!result.error) {\n model.manifest.specVersion = migration.toVersion;\n } else {\n // If migration has errors, throw to prevent partial success state\n throw new Error(result.error);\n }\n } catch (error) {\n throw new Error(\n `Migration ${migration.fromVersion} \u2192 ${migration.toVersion} failed: ${\n getErrorMessage(error)\n }`\n );\n }\n } else {\n // Dry run - just record what would happen\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n dryRun: true,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Get a summary of migrations that would be applied\n */\n getMigrationSummary(\n fromVersion: string,\n toVersion?: string\n ): {\n currentVersion: string;\n targetVersion: string;\n migrationsNeeded: number;\n migrations: Array<{\n from: string;\n to: string;\n description: string;\n rewritesCatalog?: boolean;\n }>;\n } {\n const path = this.getMigrationPath(fromVersion, toVersion);\n const targetVersion = toVersion || this.getLatestVersion();\n\n return {\n currentVersion: fromVersion,\n targetVersion,\n migrationsNeeded: path.length,\n migrations: path.map((m) => ({\n from: m.fromVersion,\n to: m.toVersion,\n description: m.description,\n ...(m.rewritesCatalog ? { rewritesCatalog: true } : {}),\n })),\n };\n }\n\n /**\n * Compare two version strings\n * Returns: -1 if a < b, 0 if a == b, 1 if a > b\n */\n private compareVersions(a: string, b: string): number {\n const aParts = a.split(\".\").map(Number);\n const bParts = b.split(\".\").map(Number);\n\n const maxLength = Math.max(aParts.length, bParts.length);\n\n for (let i = 0; i < maxLength; i++) {\n const aPart = aParts[i] || 0;\n const bPart = bParts[i] || 0;\n\n if (aPart < bPart) return -1;\n if (aPart > bPart) return 1;\n }\n\n return 0;\n }\n}\n\n/**\n * Result of applying multiple migrations\n */\nexport interface ApplyMigrationsResult {\n applied: Array<{\n from: string;\n to: string;\n description: string;\n changes?: MigrationResult;\n dryRun?: boolean;\n }>;\n currentVersion: string;\n targetVersion: string;\n totalChanges: number;\n}\n\n/**\n * Spec v0.10.0: data-model.objectschema `required` changed from a comma-separated string to an\n * array of property names. Converts every string value in 08_data-model/*.yaml, and in the\n * data-model layer when the model has it loaded: `dr upgrade` saves the loaded layers after\n * migrating, which would otherwise write the old strings back over the converted files.\n */\nasync function migrateObjectSchemaRequired(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const result = await migrateObjectSchemaRequiredFiles(model);\n migrateLoadedObjectSchemaRequired(model);\n return result;\n}\n\n/** A comma-separated `required` string as an array of property names; null for any other value */\nfunction requiredAsArray(required: unknown): string[] | null {\n if (!required || typeof required !== \"string\") {\n return null;\n }\n return required\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n}\n\nfunction migrateLoadedObjectSchemaRequired(model: Model): void {\n // The committed layer: getLayer() would merge in an active changeset's staged changes\n const layer = model.layers.get(\"data-model\");\n if (!layer) {\n return;\n }\n for (const element of layer.listElements()) {\n if (element.spec_node_id !== \"data-model.objectschema\") {\n continue;\n }\n const required = requiredAsArray(element.attributes.required);\n if (required) {\n element.attributes = { ...element.attributes, required };\n layer.updateElement(element);\n }\n }\n}\n\nasync function migrateObjectSchemaRequiredFiles(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n const YAML = await import(\"yaml\");\n\n const dataModelDir = pathModule.join(model.rootPath, \"documentation-robotics/model/08_data-model\");\n let filesModified = 0;\n\n try {\n await fs.access(dataModelDir);\n } catch (error) {\n // ENOENT means the directory doesn't exist - nothing to migrate\n if ((error as { code?: string }).code === \"ENOENT\") {\n return { filesModified: 0, errors: [] };\n }\n throw error;\n }\n\n const files = await fs.readdir(dataModelDir);\n const errors: string[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".yaml\") && !file.endsWith(\".yml\")) {\n continue;\n }\n\n const filePath = pathModule.join(dataModelDir, file);\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n let modified = false;\n const doc = YAML.parse(content);\n\n if (!doc || typeof doc !== \"object\") {\n continue;\n }\n\n for (const [, element] of Object.entries(doc)) {\n if (\n !element ||\n typeof element !== \"object\" ||\n (element as Record<string, unknown>).spec_node_id !== \"data-model.objectschema\"\n ) {\n continue;\n }\n\n const attributes = (element as Record<string, unknown>).attributes as\n | Record<string, unknown>\n | undefined;\n\n if (attributes && typeof attributes === \"object\") {\n const required = requiredAsArray(attributes.required);\n if (required) {\n attributes.required = required;\n modified = true;\n }\n }\n }\n\n if (modified) {\n await fs.writeFile(filePath, YAML.stringify(doc), \"utf-8\");\n filesModified++;\n }\n } catch (error) {\n errors.push(`Failed to migrate ${file}: ${getErrorMessage(error)}`);\n }\n }\n\n return { filesModified, errors };\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AA4BjC,MAAO,kBAAiB;EACpB,aAA0B,CAAA;EAElC,cAAA;AACE,SAAK,mBAAkB;EACzB;;;;EAKQ,qBAAkB;AAExB,SAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAGE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aACE;;KAGP,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAKE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,OAAO,UAAgB;AAC5B,cAAM,KAAK,MAAM,OAAO,aAAa,GAC/B,aAAa,MAAM,OAAO,MAAM,GAEhC,WAAW,GAAG,MAAM,QAAQ;AAClC,YAAI,gBAAgB;AAEpB,YAAI;AAEF,gBAAM,GAAG,OAAO,QAAQ;QAC1B,SAAS,OAAO;AAGd,cAFY,MAEJ,SAAS;AACf,mBAAO;cACL,mBAAmB;cACnB,eAAe;cACf,aACE;;AAIN,gBAAM;QACR;AAIA,cAAM,YAAY;UAChB,CAAC,cAAc,YAAY;UAC3B,CAAC,UAAU,QAAQ;UACnB,CAAC,iBAAiB,eAAe;UACjC,CAAC,SAAS,OAAO;UACjB,CAAC,iBAAiB,eAAe;UACjC,CAAC,iBAAiB,eAAe;UACjC,CAAC,UAAU,QAAQ;UACnB,CAAC,iBAAiB,eAAe;UACjC,CAAC,kBAAkB,gBAAgB;UACnC,CAAC,eAAe,aAAa;WAGzB,eAAyB,CAAA;AAE/B,mBAAW,CAAC,SAAS,OAAO,KAAK,WAAW;AAC1C,gBAAM,UAAU,WAAW,KAAK,UAAU,OAAO,GAC3C,UAAU,WAAW,KAAK,UAAU,OAAO;AAEjD,cAAI;AACF,kBAAM,GAAG,OAAO,SAAS,OAAO,GAChC;UACF,SAAS,OAAO;AAGd,gBAFY,MAEJ,SAAS;AACf;AAGF,yBAAa,KACX,oBAAoB,OAAO,OAAO,OAAO,KAAK,gBAAgB,KAAK,CAAC,EAAE;UAE1E;QACF;AAGA,YAAI,aAAa,SAAS;AACxB,iBAAO;YACL,mBAAmB;YACnB;YACA,aACE;YACF,OAAO,aAAa,KAAK,IAAI;;AAMjC,cAAM,cAAc,WAAW,KAAK,UAAU,YAAY;AAC1D,qBAAM,GAAG,MAAM,aAAa,EAAE,WAAW,GAAI,CAAE,GAExC;UACL,mBAAmB;UACnB;UACA,aACE;;MAEN;KACD,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aACE;MACF,iBAAiB;MACjB,OAAO,OAAO,UAAgB;AAC5B,cAAM,WAAW,MAAM,4BAA4B,KAAK,GAIlD,UAAU,MAAM,uBAAuB,KAAK,GAC5C,SAAS,CAAC,GAAG,SAAS,QAAQ,GAAG,QAAQ,MAAM;AAErD,eAAO;UACL,mBAAmB;UACnB,eAAe,SAAS,gBAAgB,QAAQ;UAChD,aAAa,yFAAyF,QAAQ,UAAU,KAAK,IAAI,CAAC;UAClI,GAAI,OAAO,SAAS,KAAK,EAAE,OAAO,OAAO,KAAK,IAAI,EAAC;UACnD,SAAS,QAAQ;;MAErB;KACD,GAID,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,iBAAiB;MACjB,OAAO,OAAO,UAAgB;AAC5B,cAAM,UAAU,MAAM,uBAAuB,KAAK;AAClD,eAAO;UACL,mBAAmB;UACnB,eAAe,QAAQ;UACvB,aAAa,mCAAmC,QAAQ,UAAU,KAAK,IAAI,CAAC;UAC5E,GAAI,QAAQ,OAAO,SAAS,KAAK,EAAE,OAAO,QAAQ,OAAO,KAAK,IAAI,EAAC;UACnE,SAAS,QAAQ;;MAErB;KACD,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAIE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB;EACH;;;;EAKA,mBAAgB;AACd,WAAI,KAAK,WAAW,WAAW,IACtB,UAIQ,KAAK,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS,EACvC,OAAO,CAAC,KAAK,YACpB,KAAK,gBAAgB,SAAS,GAAG,IAAI,IAAI,UAAU,GAC3D;EACH;;;;EAKA,iBAAiB,aAAqB,WAAkB;AACtD,UAAM,gBAAgB,aAAa,KAAK,iBAAgB;AAExD,QAAI,KAAK,gBAAgB,aAAa,aAAa,KAAK;AACtD,aAAO,CAAA;AAGT,UAAM,OAAoB,CAAA;AAC1B,QAAI,UAAU;AAGd,UAAM,mBAAmB,CAAC,GAAG,KAAK,UAAU,EAAE,KAAK,CAAC,GAAG,MACrD,KAAK,gBAAgB,EAAE,aAAa,EAAE,WAAW,CAAC;AAGpD,eAAW,aAAa;AAEtB,MACE,KAAK,gBAAgB,SAAS,UAAU,WAAW,KAAK,KACxD,KAAK,gBAAgB,SAAS,UAAU,SAAS,IAAI,KACrD,KAAK,gBAAgB,UAAU,WAAW,aAAa,KAAK,MAE5D,KAAK,KAAK,SAAS,GACnB,UAAU,UAAU;AAIxB,WAAO;EACT;;;;EAKA,kBAAkB,gBAAsB;AAEtC,WADa,KAAK,iBAAiB,cAAc,EACrC,SAAS;EACvB;;;;EAKA,MAAM,gBACJ,OACA,SAKC;AAED,UAAM,OAAO,KAAK,iBAAiB,QAAQ,aAAa,QAAQ,SAAS,GACnE,gBAAgB,QAAQ,aAAa,KAAK,iBAAgB;AAEhE,QAAI,KAAK,WAAW;AAClB,aAAO;QACL,SAAS,CAAA;QACT,gBAAgB,QAAQ;QACxB;QACA,cAAc;;AAIlB,UAAM,UAAiC;MACrC,SAAS,CAAA;MACT,gBAAgB,QAAQ;MACxB;MACA,cAAc;;AAGhB,eAAW,aAAa;AACtB,UAAK,QAAQ;AA6BX,gBAAQ,QAAQ,KAAK;UACnB,MAAM,UAAU;UAChB,IAAI,UAAU;UACd,aAAa,UAAU;UACvB,QAAQ;SACT;;AAjCD,YAAI;AACF,gBAAM,SAAS,MAAM,UAAU,MAAM,KAAK;AAY1C,cAVA,QAAQ,QAAQ,KAAK;YACnB,MAAM,UAAU;YAChB,IAAI,UAAU;YACd,aAAa,UAAU;YACvB,SAAS;WACV,GAED,QAAQ,gBAAgB,OAAO,mBAG3B,CAAC,OAAO;AACV,kBAAM,SAAS,cAAc,UAAU;;AAGvC,kBAAM,IAAI,MAAM,OAAO,KAAK;QAEhC,SAAS,OAAO;AACd,gBAAM,IAAI,MACR,aAAa,UAAU,WAAW,WAAM,UAAU,SAAS,YACzD,gBAAgB,KAAK,CACvB,EAAE;QAEN;AAYJ,WAAO;EACT;;;;EAKA,oBACE,aACA,WAAkB;AAYlB,UAAM,OAAO,KAAK,iBAAiB,aAAa,SAAS,GACnD,gBAAgB,aAAa,KAAK,iBAAgB;AAExD,WAAO;MACL,gBAAgB;MAChB;MACA,kBAAkB,KAAK;MACvB,YAAY,KAAK,IAAI,CAAC,OAAO;QAC3B,MAAM,EAAE;QACR,IAAI,EAAE;QACN,aAAa,EAAE;QACf,GAAI,EAAE,kBAAkB,EAAE,iBAAiB,GAAI,IAAK,CAAA;QACpD;;EAEN;;;;;EAMQ,gBAAgB,GAAW,GAAS;AAC1C,UAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,GAChC,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,GAEhC,YAAY,KAAK,IAAI,OAAO,QAAQ,OAAO,MAAM;AAEvD,aAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,YAAM,QAAQ,OAAO,CAAC,KAAK,GACrB,QAAQ,OAAO,CAAC,KAAK;AAE3B,UAAI,QAAQ;AAAO,eAAO;AAC1B,UAAI,QAAQ;AAAO,eAAO;IAC5B;AAEA,WAAO;EACT;;AAyBF,eAAe,4BACb,OAAY;AAEZ,QAAM,SAAS,MAAM,iCAAiC,KAAK;AAC3D,2CAAkC,KAAK,GAChC;AACT;AAGA,SAAS,gBAAgB,UAAiB;AACxC,SAAI,CAAC,YAAY,OAAO,YAAa,WAC5B,OAEF,SACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAI,CAAE,EACzB,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AACrC;AAEA,SAAS,kCAAkC,OAAY;AAErD,QAAM,QAAQ,MAAM,OAAO,IAAI,YAAY;AAC3C,MAAK;AAGL,eAAW,WAAW,MAAM,aAAY,GAAI;AAC1C,UAAI,QAAQ,iBAAiB;AAC3B;AAEF,YAAM,WAAW,gBAAgB,QAAQ,WAAW,QAAQ;AAC5D,MAAI,aACF,QAAQ,aAAa,EAAE,GAAG,QAAQ,YAAY,SAAQ,GACtD,MAAM,cAAc,OAAO;IAE/B;AACF;AAEA,eAAe,iCACb,OAAY;AAEZ,QAAM,KAAK,MAAM,OAAO,aAAa,GAC/B,aAAa,MAAM,OAAO,MAAM,GAChC,OAAO,MAAM,OAAO,MAAM,GAE1B,eAAe,WAAW,KAAK,MAAM,UAAU,4CAA4C;AACjG,MAAI,gBAAgB;AAEpB,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;EAC9B,SAAS,OAAO;AAEd,QAAK,MAA4B,SAAS;AACxC,aAAO,EAAE,eAAe,GAAG,QAAQ,CAAA,EAAE;AAEvC,UAAM;EACR;AAEA,QAAM,QAAQ,MAAM,GAAG,QAAQ,YAAY,GACrC,SAAmB,CAAA;AAEzB,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,SAAS,OAAO,KAAK,CAAC,KAAK,SAAS,MAAM;AAClD;AAGF,UAAM,WAAW,WAAW,KAAK,cAAc,IAAI;AACnD,QAAI;AACF,YAAM,UAAU,MAAM,GAAG,SAAS,UAAU,OAAO;AACnD,UAAI,WAAW;AACf,YAAM,MAAM,KAAK,MAAM,OAAO;AAE9B,UAAI,CAAC,OAAO,OAAO,OAAQ;AACzB;AAGF,iBAAW,CAAC,EAAE,OAAO,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC7C,YACE,CAAC,WACD,OAAO,WAAY,YAClB,QAAoC,iBAAiB;AAEtD;AAGF,cAAM,aAAc,QAAoC;AAIxD,YAAI,cAAc,OAAO,cAAe,UAAU;AAChD,gBAAM,WAAW,gBAAgB,WAAW,QAAQ;AACpD,UAAI,aACF,WAAW,WAAW,UACtB,WAAW;QAEf;MACF;AAEA,MAAI,aACF,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,GAAG,GAAG,OAAO,GACzD;IAEJ,SAAS,OAAO;AACd,aAAO,KAAK,qBAAqB,IAAI,KAAK,gBAAgB,KAAK,CAAC,EAAE;IACpE;EACF;AAEA,SAAO,EAAE,eAAe,OAAM;AAChC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { Model } from "./model.js";
|
|
3
|
+
import { fileExists } from "../utils/file-io.js";
|
|
4
|
+
import { logVerbose as globalLogVerbose, logDebug as globalLogDebug } from "../utils/globals.js";
|
|
5
|
+
import { installSpecReference } from "../utils/spec-installer.js";
|
|
6
|
+
import { getCliBundledSpecVersion } from "../utils/spec-version.js";
|
|
7
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
8
|
+
import { regenerateLayerReports } from "../commands/reports.js";
|
|
9
|
+
function modelManifestPath(rootPath) {
|
|
10
|
+
return join(rootPath, "documentation-robotics", "model", "manifest.yaml");
|
|
11
|
+
}
|
|
12
|
+
async function hasModel(rootPath) {
|
|
13
|
+
return fileExists(modelManifestPath(rootPath));
|
|
14
|
+
}
|
|
15
|
+
async function initProject(rootPath, options) {
|
|
16
|
+
if (await hasModel(rootPath))
|
|
17
|
+
throw new Error(`A model already exists in ${rootPath}`);
|
|
18
|
+
const logVerbose = options.logVerbose ?? globalLogVerbose, logDebug = options.logDebug ?? globalLogDebug, specVersion = getCliBundledSpecVersion(), modelPath = join(rootPath, "documentation-robotics", "model");
|
|
19
|
+
logDebug(`Creating model directory at ${rootPath}/documentation-robotics/model`);
|
|
20
|
+
const model = await Model.init(rootPath, {
|
|
21
|
+
name: options.name,
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
description: options.description || void 0,
|
|
24
|
+
author: options.author || void 0,
|
|
25
|
+
specVersion,
|
|
26
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
27
|
+
}, { lazyLoad: !1 });
|
|
28
|
+
logVerbose(`Model saved with:
|
|
29
|
+
- Version: 0.1.0
|
|
30
|
+
- Spec Version: ${specVersion}
|
|
31
|
+
- Location: ${rootPath}/documentation-robotics/model`), logDebug("Installing spec reference (.dr/ folder)..."), await installSpecReference(rootPath, !1), logVerbose("Spec reference installed");
|
|
32
|
+
let reportError;
|
|
33
|
+
logDebug("Generating reports and README...");
|
|
34
|
+
try {
|
|
35
|
+
await regenerateLayerReports(rootPath, options.log), logVerbose("Reports and README generated");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
reportError = getErrorMessage(error), logDebug(`Report generation failed (non-fatal): ${reportError}`);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
rootPath,
|
|
41
|
+
modelPath,
|
|
42
|
+
name: model.manifest.name,
|
|
43
|
+
version: "0.1.0",
|
|
44
|
+
specVersion,
|
|
45
|
+
...reportError !== void 0 ? { reportError } : {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
hasModel,
|
|
50
|
+
initProject,
|
|
51
|
+
modelManifestPath
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=project-init.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/project-init.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Project init - creates an empty model at the CLI's spec version in a folder.\n *\n * Shared by `dr init` (which gathers the name and the rest by prompting) and the\n * `project_init` MCP tool (which takes them as arguments). Neither installs per-project\n * integration files: `dr claude install` and the Claude Code plugin do that.\n */\n\nimport { join } from \"path\";\nimport { Model } from \"./model.js\";\nimport { fileExists } from \"../utils/file-io.js\";\nimport { logVerbose as globalLogVerbose, logDebug as globalLogDebug } from \"../utils/globals.js\";\nimport { installSpecReference } from \"../utils/spec-installer.js\";\nimport { getCliBundledSpecVersion } from \"../utils/spec-version.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { regenerateLayerReports } from \"../commands/reports.js\";\n\nexport interface ProjectInitOptions {\n name: string;\n description?: string;\n author?: string;\n /** Where progress lines go; `dr init` prints them, the MCP server must not write stdout. */\n log?: (message: string) => void;\n /** Where --verbose lines go (default: stdout when verbose is on). */\n logVerbose?: (message: string) => void;\n /** Where --debug lines go (default: console.debug when debug is on). */\n logDebug?: (message: string) => void;\n}\n\nexport interface ProjectInitResult {\n rootPath: string;\n modelPath: string;\n name: string;\n version: string;\n specVersion: string;\n /** Set when the model was created but its layer reports and README could not be generated. */\n reportError?: string;\n}\n\n/** The manifest path of the model in a folder (not searching parent folders). */\nexport function modelManifestPath(rootPath: string): string {\n return join(rootPath, \"documentation-robotics\", \"model\", \"manifest.yaml\");\n}\n\n/** Whether the folder already holds a model. */\nexport async function hasModel(rootPath: string): Promise<boolean> {\n return fileExists(modelManifestPath(rootPath));\n}\n\n/**\n * Create the model in rootPath, install the spec reference (.dr/) and generate the layer\n * reports and README. Throws when the folder already has a model. A report failure doesn't\n * fail the init: it comes back as reportError.\n */\nexport async function initProject(rootPath: string, options: ProjectInitOptions): Promise<ProjectInitResult> {\n if (await hasModel(rootPath)) {\n throw new Error(`A model already exists in ${rootPath}`);\n }\n\n const logVerbose = options.logVerbose ?? globalLogVerbose;\n const logDebug = options.logDebug ?? globalLogDebug;\n const specVersion = getCliBundledSpecVersion();\n const modelPath = join(rootPath, \"documentation-robotics\", \"model\");\n\n logDebug(`Creating model directory at ${rootPath}/documentation-robotics/model`);\n const model = await Model.init(\n rootPath,\n {\n name: options.name,\n version: \"0.1.0\",\n description: options.description || undefined,\n author: options.author || undefined,\n specVersion,\n created: new Date().toISOString(),\n },\n { lazyLoad: false }\n );\n\n logVerbose(`Model saved with:\n - Version: 0.1.0\n - Spec Version: ${specVersion}\n - Location: ${rootPath}/documentation-robotics/model`);\n\n logDebug(\"Installing spec reference (.dr/ folder)...\");\n await installSpecReference(rootPath, false);\n logVerbose(\"Spec reference installed\");\n\n let reportError: string | undefined;\n logDebug(\"Generating reports and README...\");\n try {\n await regenerateLayerReports(rootPath, options.log);\n logVerbose(\"Reports and README generated\");\n } catch (error) {\n reportError = getErrorMessage(error);\n logDebug(`Report generation failed (non-fatal): ${reportError}`);\n }\n\n return {\n rootPath,\n modelPath,\n name: model.manifest.name,\n version: \"0.1.0\",\n specVersion,\n ...(reportError !== undefined ? { reportError } : {}),\n };\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,cAAc,kBAAkB,YAAY,sBAAsB;AAC3E,SAAS,4BAA4B;AACrC,SAAS,gCAAgC;AACzC,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AAyBjC,SAAU,kBAAkB,UAAgB;AAChD,SAAO,KAAK,UAAU,0BAA0B,SAAS,eAAe;AAC1E;AAGA,eAAsB,SAAS,UAAgB;AAC7C,SAAO,WAAW,kBAAkB,QAAQ,CAAC;AAC/C;AAOA,eAAsB,YAAY,UAAkB,SAA2B;AAC7E,MAAI,MAAM,SAAS,QAAQ;AACzB,UAAM,IAAI,MAAM,6BAA6B,QAAQ,EAAE;AAGzD,QAAM,aAAa,QAAQ,cAAc,kBACnC,WAAW,QAAQ,YAAY,gBAC/B,cAAc,yBAAwB,GACtC,YAAY,KAAK,UAAU,0BAA0B,OAAO;AAElE,WAAS,+BAA+B,QAAQ,+BAA+B;AAC/E,QAAM,QAAQ,MAAM,MAAM,KACxB,UACA;IACE,MAAM,QAAQ;IACd,SAAS;IACT,aAAa,QAAQ,eAAe;IACpC,QAAQ,QAAQ,UAAU;IAC1B;IACA,UAAS,oBAAI,KAAI,GAAG,YAAW;KAEjC,EAAE,UAAU,GAAK,CAAE;AAGrB,aAAW;;oBAEO,WAAW;gBACf,QAAQ,+BAA+B,GAErD,SAAS,4CAA4C,GACrD,MAAM,qBAAqB,UAAU,EAAK,GAC1C,WAAW,0BAA0B;AAErC,MAAI;AACJ,WAAS,kCAAkC;AAC3C,MAAI;AACF,UAAM,uBAAuB,UAAU,QAAQ,GAAG,GAClD,WAAW,8BAA8B;EAC3C,SAAS,OAAO;AACd,kBAAc,gBAAgB,KAAK,GACnC,SAAS,yCAAyC,WAAW,EAAE;EACjE;AAEA,SAAO;IACL;IACA;IACA,MAAM,MAAM,SAAS;IACrB,SAAS;IACT;IACA,GAAI,gBAAgB,SAAY,EAAE,YAAW,IAAK,CAAA;;AAEtD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { RelationshipCatalog } from "./relationship-catalog.js";
|
|
2
|
+
import { getValidRelationships, getValidPredicatesForSource, getValidDestinationsForSourceAndPredicate } from "../generated/relationship-index.js";
|
|
3
|
+
import { normalizeNodeType } from "../generated/node-types.js";
|
|
4
|
+
import { findSimilar } from "../utils/errors.js";
|
|
1
5
|
function stagedRelationshipTriple(change) {
|
|
2
6
|
const data = change.after ?? change.before, [source, predicate, target] = data?.source ? [data.source, data.predicate, data.target] : change.elementId.split("::");
|
|
3
7
|
return source && predicate && target ? { source, predicate, target } : null;
|
|
@@ -57,12 +61,101 @@ async function stageRelationshipDeletes(manager, graph, changeset, relationships
|
|
|
57
61
|
}
|
|
58
62
|
return results;
|
|
59
63
|
}
|
|
64
|
+
function validateRelationshipCombination(sourceSpecNodeId, predicate, destSpecNodeId) {
|
|
65
|
+
if (getValidRelationships(sourceSpecNodeId, predicate, destSpecNodeId).length > 0)
|
|
66
|
+
return { valid: !0 };
|
|
67
|
+
const suggestions = [], validPredicates = getValidPredicatesForSource(sourceSpecNodeId);
|
|
68
|
+
if (validPredicates.length > 0 ? suggestions.push(`Valid predicates for ${sourceSpecNodeId}: ${validPredicates.join(", ")}`) : suggestions.push(`No valid relationships defined for ${sourceSpecNodeId}`), predicate) {
|
|
69
|
+
const validDests = getValidDestinationsForSourceAndPredicate(sourceSpecNodeId, predicate);
|
|
70
|
+
validDests.length > 0 ? suggestions.push(`Valid destinations for --[${predicate}]-->: ${validDests.join(", ")}`) : suggestions.push(`No valid destinations for ${sourceSpecNodeId} --[${predicate}]-->`);
|
|
71
|
+
}
|
|
72
|
+
return { valid: !1, suggestions };
|
|
73
|
+
}
|
|
74
|
+
async function planRelationshipBatch(graph, changeset, items, options = {}) {
|
|
75
|
+
const addStatus = options.addStatus ?? "staged", pending = options.pending ?? /* @__PURE__ */ new Map(), rejected = options.rejected ?? /* @__PURE__ */ new Map(), catalog = new RelationshipCatalog();
|
|
76
|
+
await catalog.load();
|
|
77
|
+
const changes = [], unstageKeys = [], results = [], seen = /* @__PURE__ */ new Set(), endpoint = (id, role) => {
|
|
78
|
+
const added = pending.get(id);
|
|
79
|
+
if (added)
|
|
80
|
+
return added;
|
|
81
|
+
const element = graph.getElement(id);
|
|
82
|
+
if (element && element.status !== "deleted")
|
|
83
|
+
return { id: element.id, layer: element.layer, type: element.type };
|
|
84
|
+
if (element)
|
|
85
|
+
return { error: `${role} element ${id} not found (this changeset deletes it)`, suggestions: [] };
|
|
86
|
+
const refusal = rejected.get(id);
|
|
87
|
+
if (refusal)
|
|
88
|
+
return { error: `${role} element ${id} was not added: ${refusal}`, suggestions: [] };
|
|
89
|
+
const layerPrefix = `${id.split(".")[0]}.`, candidates = [...graph.elements.values()].filter((e) => e.status !== "deleted" && e.id.startsWith(layerPrefix)).map((e) => e.id).concat([...pending.keys()].filter((k) => k.startsWith(layerPrefix))), similar = findSimilar(id, candidates, 4);
|
|
90
|
+
return {
|
|
91
|
+
error: `${role} element ${id} not found`,
|
|
92
|
+
suggestions: similar.length ? [`Did you mean: ${similar.join(" or ")}?`] : ['Use "model_search" to find elements, or "changeset_brief" to see what this changeset adds']
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
return items.forEach((item, index) => {
|
|
96
|
+
const asSent = { index, source: item.source, predicate: item.predicate, target: item.target }, invalid = (reason, suggestions) => {
|
|
97
|
+
results.push({ ...asSent, status: "invalid", reason, ...suggestions?.length ? { suggestions } : {} });
|
|
98
|
+
};
|
|
99
|
+
if (!item.source || !item.predicate || !item.target)
|
|
100
|
+
return invalid("Each relationship needs source, predicate and target");
|
|
101
|
+
const source = endpoint(item.source, "Source");
|
|
102
|
+
if ("error" in source)
|
|
103
|
+
return invalid(source.error, source.suggestions);
|
|
104
|
+
const target = endpoint(item.target, "Target");
|
|
105
|
+
if ("error" in target)
|
|
106
|
+
return invalid(target.error, target.suggestions);
|
|
107
|
+
const sourceSpec = `${source.layer}.${normalizeNodeType(source.layer, source.type)}`, targetSpec = `${target.layer}.${normalizeNodeType(target.layer, target.type)}`, validation = validateRelationshipCombination(sourceSpec, item.predicate, targetSpec);
|
|
108
|
+
if (!validation.valid)
|
|
109
|
+
return invalid(`The catalog has no ${sourceSpec} --[${item.predicate}]--> ${targetSpec}`, validation.suggestions);
|
|
110
|
+
const relType = catalog.getTypeByPredicate(item.predicate);
|
|
111
|
+
if (relType && !relType.semantics.reflexivity && source.id === target.id)
|
|
112
|
+
return invalid(`Predicate "${item.predicate}" does not allow reflexive relationships`);
|
|
113
|
+
const triple = { source: source.id, predicate: item.predicate, target: target.id }, key = `${triple.source}::${triple.predicate}::${triple.target}`, resolved = { ...asSent, ...triple };
|
|
114
|
+
if (seen.has(key)) {
|
|
115
|
+
results.push({ ...resolved, status: "duplicate", reason: "Repeated earlier in this batch" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
seen.add(key);
|
|
119
|
+
const plan = planRelationshipAdd(graph, changeset, triple);
|
|
120
|
+
if (plan.action === "duplicate") {
|
|
121
|
+
results.push({ ...resolved, status: "duplicate", reason: "Already in the model or this changeset" });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (plan.action === "restore") {
|
|
125
|
+
unstageKeys.push(...plan.keys), results.push({ ...resolved, status: "restored" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
changes.push({
|
|
129
|
+
type: "relationship-add",
|
|
130
|
+
elementId: key,
|
|
131
|
+
layerName: source.layer,
|
|
132
|
+
after: {
|
|
133
|
+
source: triple.source,
|
|
134
|
+
target: triple.target,
|
|
135
|
+
predicate: triple.predicate,
|
|
136
|
+
layer: source.layer,
|
|
137
|
+
...target.layer !== source.layer ? { targetLayer: target.layer } : {},
|
|
138
|
+
category: "structural",
|
|
139
|
+
...item.properties ? { properties: item.properties } : {}
|
|
140
|
+
}
|
|
141
|
+
}), results.push({ ...resolved, status: addStatus });
|
|
142
|
+
}), { changes, unstageKeys, results };
|
|
143
|
+
}
|
|
144
|
+
function countByStatus(results) {
|
|
145
|
+
const counts = {};
|
|
146
|
+
for (const r of results)
|
|
147
|
+
counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
148
|
+
return counts;
|
|
149
|
+
}
|
|
60
150
|
export {
|
|
151
|
+
countByStatus,
|
|
61
152
|
liveRelationshipsBetween,
|
|
62
153
|
planRelationshipAdd,
|
|
154
|
+
planRelationshipBatch,
|
|
63
155
|
stageRelationshipDeletes,
|
|
64
156
|
stagedRelationshipChangesAtEnds,
|
|
65
157
|
stagedRelationshipKeys,
|
|
66
|
-
stagedRelationshipTriple
|
|
158
|
+
stagedRelationshipTriple,
|
|
159
|
+
validateRelationshipCombination
|
|
67
160
|
};
|
|
68
161
|
//# sourceMappingURL=relationship-staging.js.map
|