@ybotok/loam 0.0.0 → 0.1.0-beta.1
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/CHANGELOG.md +89 -0
- package/COMPARISON.md +63 -0
- package/LICENSE +21 -0
- package/MIGRATING-from-OpenSpec.md +202 -0
- package/README.md +241 -9
- package/SCHEMA.md +425 -0
- package/SECURITY.md +9 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/adopt.js +226 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/archive.js +1297 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/delta.js +361 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/dependencies.js +78 -0
- package/dist/commands/dependencies.js.map +1 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gherkin.js +270 -0
- package/dist/commands/gherkin.js.map +1 -0
- package/dist/commands/init.js +250 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.js +361 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate-openspec.js +933 -0
- package/dist/commands/migrate-openspec.js.map +1 -0
- package/dist/commands/new.js +412 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/rebase.js +358 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/show.js +325 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.js +270 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unarchive.js +310 -0
- package/dist/commands/unarchive.js.map +1 -0
- package/dist/commands/validate.js +1609 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify.js +759 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/vouch.js +325 -0
- package/dist/commands/vouch.js.map +1 -0
- package/dist/core/agent.js +1823 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agents-stamp.js +68 -0
- package/dist/core/agents-stamp.js.map +1 -0
- package/dist/core/arch.js +104 -0
- package/dist/core/arch.js.map +1 -0
- package/dist/core/brief.js +733 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/coherence.js +553 -0
- package/dist/core/coherence.js.map +1 -0
- package/dist/core/config.js +172 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delta.js +457 -0
- package/dist/core/delta.js.map +1 -0
- package/dist/core/dependencies.js +352 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/docs.js +143 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/doctor.js +544 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/fleet-context.js +272 -0
- package/dist/core/fleet-context.js.map +1 -0
- package/dist/core/frontmatter.js +203 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/gherkin.js +521 -0
- package/dist/core/gherkin.js.map +1 -0
- package/dist/core/health.js +71 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/ids.js +93 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/issue.js +9 -0
- package/dist/core/issue.js.map +1 -0
- package/dist/core/json.js +67 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/likec4.js +474 -0
- package/dist/core/likec4.js.map +1 -0
- package/dist/core/openapi-merge.js +519 -0
- package/dist/core/openapi-merge.js.map +1 -0
- package/dist/core/openapi.js +243 -0
- package/dist/core/openapi.js.map +1 -0
- package/dist/core/openspec-inventory.js +1403 -0
- package/dist/core/openspec-inventory.js.map +1 -0
- package/dist/core/path-safety.js +109 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/provenance.js +695 -0
- package/dist/core/provenance.js.map +1 -0
- package/dist/core/repo.js +362 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/report.js +38 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/results.js +201 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/spec.js +481 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/staging.js +783 -0
- package/dist/core/staging.js.map +1 -0
- package/dist/core/status.js +1015 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/verify.js +793 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/version.js +8 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +63 -4
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isMap, parseDocument } from "yaml";
|
|
3
|
+
import { HTTP_METHODS, OPENAPI_BASELINE_KEY, classifyBaselineDigests, operationBaselineOf, operationDigest, withoutOperationBaseline, } from "./openapi.js";
|
|
4
|
+
/**
|
|
5
|
+
* Classify one feature operation against its living counterpart, from the raw
|
|
6
|
+
* YAML trees the merge works in. The rule itself lives in core/openapi.ts, so
|
|
7
|
+
* the gate and the merge cannot drift apart about which operations get written.
|
|
8
|
+
*
|
|
9
|
+
* A malformed pin matches no digest and lands in `stale`, which merges rather
|
|
10
|
+
* than skips. Deliberate: `openapi.baseline-invalid` refuses it at the gate, and
|
|
11
|
+
* a value nobody can evaluate must never be the reason a merge quietly drops an
|
|
12
|
+
* operation.
|
|
13
|
+
*/
|
|
14
|
+
export function classifyOperationBaseline(featureOp, livingOp) {
|
|
15
|
+
return classifyBaselineDigests(operationBaselineOf(featureOp), operationDigest(featureOp), livingOp === undefined ? undefined : operationDigest(livingOp));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pin every operation in a feature contract against the living one — what
|
|
19
|
+
* `loam rebase` writes on this axis.
|
|
20
|
+
*
|
|
21
|
+
* The pin is always `operationDigest` of the LIVING operation, never of the
|
|
22
|
+
* delta's own, and that single rule produces both merge verdicts by itself: an
|
|
23
|
+
* operation the author only QUOTED is byte-equal to living, so its pin equals
|
|
24
|
+
* its own content and `classifyOperationBaseline` calls it a quote; one the
|
|
25
|
+
* author EDITED differs from its pin, so the merge writes it. Nothing has to
|
|
26
|
+
* guess at intent — the document already records it, and the pin makes it
|
|
27
|
+
* legible to the merge.
|
|
28
|
+
*
|
|
29
|
+
* Slot-keyed (path + method), exactly as the merge upserts. An operationId that
|
|
30
|
+
* moved to another slot is a new slot with nothing living behind it, which is
|
|
31
|
+
* `unresolved` and correct: there is no living version of an operation at a
|
|
32
|
+
* path the contract does not serve yet.
|
|
33
|
+
*/
|
|
34
|
+
export function pinOpenapiOperations(featureText, livingText, service) {
|
|
35
|
+
const doc = parseDocument(featureText);
|
|
36
|
+
if (doc.errors.length > 0)
|
|
37
|
+
throw new OpenapiMergeError("feature", service, doc.errors[0].message);
|
|
38
|
+
let featPlain;
|
|
39
|
+
let livingPlain;
|
|
40
|
+
try {
|
|
41
|
+
featPlain = doc.toJS() ?? {};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new OpenapiMergeError("feature", service, errorMessage(error));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = parseDocument(livingText);
|
|
48
|
+
if (parsed.errors.length > 0)
|
|
49
|
+
throw new OpenapiMergeError("living", service, parsed.errors[0].message);
|
|
50
|
+
livingPlain = parsed.toJS() ?? {};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof OpenapiMergeError)
|
|
54
|
+
throw error;
|
|
55
|
+
throw new OpenapiMergeError("living", service, errorMessage(error));
|
|
56
|
+
}
|
|
57
|
+
const featPaths = plainChild(featPlain, "paths");
|
|
58
|
+
if (!isRecord(featPaths))
|
|
59
|
+
return { text: null, pins: [] };
|
|
60
|
+
const livingPaths = plainChild(livingPlain, "paths");
|
|
61
|
+
const pins = [];
|
|
62
|
+
let edited = false;
|
|
63
|
+
for (const [path, item] of Object.entries(featPaths)) {
|
|
64
|
+
if (!isRecord(item))
|
|
65
|
+
continue;
|
|
66
|
+
for (const [method, op] of Object.entries(item)) {
|
|
67
|
+
if (!HTTP_METHODS.has(method) || !isRecord(op))
|
|
68
|
+
continue;
|
|
69
|
+
// A removal marker asserts a slot rather than restating an operation, so
|
|
70
|
+
// it has nothing to be based on; `openapi.remove-target-mismatch` is what
|
|
71
|
+
// guards it against a living slot that moved under it.
|
|
72
|
+
if (isRemoval(op))
|
|
73
|
+
continue;
|
|
74
|
+
const id = typeof op["operationId"] === "string" ? op["operationId"] : "";
|
|
75
|
+
const at = { path, method, operationId: id };
|
|
76
|
+
const from = operationBaselineOf(op) ?? null;
|
|
77
|
+
const livingItem = plainChild(livingPaths, path);
|
|
78
|
+
const livingOp = isRecord(livingItem) ? livingItem[method] : undefined;
|
|
79
|
+
if (!isRecord(livingOp)) {
|
|
80
|
+
pins.push({ ...at, status: "unresolved", from, to: null });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const digest = operationDigest(livingOp);
|
|
84
|
+
if (from === digest) {
|
|
85
|
+
pins.push({ ...at, status: "unchanged", from: digest, to: digest });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// One shared value backs every use of an anchor, so `setIn` through an
|
|
89
|
+
// alias would pin every other use of it too — the same reason
|
|
90
|
+
// stripOpenapiRemovalMarkers refuses to edit aliases in place.
|
|
91
|
+
if (!isMap(doc.getIn(["paths", path, method]))) {
|
|
92
|
+
pins.push({ ...at, status: "unwritable", from, to: null });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
pins.push({ ...at, status: from === null ? "pinned" : "repinned", from, to: digest });
|
|
96
|
+
doc.setIn(["paths", path, method, OPENAPI_BASELINE_KEY], digest);
|
|
97
|
+
edited = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!edited)
|
|
101
|
+
return { text: null, pins };
|
|
102
|
+
try {
|
|
103
|
+
return { text: doc.toString(), pins };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new OpenapiMergeError("feature", service, errorMessage(error));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** A document the merge planner cannot read. Commands map this domain error to their own envelope. */
|
|
110
|
+
export class OpenapiMergeError extends Error {
|
|
111
|
+
source;
|
|
112
|
+
service;
|
|
113
|
+
name = "OpenapiMergeError";
|
|
114
|
+
constructor(source, service, detail) {
|
|
115
|
+
super(`${source} openapi for ${service} is not valid YAML: ${detail}`);
|
|
116
|
+
this.source = source;
|
|
117
|
+
this.service = service;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Remove feature-only operation-removal markers before a feature contract is
|
|
122
|
+
* used to create a brand-new living document. Normally coherence refuses this
|
|
123
|
+
* shape because there cannot be a removal target; this guard also keeps
|
|
124
|
+
* `--approve` from ever persisting the marker into living docs.
|
|
125
|
+
*/
|
|
126
|
+
export function stripOpenapiRemovalMarkers(featureText, service) {
|
|
127
|
+
const feature = parseDocument(featureText);
|
|
128
|
+
if (feature.errors.length > 0) {
|
|
129
|
+
throw new OpenapiMergeError("feature", service, feature.errors[0].message);
|
|
130
|
+
}
|
|
131
|
+
let plain;
|
|
132
|
+
try {
|
|
133
|
+
plain = feature.toJS() ?? {};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new OpenapiMergeError("feature", service, errorMessage(error));
|
|
137
|
+
}
|
|
138
|
+
// Shape from the RESOLVED tree, for the same reason the merge reads it there:
|
|
139
|
+
// an aliased `paths` or path item is not a map node, and treating it as
|
|
140
|
+
// "nothing to strip" is how a feature-only marker reached a living contract.
|
|
141
|
+
const paths = plainChild(plain, "paths");
|
|
142
|
+
if (!isRecord(paths))
|
|
143
|
+
return featureText;
|
|
144
|
+
const cleaned = {};
|
|
145
|
+
let stripped = false;
|
|
146
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
147
|
+
const kept = withoutFeatureMarkers(item);
|
|
148
|
+
if (!isDeepStrictEqual(kept, item))
|
|
149
|
+
stripped = true;
|
|
150
|
+
// A path the strip emptied goes with its last operation: `\/x: {}` is a
|
|
151
|
+
// path the contract advertises and nothing answers.
|
|
152
|
+
if (kept !== undefined)
|
|
153
|
+
cleaned[path] = kept;
|
|
154
|
+
}
|
|
155
|
+
if (!stripped)
|
|
156
|
+
return featureText;
|
|
157
|
+
// Edit the AST in place when every node the strip touches IS a node — the
|
|
158
|
+
// author's comments, key order and formatting are theirs to keep. An alias
|
|
159
|
+
// cannot be edited in place (there is one shared value behind it, and
|
|
160
|
+
// deleting a key through it would change every use), so those documents get
|
|
161
|
+
// their `paths` rewritten from the resolved tree instead: losing formatting
|
|
162
|
+
// is a cost, shipping the marker is a corruption.
|
|
163
|
+
const editable = isMap(feature.get("paths")) && Object.keys(paths).every((p) => isMap(feature.getIn(["paths", p])));
|
|
164
|
+
if (!editable) {
|
|
165
|
+
feature.setIn(["paths"], cleaned);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
169
|
+
if (!isRecord(item))
|
|
170
|
+
continue;
|
|
171
|
+
for (const [m, value] of Object.entries(item)) {
|
|
172
|
+
// A loam marker at PATH level, deleted here for the same reason the
|
|
173
|
+
// resolved-tree branch drops it: the two branches must agree exactly
|
|
174
|
+
// about what a living contract may carry.
|
|
175
|
+
if (FEATURE_ONLY_KEYS.has(m))
|
|
176
|
+
feature.deleteIn(["paths", path, m]);
|
|
177
|
+
if (!HTTP_METHODS.has(m))
|
|
178
|
+
continue;
|
|
179
|
+
if (isRemoval(value))
|
|
180
|
+
feature.deleteIn(["paths", path, m]);
|
|
181
|
+
// The surviving operations keep their formatting but lose their pin:
|
|
182
|
+
// the in-place branch must strip everything the resolved-tree branch
|
|
183
|
+
// does, or the two disagree about what a living contract may carry.
|
|
184
|
+
else if (operationBaselineOf(value) !== undefined) {
|
|
185
|
+
feature.deleteIn(["paths", path, m, OPENAPI_BASELINE_KEY]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const remaining = feature.getIn(["paths", path]);
|
|
189
|
+
if (isMap(remaining) && remaining.items.length === 0)
|
|
190
|
+
feature.deleteIn(["paths", path]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return feature.toString();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new OpenapiMergeError("feature", service, errorMessage(error));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Merge the feature's `paths` into the living OpenAPI structurally (YAML AST, not
|
|
202
|
+
* text splicing). A feature document without paths is a successful no-op.
|
|
203
|
+
* The merged operations' local component-ref closure rides along recursively;
|
|
204
|
+
* external refs are left untouched and never gated.
|
|
205
|
+
*
|
|
206
|
+
* Every SHAPE question — is there a `paths` mapping, is this path item a
|
|
207
|
+
* mapping, which methods does it hold — is answered from the RESOLVED plain
|
|
208
|
+
* trees, never from the AST node. Asking the AST (`isMap(node)`) answers a
|
|
209
|
+
* different question: an alias node is not a map even when it resolves to one.
|
|
210
|
+
* `paths: *alias` therefore read as "no paths to merge" and the whole contract
|
|
211
|
+
* delta was dropped with a successful exit, and an aliased path ITEM fell into
|
|
212
|
+
* the wholesale-replace branch and deleted every living operation on that path
|
|
213
|
+
* that the alias did not restate. Aliases are legal OpenAPI and the natural way
|
|
214
|
+
* to write a delta that repeats a shape; nothing here may treat them as absent.
|
|
215
|
+
* An alias that cannot be resolved at all is a document loam cannot read, and
|
|
216
|
+
* says so — it is never "nothing to merge".
|
|
217
|
+
*/
|
|
218
|
+
export function mergeOpenapiPaths(livingText, featureText, service) {
|
|
219
|
+
const feature = parseDocument(featureText);
|
|
220
|
+
if (feature.errors.length > 0) {
|
|
221
|
+
throw new OpenapiMergeError("feature", service, feature.errors[0].message);
|
|
222
|
+
}
|
|
223
|
+
let featPlain;
|
|
224
|
+
try {
|
|
225
|
+
// Resolve aliases once with the document's own anchor context. Calling an
|
|
226
|
+
// individual AST node's toJSON() loses that context and can silently turn
|
|
227
|
+
// an aliased operation or component into the wrong value.
|
|
228
|
+
featPlain = feature.toJS() ?? {};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
throw new OpenapiMergeError("feature", service, errorMessage(error));
|
|
232
|
+
}
|
|
233
|
+
const featPathsPlain = plainChild(featPlain, "paths");
|
|
234
|
+
if (featPathsPlain === undefined || featPathsPlain === null) {
|
|
235
|
+
return noop();
|
|
236
|
+
}
|
|
237
|
+
if (!isRecord(featPathsPlain)) {
|
|
238
|
+
throw new OpenapiMergeError("feature", service, "`paths` is not a mapping");
|
|
239
|
+
}
|
|
240
|
+
const featPathEntries = Object.entries(featPathsPlain);
|
|
241
|
+
if (featPathEntries.length === 0)
|
|
242
|
+
return noop();
|
|
243
|
+
const living = parseDocument(livingText);
|
|
244
|
+
if (living.errors.length > 0) {
|
|
245
|
+
throw new OpenapiMergeError("living", service, living.errors[0].message);
|
|
246
|
+
}
|
|
247
|
+
let livingPlain;
|
|
248
|
+
try {
|
|
249
|
+
livingPlain = living.toJS() ?? {};
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new OpenapiMergeError("living", service, errorMessage(error));
|
|
253
|
+
}
|
|
254
|
+
const livingPathsPlain = plainChild(livingPlain, "paths");
|
|
255
|
+
if (livingPathsPlain !== undefined && livingPathsPlain !== null && !isRecord(livingPathsPlain)) {
|
|
256
|
+
throw new OpenapiMergeError("living", service, "`paths` is not a mapping");
|
|
257
|
+
}
|
|
258
|
+
const modified = [];
|
|
259
|
+
const pathItemModified = [];
|
|
260
|
+
const removed = [];
|
|
261
|
+
const quoted = [];
|
|
262
|
+
const baselineStale = [];
|
|
263
|
+
for (const [path, featItemPlain] of featPathEntries) {
|
|
264
|
+
const existing = living.getIn(["paths", path]);
|
|
265
|
+
const existingPlain = plainChild(livingPathsPlain, path);
|
|
266
|
+
if (existing !== undefined && !isRecord(existingPlain)) {
|
|
267
|
+
// The living contract holds this path as something other than a path
|
|
268
|
+
// item. Replacing it wholesale is the one branch that may delete living
|
|
269
|
+
// operations without naming them, so it is reserved for paths the living
|
|
270
|
+
// contract does not have at all.
|
|
271
|
+
throw new OpenapiMergeError("living", service, `path '${path}' is not a mapping`);
|
|
272
|
+
}
|
|
273
|
+
if (existing !== undefined) {
|
|
274
|
+
if (!isRecord(featItemPlain)) {
|
|
275
|
+
throw new OpenapiMergeError("feature", service, `path '${path}' is not a mapping`);
|
|
276
|
+
}
|
|
277
|
+
// EVERY branch publishes through withoutFeatureMarkers. This one used to
|
|
278
|
+
// copy every key of the feature path item and guard only the HTTP
|
|
279
|
+
// methods, so a path-level `x-loam-remove` landed in the living contract
|
|
280
|
+
// with `modified: []` and no warning. A key the strip drops is never
|
|
281
|
+
// written; the removal handling below still reads the RAW item, because
|
|
282
|
+
// that is where the markers it acts on live.
|
|
283
|
+
const publish = withoutFeatureMarkers(featItemPlain);
|
|
284
|
+
const publishable = isRecord(publish) ? publish : {};
|
|
285
|
+
for (const [m, afterPlain] of Object.entries(featItemPlain)) {
|
|
286
|
+
const before = living.getIn(["paths", path, m]);
|
|
287
|
+
const beforePlain = plainChild(existingPlain, m);
|
|
288
|
+
if (HTTP_METHODS.has(m) && isRemoval(afterPlain)) {
|
|
289
|
+
// Coherence validates the marker and gates absent/mismatched targets.
|
|
290
|
+
// The merge remains defensive under --approve: never delete a
|
|
291
|
+
// different operation merely because it occupies the requested slot.
|
|
292
|
+
if (before !== undefined &&
|
|
293
|
+
operationIdOf(beforePlain) !== undefined &&
|
|
294
|
+
operationIdOf(beforePlain) === operationIdOf(afterPlain)) {
|
|
295
|
+
removed.push(opLabel(beforePlain, afterPlain, m, path));
|
|
296
|
+
living.deleteIn(["paths", path, m]);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (HTTP_METHODS.has(m)) {
|
|
301
|
+
const verdict = classifyOperationBaseline(afterPlain, before === undefined ? undefined : beforePlain);
|
|
302
|
+
// A QUOTE is not a merge input. The author wrote this operation down
|
|
303
|
+
// because the document is complete, not because they changed it, so
|
|
304
|
+
// the living contract keeps whatever it holds — including a change
|
|
305
|
+
// that landed after this delta was written. This is mechanical, not a
|
|
306
|
+
// judgement, so `--approve` does not turn it back on: overriding a
|
|
307
|
+
// gate is a decision, reverting an operation nobody edited is a bug.
|
|
308
|
+
if (verdict === "quote") {
|
|
309
|
+
quoted.push(opLabel(beforePlain, afterPlain, m, path));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (verdict === "stale")
|
|
313
|
+
baselineStale.push(opLabel(beforePlain, afterPlain, m, path));
|
|
314
|
+
}
|
|
315
|
+
if (!(m in publishable)) {
|
|
316
|
+
// A feature-only marker at path level. It is never published, and
|
|
317
|
+
// never reported as a path-item overwrite either — it is not a key of
|
|
318
|
+
// the contract at all. The archive gate names it to the author.
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
// The difference check covers EVERY key of the path item; only the
|
|
322
|
+
// LABEL depends on whether the key is an HTTP method.
|
|
323
|
+
if (before !== undefined && !isDeepStrictEqual(beforePlain, afterPlain)) {
|
|
324
|
+
if (HTTP_METHODS.has(m))
|
|
325
|
+
modified.push(opLabel(beforePlain, afterPlain, m, path));
|
|
326
|
+
else
|
|
327
|
+
pathItemModified.push(`'${m}' (${path})`);
|
|
328
|
+
}
|
|
329
|
+
// Without the strip the living contract would grow a pin to a version
|
|
330
|
+
// of itself, and the NEXT feature's baseline would hash it.
|
|
331
|
+
living.setIn(["paths", path, m], publishable[m]);
|
|
332
|
+
}
|
|
333
|
+
// Removing the last method leaves `\/x: {}` — a path the contract still
|
|
334
|
+
// advertises and nothing answers. The same cleanup
|
|
335
|
+
// stripOpenapiRemovalMarkers already does on the feature side.
|
|
336
|
+
const remaining = living.getIn(["paths", path]);
|
|
337
|
+
if (isMap(remaining) && remaining.items.length === 0)
|
|
338
|
+
living.deleteIn(["paths", path]);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!isRecord(featItemPlain)) {
|
|
342
|
+
throw new OpenapiMergeError("feature", service, `path '${path}' is not a mapping`);
|
|
343
|
+
}
|
|
344
|
+
const clean = withoutFeatureMarkers(featItemPlain);
|
|
345
|
+
if (clean !== undefined)
|
|
346
|
+
living.setIn(["paths", path], clean);
|
|
347
|
+
}
|
|
348
|
+
const componentsModified = [];
|
|
349
|
+
const unresolved = [];
|
|
350
|
+
const visited = new Set();
|
|
351
|
+
const copies = [];
|
|
352
|
+
const visitRef = (ref, from) => {
|
|
353
|
+
if (!ref.startsWith("#/"))
|
|
354
|
+
return;
|
|
355
|
+
const match = /^#\/components\/([^/]+)\/([^/]+)(?:\/|$)/.exec(ref);
|
|
356
|
+
if (!match) {
|
|
357
|
+
if (!resolvePointer(featPlain, ref).found && !resolvePointer(livingPlain, ref).found) {
|
|
358
|
+
unresolved.push({ ref, from });
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const kind = match[1];
|
|
363
|
+
const name = match[2];
|
|
364
|
+
const key = `${kind}/${name}`;
|
|
365
|
+
if (visited.has(key))
|
|
366
|
+
return;
|
|
367
|
+
visited.add(key);
|
|
368
|
+
const inFeature = resolvePointer(featPlain, `#/components/${kind}/${name}`);
|
|
369
|
+
if (inFeature.found) {
|
|
370
|
+
copies.push({ kind, name, value: inFeature.value });
|
|
371
|
+
walk(inFeature.value, `components/${key}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!resolvePointer(livingPlain, `#/components/${kind}/${name}`).found) {
|
|
375
|
+
unresolved.push({ ref, from });
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const walk = (node, from) => {
|
|
379
|
+
for (const ref of collectRefs(node))
|
|
380
|
+
visitRef(ref, from);
|
|
381
|
+
};
|
|
382
|
+
for (const [path, featItemPlain] of featPathEntries) {
|
|
383
|
+
walk(withoutFeatureMarkers(featItemPlain), `paths ${path}`);
|
|
384
|
+
}
|
|
385
|
+
for (const { kind, name, value } of copies) {
|
|
386
|
+
const existing = living.getIn(["components", kind, name]);
|
|
387
|
+
if (existing !== undefined) {
|
|
388
|
+
const existingPlain = resolvePointer(livingPlain, `#/components/${kind}/${name}`);
|
|
389
|
+
if (existingPlain.found && isDeepStrictEqual(existingPlain.value, value))
|
|
390
|
+
continue;
|
|
391
|
+
componentsModified.push(`${kind}/${name}`);
|
|
392
|
+
}
|
|
393
|
+
living.setIn(["components", kind, name], value);
|
|
394
|
+
}
|
|
395
|
+
let text;
|
|
396
|
+
try {
|
|
397
|
+
text = living.toString();
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
throw new OpenapiMergeError("living", service, errorMessage(error));
|
|
401
|
+
}
|
|
402
|
+
return { text, modified, pathItemModified, removed, quoted, baselineStale, componentsModified, unresolved };
|
|
403
|
+
}
|
|
404
|
+
/** The successful "the feature document has nothing to merge" answer. */
|
|
405
|
+
function noop() {
|
|
406
|
+
return {
|
|
407
|
+
text: null,
|
|
408
|
+
modified: [],
|
|
409
|
+
pathItemModified: [],
|
|
410
|
+
removed: [],
|
|
411
|
+
quoted: [],
|
|
412
|
+
baselineStale: [],
|
|
413
|
+
componentsModified: [],
|
|
414
|
+
unresolved: [],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** A resolved plain tree that can hold OpenAPI keys — an object, not an array. */
|
|
418
|
+
function isRecord(node) {
|
|
419
|
+
return node !== null && typeof node === "object" && !Array.isArray(node);
|
|
420
|
+
}
|
|
421
|
+
/** Every `$ref` string value anywhere in a plain JS tree, in document order. */
|
|
422
|
+
export function collectRefs(node) {
|
|
423
|
+
const out = [];
|
|
424
|
+
const walk = (current) => {
|
|
425
|
+
if (Array.isArray(current)) {
|
|
426
|
+
for (const value of current)
|
|
427
|
+
walk(value);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (current !== null && typeof current === "object") {
|
|
431
|
+
for (const [key, value] of Object.entries(current)) {
|
|
432
|
+
if (key === "$ref" && typeof value === "string")
|
|
433
|
+
out.push(value);
|
|
434
|
+
else
|
|
435
|
+
walk(value);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
walk(node);
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
/** Resolve a local JSON pointer (`#/a/b~1c`) against a plain JS tree. */
|
|
443
|
+
export function resolvePointer(root, ref) {
|
|
444
|
+
let current = root;
|
|
445
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
446
|
+
const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
447
|
+
if (Array.isArray(current)) {
|
|
448
|
+
const index = Number(segment);
|
|
449
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
450
|
+
return { found: false, value: undefined };
|
|
451
|
+
}
|
|
452
|
+
current = current[index];
|
|
453
|
+
}
|
|
454
|
+
else if (current !== null && typeof current === "object" && segment in current) {
|
|
455
|
+
current = current[segment];
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
return { found: false, value: undefined };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { found: true, value: current };
|
|
462
|
+
}
|
|
463
|
+
/** Name an overwritten operation by its operationId (feature's, else living's), or path+method. */
|
|
464
|
+
export function opLabel(before, after, method, path) {
|
|
465
|
+
const operation = operationIdOf(after) ?? operationIdOf(before);
|
|
466
|
+
return operation !== undefined ? `'${operation}' (${method} ${path})` : `${method} ${path}`;
|
|
467
|
+
}
|
|
468
|
+
/** Is this operation node a feature-only explicit removal marker? */
|
|
469
|
+
function isRemoval(node) {
|
|
470
|
+
return node !== null &&
|
|
471
|
+
typeof node === "object" &&
|
|
472
|
+
node["x-loam-remove"] === true;
|
|
473
|
+
}
|
|
474
|
+
/** Read an operationId from a YAML or plain operation node. */
|
|
475
|
+
function operationIdOf(node) {
|
|
476
|
+
const value = isMap(node)
|
|
477
|
+
? node.get("operationId")
|
|
478
|
+
: node !== null && typeof node === "object"
|
|
479
|
+
? node["operationId"]
|
|
480
|
+
: undefined;
|
|
481
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Every key that is an instruction to loam rather than part of the contract.
|
|
485
|
+
* At PATH level neither has any meaning — `x-loam-remove` addresses one
|
|
486
|
+
* operation and `x-loam-based-on` pins one — so a path item carrying either is
|
|
487
|
+
* an authoring mistake, and one that must not be published whatever else is
|
|
488
|
+
* done about it.
|
|
489
|
+
*/
|
|
490
|
+
const FEATURE_ONLY_KEYS = new Set(["x-loam-remove", OPENAPI_BASELINE_KEY]);
|
|
491
|
+
/**
|
|
492
|
+
* Copy a path item without anything that belongs to the FEATURE rather than to
|
|
493
|
+
* the contract: removal-marker operations, the `x-loam-based-on` pins on the
|
|
494
|
+
* operations that survive, and a loam marker written at PATH level. All three
|
|
495
|
+
* are instructions to loam, and a living contract that carries any of them is
|
|
496
|
+
* lying to the next reader — and, for the pin, to the next feature's baseline.
|
|
497
|
+
*
|
|
498
|
+
* The path-level case used to fall through: the filter guarded only HTTP
|
|
499
|
+
* methods, so `x-loam-remove: true` beside the methods was copied verbatim into
|
|
500
|
+
* the fleet's living contract, where it also kept the empty-path cleanup from
|
|
501
|
+
* ever seeing an empty path.
|
|
502
|
+
*/
|
|
503
|
+
function withoutFeatureMarkers(node) {
|
|
504
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
505
|
+
return node;
|
|
506
|
+
const entries = Object.entries(node)
|
|
507
|
+
.filter(([key, value]) => !FEATURE_ONLY_KEYS.has(key) && !(HTTP_METHODS.has(key) && isRemoval(value)))
|
|
508
|
+
.map(([key, value]) => [key, HTTP_METHODS.has(key) ? withoutOperationBaseline(value) : value]);
|
|
509
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
510
|
+
}
|
|
511
|
+
function plainChild(parent, key) {
|
|
512
|
+
return parent !== null && typeof parent === "object"
|
|
513
|
+
? parent[key]
|
|
514
|
+
: undefined;
|
|
515
|
+
}
|
|
516
|
+
function errorMessage(error) {
|
|
517
|
+
return error instanceof Error ? error.message : String(error);
|
|
518
|
+
}
|
|
519
|
+
//# sourceMappingURL=openapi-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-merge.js","sourceRoot":"","sources":["../../src/core/openapi-merge.ts"],"names":[],"mappings":"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","sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\nimport { isMap, parseDocument } from \"yaml\";\nimport {\n HTTP_METHODS,\n OPENAPI_BASELINE_KEY,\n classifyBaselineDigests,\n operationBaselineOf,\n operationDigest,\n withoutOperationBaseline,\n} from \"./openapi.js\";\n\n/**\n * What a feature's operation IS, relative to the living contract — the one\n * judgement that tells a QUOTE from an EDIT.\n *\n * A feature's openapi.yaml is a complete document, so most of what it spells is\n * quotation: the author restated the living contract around the slot they were\n * changing. Without a pin the merge cannot tell the two apart and upserts both,\n * which is how a feature that only meant to touch `cancelOrder` silently\n * reverted another team's landed change to `refundOrder` — no overlap between\n * the features required, just two deltas over one service.\n */\nexport type OperationBaselineVerdict = ReturnType<typeof classifyBaselineDigests>;\n\n/**\n * Classify one feature operation against its living counterpart, from the raw\n * YAML trees the merge works in. The rule itself lives in core/openapi.ts, so\n * the gate and the merge cannot drift apart about which operations get written.\n *\n * A malformed pin matches no digest and lands in `stale`, which merges rather\n * than skips. Deliberate: `openapi.baseline-invalid` refuses it at the gate, and\n * a value nobody can evaluate must never be the reason a merge quietly drops an\n * operation.\n */\nexport function classifyOperationBaseline(featureOp: unknown, livingOp: unknown): OperationBaselineVerdict {\n return classifyBaselineDigests(\n operationBaselineOf(featureOp),\n operationDigest(featureOp),\n livingOp === undefined ? undefined : operationDigest(livingOp),\n );\n}\n\n/** What happened to one operation's `x-loam-based-on`. */\nexport interface OperationPin {\n path: string;\n method: string;\n /** Empty when the operation declares none — the slot is still the identity. */\n operationId: string;\n status:\n /** It had no pin and now has one. */\n | \"pinned\"\n /** It named an older living version; it now names the current one. */\n | \"repinned\"\n /** It already named the current living version — no write. */\n | \"unchanged\"\n /** The living contract has no operation at this slot: this feature is adding it. */\n | \"unresolved\"\n /** Written as a YAML alias — one shared value backs every use, so loam will not stamp through it. */\n | \"unwritable\";\n from: string | null;\n to: string | null;\n}\n\nexport interface OpenapiPinPlan {\n /** The rewritten contract, or null when no pin changes. */\n text: string | null;\n pins: OperationPin[];\n}\n\n/**\n * Pin every operation in a feature contract against the living one — what\n * `loam rebase` writes on this axis.\n *\n * The pin is always `operationDigest` of the LIVING operation, never of the\n * delta's own, and that single rule produces both merge verdicts by itself: an\n * operation the author only QUOTED is byte-equal to living, so its pin equals\n * its own content and `classifyOperationBaseline` calls it a quote; one the\n * author EDITED differs from its pin, so the merge writes it. Nothing has to\n * guess at intent — the document already records it, and the pin makes it\n * legible to the merge.\n *\n * Slot-keyed (path + method), exactly as the merge upserts. An operationId that\n * moved to another slot is a new slot with nothing living behind it, which is\n * `unresolved` and correct: there is no living version of an operation at a\n * path the contract does not serve yet.\n */\nexport function pinOpenapiOperations(featureText: string, livingText: string, service: string): OpenapiPinPlan {\n const doc = parseDocument(featureText);\n if (doc.errors.length > 0) throw new OpenapiMergeError(\"feature\", service, doc.errors[0]!.message);\n let featPlain: unknown;\n let livingPlain: unknown;\n try {\n featPlain = doc.toJS() ?? {};\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n try {\n const parsed = parseDocument(livingText);\n if (parsed.errors.length > 0) throw new OpenapiMergeError(\"living\", service, parsed.errors[0]!.message);\n livingPlain = parsed.toJS() ?? {};\n } catch (error) {\n if (error instanceof OpenapiMergeError) throw error;\n throw new OpenapiMergeError(\"living\", service, errorMessage(error));\n }\n\n const featPaths = plainChild(featPlain, \"paths\");\n if (!isRecord(featPaths)) return { text: null, pins: [] };\n const livingPaths = plainChild(livingPlain, \"paths\");\n\n const pins: OperationPin[] = [];\n let edited = false;\n for (const [path, item] of Object.entries(featPaths)) {\n if (!isRecord(item)) continue;\n for (const [method, op] of Object.entries(item)) {\n if (!HTTP_METHODS.has(method) || !isRecord(op)) continue;\n // A removal marker asserts a slot rather than restating an operation, so\n // it has nothing to be based on; `openapi.remove-target-mismatch` is what\n // guards it against a living slot that moved under it.\n if (isRemoval(op)) continue;\n\n const id = typeof op[\"operationId\"] === \"string\" ? op[\"operationId\"] : \"\";\n const at = { path, method, operationId: id };\n const from = operationBaselineOf(op) ?? null;\n const livingItem = plainChild(livingPaths, path);\n const livingOp = isRecord(livingItem) ? livingItem[method] : undefined;\n if (!isRecord(livingOp)) {\n pins.push({ ...at, status: \"unresolved\", from, to: null });\n continue;\n }\n const digest = operationDigest(livingOp);\n if (from === digest) {\n pins.push({ ...at, status: \"unchanged\", from: digest, to: digest });\n continue;\n }\n // One shared value backs every use of an anchor, so `setIn` through an\n // alias would pin every other use of it too — the same reason\n // stripOpenapiRemovalMarkers refuses to edit aliases in place.\n if (!isMap(doc.getIn([\"paths\", path, method]))) {\n pins.push({ ...at, status: \"unwritable\", from, to: null });\n continue;\n }\n pins.push({ ...at, status: from === null ? \"pinned\" : \"repinned\", from, to: digest });\n doc.setIn([\"paths\", path, method, OPENAPI_BASELINE_KEY], digest);\n edited = true;\n }\n }\n\n if (!edited) return { text: null, pins };\n try {\n return { text: doc.toString(), pins };\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n}\n\n/** What an OpenAPI path merge computed, including every condition the caller must surface. */\nexport interface OpenapiMergeResult {\n /** The merged living document, or null when the feature document has no paths to merge. */\n text: string | null;\n /** Labels of existing operations overwritten with different content. */\n modified: string[];\n /**\n * Labels of PATH-LEVEL keys (`parameters`, `servers`, `summary`, `x-*`)\n * overwritten with different content. They are not operations, so they were\n * excluded from the difference check entirely and overwritten in silence —\n * a delta restating a path with a shorter `parameters` list dropped shared\n * parameters from every operation under it, and the plan said nothing.\n */\n pathItemModified: string[];\n /** Labels of living operations deleted by `x-loam-remove: true` markers. */\n removed: string[];\n /**\n * Labels of operations the delta QUOTED — pinned, and equal to their own\n * baseline — so the merge left the living contract's own copy alone. Reported\n * rather than silent: \"your delta mentions this and I did not write it\" is\n * exactly the sentence whose absence made the revert invisible.\n */\n quoted: string[];\n /**\n * Labels of operations whose pin matches neither the delta's own content nor\n * the living one: edited here AND changed by somebody else in between.\n * `openapi.baseline-stale` refuses these at the gate; the merge still\n * overwrites, because reaching it at all means `--approve` said to.\n */\n baselineStale: string[];\n /** `<kind>/<name>` of living components overwritten with different content. */\n componentsModified: string[];\n /** Local refs reachable from merged content that resolve in neither document. */\n unresolved: Array<{ ref: string; from: string }>;\n}\n\nexport type OpenapiMergeSource = \"feature\" | \"living\";\n\n/** A document the merge planner cannot read. Commands map this domain error to their own envelope. */\nexport class OpenapiMergeError extends Error {\n override readonly name = \"OpenapiMergeError\";\n\n constructor(\n readonly source: OpenapiMergeSource,\n readonly service: string,\n detail: string,\n ) {\n super(`${source} openapi for ${service} is not valid YAML: ${detail}`);\n }\n}\n\n/**\n * Remove feature-only operation-removal markers before a feature contract is\n * used to create a brand-new living document. Normally coherence refuses this\n * shape because there cannot be a removal target; this guard also keeps\n * `--approve` from ever persisting the marker into living docs.\n */\nexport function stripOpenapiRemovalMarkers(featureText: string, service: string): string {\n const feature = parseDocument(featureText);\n if (feature.errors.length > 0) {\n throw new OpenapiMergeError(\"feature\", service, feature.errors[0]!.message);\n }\n let plain: unknown;\n try {\n plain = feature.toJS() ?? {};\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n // Shape from the RESOLVED tree, for the same reason the merge reads it there:\n // an aliased `paths` or path item is not a map node, and treating it as\n // \"nothing to strip\" is how a feature-only marker reached a living contract.\n const paths = plainChild(plain, \"paths\");\n if (!isRecord(paths)) return featureText;\n\n const cleaned: Record<string, unknown> = {};\n let stripped = false;\n for (const [path, item] of Object.entries(paths)) {\n const kept = withoutFeatureMarkers(item);\n if (!isDeepStrictEqual(kept, item)) stripped = true;\n // A path the strip emptied goes with its last operation: `\\/x: {}` is a\n // path the contract advertises and nothing answers.\n if (kept !== undefined) cleaned[path] = kept;\n }\n if (!stripped) return featureText;\n\n // Edit the AST in place when every node the strip touches IS a node — the\n // author's comments, key order and formatting are theirs to keep. An alias\n // cannot be edited in place (there is one shared value behind it, and\n // deleting a key through it would change every use), so those documents get\n // their `paths` rewritten from the resolved tree instead: losing formatting\n // is a cost, shipping the marker is a corruption.\n const editable =\n isMap(feature.get(\"paths\")) && Object.keys(paths).every((p) => isMap(feature.getIn([\"paths\", p])));\n if (!editable) {\n feature.setIn([\"paths\"], cleaned);\n } else {\n for (const [path, item] of Object.entries(paths)) {\n if (!isRecord(item)) continue;\n for (const [m, value] of Object.entries(item)) {\n // A loam marker at PATH level, deleted here for the same reason the\n // resolved-tree branch drops it: the two branches must agree exactly\n // about what a living contract may carry.\n if (FEATURE_ONLY_KEYS.has(m)) feature.deleteIn([\"paths\", path, m]);\n if (!HTTP_METHODS.has(m)) continue;\n if (isRemoval(value)) feature.deleteIn([\"paths\", path, m]);\n // The surviving operations keep their formatting but lose their pin:\n // the in-place branch must strip everything the resolved-tree branch\n // does, or the two disagree about what a living contract may carry.\n else if (operationBaselineOf(value) !== undefined) {\n feature.deleteIn([\"paths\", path, m, OPENAPI_BASELINE_KEY]);\n }\n }\n const remaining = feature.getIn([\"paths\", path]);\n if (isMap(remaining) && remaining.items.length === 0) feature.deleteIn([\"paths\", path]);\n }\n }\n try {\n return feature.toString();\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n}\n\n/**\n * Merge the feature's `paths` into the living OpenAPI structurally (YAML AST, not\n * text splicing). A feature document without paths is a successful no-op.\n * The merged operations' local component-ref closure rides along recursively;\n * external refs are left untouched and never gated.\n *\n * Every SHAPE question — is there a `paths` mapping, is this path item a\n * mapping, which methods does it hold — is answered from the RESOLVED plain\n * trees, never from the AST node. Asking the AST (`isMap(node)`) answers a\n * different question: an alias node is not a map even when it resolves to one.\n * `paths: *alias` therefore read as \"no paths to merge\" and the whole contract\n * delta was dropped with a successful exit, and an aliased path ITEM fell into\n * the wholesale-replace branch and deleted every living operation on that path\n * that the alias did not restate. Aliases are legal OpenAPI and the natural way\n * to write a delta that repeats a shape; nothing here may treat them as absent.\n * An alias that cannot be resolved at all is a document loam cannot read, and\n * says so — it is never \"nothing to merge\".\n */\nexport function mergeOpenapiPaths(\n livingText: string,\n featureText: string,\n service: string,\n): OpenapiMergeResult {\n const feature = parseDocument(featureText);\n if (feature.errors.length > 0) {\n throw new OpenapiMergeError(\"feature\", service, feature.errors[0]!.message);\n }\n let featPlain: unknown;\n try {\n // Resolve aliases once with the document's own anchor context. Calling an\n // individual AST node's toJSON() loses that context and can silently turn\n // an aliased operation or component into the wrong value.\n featPlain = feature.toJS() ?? {};\n } catch (error) {\n throw new OpenapiMergeError(\"feature\", service, errorMessage(error));\n }\n const featPathsPlain = plainChild(featPlain, \"paths\");\n if (featPathsPlain === undefined || featPathsPlain === null) {\n return noop();\n }\n if (!isRecord(featPathsPlain)) {\n throw new OpenapiMergeError(\"feature\", service, \"`paths` is not a mapping\");\n }\n const featPathEntries = Object.entries(featPathsPlain);\n if (featPathEntries.length === 0) return noop();\n\n const living = parseDocument(livingText);\n if (living.errors.length > 0) {\n throw new OpenapiMergeError(\"living\", service, living.errors[0]!.message);\n }\n let livingPlain: unknown;\n try {\n livingPlain = living.toJS() ?? {};\n } catch (error) {\n throw new OpenapiMergeError(\"living\", service, errorMessage(error));\n }\n const livingPathsPlain = plainChild(livingPlain, \"paths\");\n if (livingPathsPlain !== undefined && livingPathsPlain !== null && !isRecord(livingPathsPlain)) {\n throw new OpenapiMergeError(\"living\", service, \"`paths` is not a mapping\");\n }\n const modified: string[] = [];\n const pathItemModified: string[] = [];\n const removed: string[] = [];\n const quoted: string[] = [];\n const baselineStale: string[] = [];\n for (const [path, featItemPlain] of featPathEntries) {\n const existing = living.getIn([\"paths\", path]);\n const existingPlain = plainChild(livingPathsPlain, path);\n if (existing !== undefined && !isRecord(existingPlain)) {\n // The living contract holds this path as something other than a path\n // item. Replacing it wholesale is the one branch that may delete living\n // operations without naming them, so it is reserved for paths the living\n // contract does not have at all.\n throw new OpenapiMergeError(\"living\", service, `path '${path}' is not a mapping`);\n }\n if (existing !== undefined) {\n if (!isRecord(featItemPlain)) {\n throw new OpenapiMergeError(\"feature\", service, `path '${path}' is not a mapping`);\n }\n // EVERY branch publishes through withoutFeatureMarkers. This one used to\n // copy every key of the feature path item and guard only the HTTP\n // methods, so a path-level `x-loam-remove` landed in the living contract\n // with `modified: []` and no warning. A key the strip drops is never\n // written; the removal handling below still reads the RAW item, because\n // that is where the markers it acts on live.\n const publish = withoutFeatureMarkers(featItemPlain);\n const publishable = isRecord(publish) ? publish : {};\n for (const [m, afterPlain] of Object.entries(featItemPlain)) {\n const before = living.getIn([\"paths\", path, m]);\n const beforePlain = plainChild(existingPlain, m);\n if (HTTP_METHODS.has(m) && isRemoval(afterPlain)) {\n // Coherence validates the marker and gates absent/mismatched targets.\n // The merge remains defensive under --approve: never delete a\n // different operation merely because it occupies the requested slot.\n if (\n before !== undefined &&\n operationIdOf(beforePlain) !== undefined &&\n operationIdOf(beforePlain) === operationIdOf(afterPlain)\n ) {\n removed.push(opLabel(beforePlain, afterPlain, m, path));\n living.deleteIn([\"paths\", path, m]);\n }\n continue;\n }\n if (HTTP_METHODS.has(m)) {\n const verdict = classifyOperationBaseline(afterPlain, before === undefined ? undefined : beforePlain);\n // A QUOTE is not a merge input. The author wrote this operation down\n // because the document is complete, not because they changed it, so\n // the living contract keeps whatever it holds — including a change\n // that landed after this delta was written. This is mechanical, not a\n // judgement, so `--approve` does not turn it back on: overriding a\n // gate is a decision, reverting an operation nobody edited is a bug.\n if (verdict === \"quote\") {\n quoted.push(opLabel(beforePlain, afterPlain, m, path));\n continue;\n }\n if (verdict === \"stale\") baselineStale.push(opLabel(beforePlain, afterPlain, m, path));\n }\n if (!(m in publishable)) {\n // A feature-only marker at path level. It is never published, and\n // never reported as a path-item overwrite either — it is not a key of\n // the contract at all. The archive gate names it to the author.\n continue;\n }\n // The difference check covers EVERY key of the path item; only the\n // LABEL depends on whether the key is an HTTP method.\n if (before !== undefined && !isDeepStrictEqual(beforePlain, afterPlain)) {\n if (HTTP_METHODS.has(m)) modified.push(opLabel(beforePlain, afterPlain, m, path));\n else pathItemModified.push(`'${m}' (${path})`);\n }\n // Without the strip the living contract would grow a pin to a version\n // of itself, and the NEXT feature's baseline would hash it.\n living.setIn([\"paths\", path, m], publishable[m]);\n }\n // Removing the last method leaves `\\/x: {}` — a path the contract still\n // advertises and nothing answers. The same cleanup\n // stripOpenapiRemovalMarkers already does on the feature side.\n const remaining = living.getIn([\"paths\", path]);\n if (isMap(remaining) && remaining.items.length === 0) living.deleteIn([\"paths\", path]);\n continue;\n }\n if (!isRecord(featItemPlain)) {\n throw new OpenapiMergeError(\"feature\", service, `path '${path}' is not a mapping`);\n }\n const clean = withoutFeatureMarkers(featItemPlain);\n if (clean !== undefined) living.setIn([\"paths\", path], clean);\n }\n\n const componentsModified: string[] = [];\n const unresolved: OpenapiMergeResult[\"unresolved\"] = [];\n const visited = new Set<string>();\n const copies: Array<{ kind: string; name: string; value: unknown }> = [];\n\n const visitRef = (ref: string, from: string): void => {\n if (!ref.startsWith(\"#/\")) return;\n const match = /^#\\/components\\/([^/]+)\\/([^/]+)(?:\\/|$)/.exec(ref);\n if (!match) {\n if (!resolvePointer(featPlain, ref).found && !resolvePointer(livingPlain, ref).found) {\n unresolved.push({ ref, from });\n }\n return;\n }\n const kind = match[1]!;\n const name = match[2]!;\n const key = `${kind}/${name}`;\n if (visited.has(key)) return;\n visited.add(key);\n const inFeature = resolvePointer(featPlain, `#/components/${kind}/${name}`);\n if (inFeature.found) {\n copies.push({ kind, name, value: inFeature.value });\n walk(inFeature.value, `components/${key}`);\n return;\n }\n if (!resolvePointer(livingPlain, `#/components/${kind}/${name}`).found) {\n unresolved.push({ ref, from });\n }\n };\n\n const walk = (node: unknown, from: string): void => {\n for (const ref of collectRefs(node)) visitRef(ref, from);\n };\n\n for (const [path, featItemPlain] of featPathEntries) {\n walk(withoutFeatureMarkers(featItemPlain), `paths ${path}`);\n }\n\n for (const { kind, name, value } of copies) {\n const existing = living.getIn([\"components\", kind, name]);\n if (existing !== undefined) {\n const existingPlain = resolvePointer(livingPlain, `#/components/${kind}/${name}`);\n if (existingPlain.found && isDeepStrictEqual(existingPlain.value, value)) continue;\n componentsModified.push(`${kind}/${name}`);\n }\n living.setIn([\"components\", kind, name], value);\n }\n\n let text: string;\n try {\n text = living.toString();\n } catch (error) {\n throw new OpenapiMergeError(\"living\", service, errorMessage(error));\n }\n return { text, modified, pathItemModified, removed, quoted, baselineStale, componentsModified, unresolved };\n}\n\n/** The successful \"the feature document has nothing to merge\" answer. */\nfunction noop(): OpenapiMergeResult {\n return {\n text: null,\n modified: [],\n pathItemModified: [],\n removed: [],\n quoted: [],\n baselineStale: [],\n componentsModified: [],\n unresolved: [],\n };\n}\n\n/** A resolved plain tree that can hold OpenAPI keys — an object, not an array. */\nfunction isRecord(node: unknown): node is Record<string, unknown> {\n return node !== null && typeof node === \"object\" && !Array.isArray(node);\n}\n\n/** Every `$ref` string value anywhere in a plain JS tree, in document order. */\nexport function collectRefs(node: unknown): string[] {\n const out: string[] = [];\n const walk = (current: unknown): void => {\n if (Array.isArray(current)) {\n for (const value of current) walk(value);\n return;\n }\n if (current !== null && typeof current === \"object\") {\n for (const [key, value] of Object.entries(current)) {\n if (key === \"$ref\" && typeof value === \"string\") out.push(value);\n else walk(value);\n }\n }\n };\n walk(node);\n return out;\n}\n\n/** Resolve a local JSON pointer (`#/a/b~1c`) against a plain JS tree. */\nexport function resolvePointer(root: unknown, ref: string): { found: boolean; value: unknown } {\n let current: unknown = root;\n for (const raw of ref.slice(2).split(\"/\")) {\n const segment = raw.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n if (Array.isArray(current)) {\n const index = Number(segment);\n if (!Number.isInteger(index) || index < 0 || index >= current.length) {\n return { found: false, value: undefined };\n }\n current = current[index];\n } else if (current !== null && typeof current === \"object\" && segment in (current as Record<string, unknown>)) {\n current = (current as Record<string, unknown>)[segment];\n } else {\n return { found: false, value: undefined };\n }\n }\n return { found: true, value: current };\n}\n\n/** Name an overwritten operation by its operationId (feature's, else living's), or path+method. */\nexport function opLabel(before: unknown, after: unknown, method: string, path: string): string {\n const operation = operationIdOf(after) ?? operationIdOf(before);\n return operation !== undefined ? `'${operation}' (${method} ${path})` : `${method} ${path}`;\n}\n\n/** Is this operation node a feature-only explicit removal marker? */\nfunction isRemoval(node: unknown): boolean {\n return node !== null &&\n typeof node === \"object\" &&\n (node as Record<string, unknown>)[\"x-loam-remove\"] === true;\n}\n\n/** Read an operationId from a YAML or plain operation node. */\nfunction operationIdOf(node: unknown): string | undefined {\n const value = isMap(node)\n ? node.get(\"operationId\")\n : node !== null && typeof node === \"object\"\n ? (node as Record<string, unknown>)[\"operationId\"]\n : undefined;\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/**\n * Every key that is an instruction to loam rather than part of the contract.\n * At PATH level neither has any meaning — `x-loam-remove` addresses one\n * operation and `x-loam-based-on` pins one — so a path item carrying either is\n * an authoring mistake, and one that must not be published whatever else is\n * done about it.\n */\nconst FEATURE_ONLY_KEYS = new Set([\"x-loam-remove\", OPENAPI_BASELINE_KEY]);\n\n/**\n * Copy a path item without anything that belongs to the FEATURE rather than to\n * the contract: removal-marker operations, the `x-loam-based-on` pins on the\n * operations that survive, and a loam marker written at PATH level. All three\n * are instructions to loam, and a living contract that carries any of them is\n * lying to the next reader — and, for the pin, to the next feature's baseline.\n *\n * The path-level case used to fall through: the filter guarded only HTTP\n * methods, so `x-loam-remove: true` beside the methods was copied verbatim into\n * the fleet's living contract, where it also kept the empty-path cleanup from\n * ever seeing an empty path.\n */\nfunction withoutFeatureMarkers(node: unknown): unknown {\n if (node === null || typeof node !== \"object\" || Array.isArray(node)) return node;\n const entries = Object.entries(node as Record<string, unknown>)\n .filter(([key, value]) => !FEATURE_ONLY_KEYS.has(key) && !(HTTP_METHODS.has(key) && isRemoval(value)))\n .map(([key, value]) => [key, HTTP_METHODS.has(key) ? withoutOperationBaseline(value) : value] as const);\n return entries.length === 0 ? undefined : Object.fromEntries(entries);\n}\n\nfunction plainChild(parent: unknown, key: string): unknown {\n return parent !== null && typeof parent === \"object\"\n ? (parent as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|