@wairon/cli 5.1.1-dev.112 → 5.1.1-dev.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2062 -374
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +252 -11
- package/dist/index.js +1915 -345
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -1
- package/dist/templates/skills/sdd-delegate.md +7 -0
- package/dist/templates/skills/sdd-implement.md +8 -0
- package/docs/cli.md +142 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3115,8 +3115,8 @@ var require_dist = __commonJS({
|
|
|
3115
3115
|
walkPackDir(root, path52.join(current2, entry.name), out);
|
|
3116
3116
|
} else if (entry.isFile()) {
|
|
3117
3117
|
const absolute = path52.join(current2, entry.name);
|
|
3118
|
-
const
|
|
3119
|
-
out.push({ path:
|
|
3118
|
+
const relative27 = path52.relative(root, absolute).split(path52.sep).join("/");
|
|
3119
|
+
out.push({ path: relative27, contents: fs33.readFileSync(absolute) });
|
|
3120
3120
|
}
|
|
3121
3121
|
}
|
|
3122
3122
|
}
|
|
@@ -3640,6 +3640,7 @@ __export(src_exports, {
|
|
|
3640
3640
|
createEmptyTopologyConfig: () => createEmptyTopologyConfig,
|
|
3641
3641
|
createMember: () => createMember,
|
|
3642
3642
|
createProjectConfig: () => createProjectConfig,
|
|
3643
|
+
crossProjectWriteRefusal: () => crossProjectWriteRefusal,
|
|
3643
3644
|
currentRootBinding: () => currentRootBinding,
|
|
3644
3645
|
declareExternal: () => declareExternal,
|
|
3645
3646
|
declareMember: () => declareMember,
|
|
@@ -3809,6 +3810,7 @@ __export(src_exports, {
|
|
|
3809
3810
|
moveMember: () => moveMember,
|
|
3810
3811
|
moveMethods: () => moveMethods,
|
|
3811
3812
|
moveMountToMembers: () => moveMountToMembers,
|
|
3813
|
+
moveSpec: () => moveSpec,
|
|
3812
3814
|
nameKey: () => nameKey,
|
|
3813
3815
|
namesAnInteger: () => namesAnInteger,
|
|
3814
3816
|
narrativeStepForeignFields: () => narrativeStepForeignFields,
|
|
@@ -3838,6 +3840,7 @@ __export(src_exports, {
|
|
|
3838
3840
|
projectIdentity: () => projectIdentity,
|
|
3839
3841
|
promoteMember: () => promoteMember,
|
|
3840
3842
|
provisionProject: () => provisionProject,
|
|
3843
|
+
publishedUsesOf: () => publishedUsesOf,
|
|
3841
3844
|
qualifiedTypeId: () => qualifiedTypeId,
|
|
3842
3845
|
rangeProblem: () => rangeProblem,
|
|
3843
3846
|
readCodeAnnotation: () => readCodeAnnotation,
|
|
@@ -3867,6 +3870,7 @@ __export(src_exports, {
|
|
|
3867
3870
|
renameId: () => renameProjectId,
|
|
3868
3871
|
renameMethod: () => renameMethod,
|
|
3869
3872
|
renameParam: () => renameParam,
|
|
3873
|
+
renameSpecId: () => renameSpecId,
|
|
3870
3874
|
renameType: () => renameType,
|
|
3871
3875
|
renderDiagram: () => renderDiagram2,
|
|
3872
3876
|
repairForeignStepFields: () => repairForeignStepFields,
|
|
@@ -3896,6 +3900,7 @@ __export(src_exports, {
|
|
|
3896
3900
|
retiredReachFacts: () => retiredReachFacts,
|
|
3897
3901
|
returnTypeOf: () => returnTypeOf,
|
|
3898
3902
|
rewriteReferences: () => rewriteReferences2,
|
|
3903
|
+
routerLinkage: () => routerLinkage,
|
|
3899
3904
|
runWithHostedLookup: () => runWithHostedLookup,
|
|
3900
3905
|
runWithProjectBinding: () => runWithProjectBinding,
|
|
3901
3906
|
runWithProjectRoot: () => runWithProjectRoot,
|
|
@@ -3916,6 +3921,7 @@ __export(src_exports, {
|
|
|
3916
3921
|
settledSpecPaths: () => settledSpecPaths,
|
|
3917
3922
|
signatureFacts: () => signatureFacts,
|
|
3918
3923
|
signatureTypeRefs: () => signatureTypeRefs,
|
|
3924
|
+
specIndexMethodReferences: () => specIndexMethodReferences,
|
|
3919
3925
|
specIndexReferencesTo: () => specIndexReferencesTo,
|
|
3920
3926
|
specIndexRetiredBy: () => specIndexRetiredBy,
|
|
3921
3927
|
specIndexSubtreeOf: () => specIndexSubtreeOf,
|
|
@@ -4022,22 +4028,22 @@ function sampleOf(file) {
|
|
|
4022
4028
|
if (fd !== void 0) fs.closeSync(fd);
|
|
4023
4029
|
}
|
|
4024
4030
|
}
|
|
4025
|
-
function attributeMatches(pattern,
|
|
4031
|
+
function attributeMatches(pattern, relative27) {
|
|
4026
4032
|
const glob = pattern.replace(/^\//, "");
|
|
4027
|
-
const target = glob.includes("/") ?
|
|
4033
|
+
const target = glob.includes("/") ? relative27 : path.posix.basename(relative27);
|
|
4028
4034
|
const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
|
|
4029
4035
|
return new RegExp(`^${source}$`).test(target);
|
|
4030
4036
|
}
|
|
4031
4037
|
function attributeEol(attributesFile, file) {
|
|
4032
4038
|
const text3 = readFileOrNull(attributesFile);
|
|
4033
4039
|
if (text3 === null) return null;
|
|
4034
|
-
const
|
|
4040
|
+
const relative27 = path.relative(path.dirname(attributesFile), file).split(path.sep).join("/");
|
|
4035
4041
|
let eol = null;
|
|
4036
4042
|
for (const raw of text3.split(/\r?\n/)) {
|
|
4037
4043
|
const line = raw.trim();
|
|
4038
4044
|
if (line === "" || line.startsWith("#")) continue;
|
|
4039
4045
|
const [pattern, ...attrs] = line.split(/\s+/);
|
|
4040
|
-
if (!attributeMatches(pattern,
|
|
4046
|
+
if (!attributeMatches(pattern, relative27)) continue;
|
|
4041
4047
|
for (const attr of attrs) {
|
|
4042
4048
|
if (attr === "eol=crlf") eol = "\r\n";
|
|
4043
4049
|
else if (attr === "eol=lf") eol = "\n";
|
|
@@ -4318,7 +4324,7 @@ function defaultTargetConfig(type) {
|
|
|
4318
4324
|
enabled: true
|
|
4319
4325
|
};
|
|
4320
4326
|
}
|
|
4321
|
-
var WAIRON_VERSION = "5.1.1-dev.
|
|
4327
|
+
var WAIRON_VERSION = "5.1.1-dev.113";
|
|
4322
4328
|
var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
4323
4329
|
var ARCHITECT_AGENT_ID = "agent-architect";
|
|
4324
4330
|
var ARCHITECT_TEMPLATE_ID = "architect";
|
|
@@ -7489,9 +7495,9 @@ var RawParser = class {
|
|
|
7489
7495
|
}
|
|
7490
7496
|
name += this.ident();
|
|
7491
7497
|
while (this.isP("::") || this.isP(".")) {
|
|
7492
|
-
const
|
|
7498
|
+
const sep18 = this.peek().v;
|
|
7493
7499
|
this.at++;
|
|
7494
|
-
name +=
|
|
7500
|
+
name += sep18 + this.ident();
|
|
7495
7501
|
}
|
|
7496
7502
|
if (!this.isP("<")) return { k: "name", name };
|
|
7497
7503
|
this.at++;
|
|
@@ -8690,7 +8696,10 @@ function conceptNoun(component) {
|
|
|
8690
8696
|
}
|
|
8691
8697
|
var HttpMethodSchema = import_zod5.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]);
|
|
8692
8698
|
var EndpointSchema = import_zod5.z.discriminatedUnion("transport", [
|
|
8693
|
-
|
|
8699
|
+
// `status`: the success code the operation answers with when it is not the
|
|
8700
|
+
// verb's convention (202 Accepted, a 3xx redirect, a 201 for a workflow verb
|
|
8701
|
+
// that files something) — the design's own word, which the OpenAPI export uses.
|
|
8702
|
+
import_zod5.z.object({ transport: import_zod5.z.literal("HTTP"), method: HttpMethodSchema, path: import_zod5.z.string(), status: import_zod5.z.number().int().min(200).max(399).optional() }),
|
|
8694
8703
|
import_zod5.z.object({ transport: import_zod5.z.literal("gRPC"), service: import_zod5.z.string(), method: import_zod5.z.string() }),
|
|
8695
8704
|
import_zod5.z.object({ transport: import_zod5.z.literal("GraphQL"), operation: import_zod5.z.enum(["query", "mutation", "subscription"]), field: import_zod5.z.string() }),
|
|
8696
8705
|
import_zod5.z.object({ transport: import_zod5.z.literal("MessageBus"), topic: import_zod5.z.string(), event: import_zod5.z.string(), queue: import_zod5.z.string().optional(), direction: import_zod5.z.enum(["subscribe", "publish"]).default("subscribe") }),
|
|
@@ -10033,6 +10042,49 @@ function specIndexReferencesTo(index, targets, system) {
|
|
|
10033
10042
|
}
|
|
10034
10043
|
return out;
|
|
10035
10044
|
}
|
|
10045
|
+
function specIndexMethodReferences(index, component, methods) {
|
|
10046
|
+
const named2 = new Set(methods);
|
|
10047
|
+
const contracts = index.interfaces.filter((i) => i.component === component);
|
|
10048
|
+
const target = (method2) => {
|
|
10049
|
+
const holder = contracts.find((i) => i.methods.some((m) => m.name === method2));
|
|
10050
|
+
return holder ? { kind: "interface", id: holder.id } : { kind: "component", id: component };
|
|
10051
|
+
};
|
|
10052
|
+
const hits = (comp, method2) => comp === component && method2 !== void 0 && named2.has(method2);
|
|
10053
|
+
const out = [];
|
|
10054
|
+
for (const sub of index.subsystems) {
|
|
10055
|
+
for (const entry of sub.lifecycle ?? []) {
|
|
10056
|
+
if (hits(entry.component, entry.method)) out.push({ kind: "subsystem", id: sub.id, position: "lifecycle", target: target(entry.method) });
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
10059
|
+
for (const comp of index.components) {
|
|
10060
|
+
for (const binding of comp.dispatch ?? []) {
|
|
10061
|
+
if (hits(binding.component, binding.method)) out.push({ kind: "component", id: comp.id, position: "dispatch", target: target(binding.method) });
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
for (const intf of index.interfaces) {
|
|
10065
|
+
for (const method2 of intf.methods) {
|
|
10066
|
+
const source = method2.signatureFrom;
|
|
10067
|
+
const cut = source?.lastIndexOf(".") ?? -1;
|
|
10068
|
+
if (source === void 0 || cut <= 0) continue;
|
|
10069
|
+
const name = source.slice(cut + 1);
|
|
10070
|
+
if (hits(source.slice(0, cut), name)) out.push({ kind: "interface", id: intf.id, position: `methods.${method2.name}.signatureFrom`, target: target(name) });
|
|
10071
|
+
}
|
|
10072
|
+
}
|
|
10073
|
+
for (const impl of index.implementations) {
|
|
10074
|
+
for (const method2 of impl.methods) {
|
|
10075
|
+
for (const step of method2.narrative ?? []) {
|
|
10076
|
+
if (hits(step.targetComponent, step.targetMethod)) {
|
|
10077
|
+
out.push({ kind: "implementation", id: impl.id, position: `methods.${method2.name}.narrative.step ${step.stepNumber}`, target: target(step.targetMethod) });
|
|
10078
|
+
}
|
|
10079
|
+
}
|
|
10080
|
+
for (const entry of method2.calls ?? []) {
|
|
10081
|
+
const call = parseDeclaredCall(entry);
|
|
10082
|
+
if (call && hits(call.compId, call.methodName)) out.push({ kind: "implementation", id: impl.id, position: `methods.${method2.name}.calls`, target: target(call.methodName) });
|
|
10083
|
+
}
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
return out;
|
|
10087
|
+
}
|
|
10036
10088
|
|
|
10037
10089
|
// src/utils/canonical-json.ts
|
|
10038
10090
|
function canonicalize(value) {
|
|
@@ -10184,6 +10236,10 @@ function parseLockRecord(text3, file) {
|
|
|
10184
10236
|
} catch (e) {
|
|
10185
10237
|
throw new LockRecordUnreadableError(file, `it is not valid JSON (${e.message})`);
|
|
10186
10238
|
}
|
|
10239
|
+
const duplicate = firstDuplicateKey(body);
|
|
10240
|
+
if (duplicate !== null) {
|
|
10241
|
+
throw new LockRecordUnreadableError(file, `it names the key "${duplicate}" twice (a conflict resolved by hand leaves this), so which of the two is the record cannot be told`);
|
|
10242
|
+
}
|
|
10187
10243
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
10188
10244
|
const what = raw === null ? "null" : Array.isArray(raw) ? "an array" : `a ${typeof raw}`;
|
|
10189
10245
|
throw new LockRecordUnreadableError(file, `it holds ${what}, not a lock record object`);
|
|
@@ -10204,6 +10260,37 @@ function parseLockRecord(text3, file) {
|
|
|
10204
10260
|
}
|
|
10205
10261
|
return normalizeRecord(record);
|
|
10206
10262
|
}
|
|
10263
|
+
function firstDuplicateKey(text3) {
|
|
10264
|
+
const stack = [];
|
|
10265
|
+
const pathOf = (frame, key2) => frame && frame.path ? `${frame.path}.${key2}` : key2;
|
|
10266
|
+
for (let i = 0; i < text3.length; i++) {
|
|
10267
|
+
const c = text3[i];
|
|
10268
|
+
if (c === '"') {
|
|
10269
|
+
let j = i + 1;
|
|
10270
|
+
while (j < text3.length && text3[j] !== '"') j += text3[j] === "\\" ? 2 : 1;
|
|
10271
|
+
const top = stack[stack.length - 1];
|
|
10272
|
+
if (top && top.keys && top.expectKey) {
|
|
10273
|
+
const key2 = JSON.parse(text3.slice(i, j + 1));
|
|
10274
|
+
if (top.keys.has(key2)) return pathOf(top, key2);
|
|
10275
|
+
top.keys.add(key2);
|
|
10276
|
+
top.lastKey = key2;
|
|
10277
|
+
top.expectKey = false;
|
|
10278
|
+
}
|
|
10279
|
+
i = j;
|
|
10280
|
+
} else if (c === "{" || c === "[") {
|
|
10281
|
+
const top = stack[stack.length - 1];
|
|
10282
|
+
const segment = top ? top.keys ? top.lastKey ?? "" : "[]" : "";
|
|
10283
|
+
const path52 = top && segment ? pathOf(top, segment) : top?.path ?? "";
|
|
10284
|
+
stack.push({ keys: c === "{" ? /* @__PURE__ */ new Set() : null, expectKey: c === "{", path: path52 });
|
|
10285
|
+
} else if (c === "}" || c === "]") {
|
|
10286
|
+
stack.pop();
|
|
10287
|
+
} else if (c === ",") {
|
|
10288
|
+
const top = stack[stack.length - 1];
|
|
10289
|
+
if (top && top.keys) top.expectKey = true;
|
|
10290
|
+
}
|
|
10291
|
+
}
|
|
10292
|
+
return null;
|
|
10293
|
+
}
|
|
10207
10294
|
function displayPath2(file) {
|
|
10208
10295
|
const rel2 = path9.relative(process.cwd(), file);
|
|
10209
10296
|
const shown2 = rel2 && !rel2.startsWith("..") && !path9.isAbsolute(rel2) ? rel2 : file;
|
|
@@ -10859,6 +10946,12 @@ var EXTENSION_LANGUAGE = {
|
|
|
10859
10946
|
".kt": "kotlin",
|
|
10860
10947
|
".swift": "swift"
|
|
10861
10948
|
};
|
|
10949
|
+
function routerLinkage(router) {
|
|
10950
|
+
const hash = router.lastIndexOf("#");
|
|
10951
|
+
if (hash > 0) return { file: router.slice(0, hash), ...hash < router.length - 1 ? { name: router.slice(hash + 1) } : {} };
|
|
10952
|
+
if (router.includes("/") || EXTENSION_LANGUAGE[path11.extname(router).toLowerCase()] !== void 0) return { file: router };
|
|
10953
|
+
return { name: router };
|
|
10954
|
+
}
|
|
10862
10955
|
function languageOfSourcePath(sourcePath) {
|
|
10863
10956
|
return EXTENSION_LANGUAGE[path11.extname(sourcePath).toLowerCase()];
|
|
10864
10957
|
}
|
|
@@ -11109,6 +11202,12 @@ function fieldRenamesSince(pinned, live, publicName, member, pinnedDigest) {
|
|
|
11109
11202
|
const back = { ...live, types: live.types.map((d) => undone.get(d.id) ?? d) };
|
|
11110
11203
|
return { renames, renameOnly: memberDigest(back, publicName, member) === pinnedDigest };
|
|
11111
11204
|
}
|
|
11205
|
+
function shownEndpoint(endpoint) {
|
|
11206
|
+
if (!endpoint) return "none";
|
|
11207
|
+
if (endpoint.transport === "HTTP") return `${endpoint.method} ${endpoint.path}${endpoint.status !== void 0 ? ` (answers ${endpoint.status})` : ""}`;
|
|
11208
|
+
const { transport, ...address } = endpoint;
|
|
11209
|
+
return `${String(transport)} ${Object.entries(address).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${String(v)}`).join(" ")}`;
|
|
11210
|
+
}
|
|
11112
11211
|
function shownFact(value) {
|
|
11113
11212
|
return value === void 0 || value === null || value === "" ? "none" : typeof value === "string" ? value : canonicalize(value);
|
|
11114
11213
|
}
|
|
@@ -11278,6 +11377,7 @@ function surfaceChanges(newer, older) {
|
|
|
11278
11377
|
}
|
|
11279
11378
|
const oldMethods = new Map(before.methods.map((m) => [m.name, m]));
|
|
11280
11379
|
const usedOld = /* @__PURE__ */ new Set();
|
|
11380
|
+
const embeddedBy = /* @__PURE__ */ new Map();
|
|
11281
11381
|
entry.methods.forEach((method2, methodIndex) => {
|
|
11282
11382
|
let was = oldMethods.get(method2.name);
|
|
11283
11383
|
if (!was) {
|
|
@@ -11301,7 +11401,10 @@ function surfaceChanges(newer, older) {
|
|
|
11301
11401
|
} else if (a !== b) {
|
|
11302
11402
|
const exported = new Set((newer.exportedTypes ?? []).map((t) => t.type));
|
|
11303
11403
|
const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id)), notes);
|
|
11304
|
-
if (embedded.length > 0)
|
|
11404
|
+
if (embedded.length > 0) {
|
|
11405
|
+
const key2 = embedded.join("; ");
|
|
11406
|
+
embeddedBy.set(key2, [...embeddedBy.get(key2) ?? [], method2.name]);
|
|
11407
|
+
}
|
|
11305
11408
|
}
|
|
11306
11409
|
for (const r of paramRenames(was, method2)) {
|
|
11307
11410
|
out.push({ kind: "renamed", name: entry.id, member: method2.name, from: r.from, detail: `parameter "${r.from}" renamed to "${r.to}"` });
|
|
@@ -11309,7 +11412,13 @@ function surfaceChanges(newer, older) {
|
|
|
11309
11412
|
if (factValue(was.effect) !== factValue(method2.effect)) {
|
|
11310
11413
|
out.push({ kind: "changed", name: entry.id, member: method2.name, detail: `effect ${shownEffect(was.effect)} \u2192 ${shownEffect(method2.effect)}` });
|
|
11311
11414
|
}
|
|
11415
|
+
if (factValue(was.endpoint) !== factValue(method2.endpoint)) {
|
|
11416
|
+
out.push({ kind: "changed", name: entry.id, member: method2.name, detail: `endpoint ${shownEndpoint(was.endpoint)} \u2192 ${shownEndpoint(method2.endpoint)}` });
|
|
11417
|
+
}
|
|
11312
11418
|
});
|
|
11419
|
+
for (const [renames, methods] of embeddedBy) {
|
|
11420
|
+
out.push(methods.length === 1 ? { kind: "changed", name: entry.id, member: methods[0], detail: `signature reads the same, but it names renamed types: ${renames}` } : { kind: "changed", name: entry.id, detail: `${methods.length} methods (${methods.join(", ")}): signatures read the same, but they name renamed types: ${renames}` });
|
|
11421
|
+
}
|
|
11313
11422
|
for (const m of before.methods) {
|
|
11314
11423
|
if (!usedOld.has(m.name)) out.push({ kind: "removed", name: entry.id, member: m.name, detail: `method removed: ${shownSignature(m)}` });
|
|
11315
11424
|
}
|
|
@@ -11354,7 +11463,14 @@ function surfaceChanges(newer, older) {
|
|
|
11354
11463
|
for (const t of older.exportedTypes ?? []) {
|
|
11355
11464
|
if (!(newer.exportedTypes ?? []).some((x) => x.id === t.id) && !consumed.has(t.id)) out.push({ kind: "removed", name: t.id, detail: "type removed" });
|
|
11356
11465
|
}
|
|
11357
|
-
|
|
11466
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11467
|
+
const unique2 = out.filter((c) => {
|
|
11468
|
+
const key2 = JSON.stringify([c.kind, c.name, c.member ?? "", c.from ?? "", c.detail]);
|
|
11469
|
+
if (seen.has(key2)) return false;
|
|
11470
|
+
seen.add(key2);
|
|
11471
|
+
return true;
|
|
11472
|
+
});
|
|
11473
|
+
return unique2.sort((x, y) => x.name === y.name ? (x.member ?? "") < (y.member ?? "") ? -1 : (x.member ?? "") > (y.member ?? "") ? 1 : 0 : x.name < y.name ? -1 : 1);
|
|
11358
11474
|
}
|
|
11359
11475
|
|
|
11360
11476
|
// src/models/exports.ts
|
|
@@ -11392,6 +11508,14 @@ function narrowedToUses(consumer, published) {
|
|
|
11392
11508
|
}
|
|
11393
11509
|
}
|
|
11394
11510
|
}
|
|
11511
|
+
if (use.kind === "component") {
|
|
11512
|
+
for (const name of names) {
|
|
11513
|
+
for (const u of consumer.uses ?? []) {
|
|
11514
|
+
if (u.publicName !== name || u.members.length > 0) continue;
|
|
11515
|
+
add2({ publicName: name, kind: u.kind, members: [...use.members].sort(), ...u.specs ? { specs: [...u.specs] } : {} });
|
|
11516
|
+
}
|
|
11517
|
+
}
|
|
11518
|
+
}
|
|
11395
11519
|
for (const former of use.formerNames ?? []) {
|
|
11396
11520
|
if ((consumer.broken ?? []).includes(former) && !merged.has(former)) add2({ publicName: former, kind: use.kind, members: use.kind === "type" ? ["type"] : [] });
|
|
11397
11521
|
}
|
|
@@ -12317,12 +12441,15 @@ function generateExcalidrawScene(model) {
|
|
|
12317
12441
|
// src/core/canvas.ts
|
|
12318
12442
|
function memberProjectNodes() {
|
|
12319
12443
|
const family = graph();
|
|
12444
|
+
const top = family.nodes.find((n) => n.namespace === "")?.directory;
|
|
12445
|
+
const sibling = (dir) => top !== void 0 && path13.relative(top, dir).startsWith("..");
|
|
12320
12446
|
const contained = family.nodes.filter((n) => n.namespace !== "").map((n) => ({
|
|
12321
12447
|
id: n.namespace,
|
|
12322
12448
|
name: n.mountAlias ?? n.namespace,
|
|
12323
12449
|
description: n.memberDescription ?? n.name ?? "",
|
|
12324
12450
|
trustedLinks: [],
|
|
12325
|
-
project: true
|
|
12451
|
+
project: true,
|
|
12452
|
+
...sibling(n.directory) ? { storage: storageBadge("path", n.directory, void 0) } : {}
|
|
12326
12453
|
}));
|
|
12327
12454
|
const rootExternals = family.nodes.find((n) => n.namespace === "")?.externals ?? [];
|
|
12328
12455
|
const referenced = rootExternals.filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
|
|
@@ -17225,40 +17352,64 @@ function idHit(name, ids) {
|
|
|
17225
17352
|
const local = name.split(/::|\./).pop().toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
17226
17353
|
return [...ids].find((id) => id.toLowerCase() === local);
|
|
17227
17354
|
}
|
|
17228
|
-
function
|
|
17355
|
+
function componentKey(name) {
|
|
17356
|
+
return name.replace(/::/g, ".").replace(/[^A-Za-z0-9._-]/g, "_");
|
|
17357
|
+
}
|
|
17358
|
+
function projectIdentity2(surface) {
|
|
17359
|
+
return surface.projectId ?? surface.projectName;
|
|
17360
|
+
}
|
|
17361
|
+
function schemaScope(ownDefs, externals, registry, alias) {
|
|
17229
17362
|
const ownIds = ownDefs.map((t) => t.id);
|
|
17230
17363
|
const externalId = (pinned, name) => {
|
|
17231
17364
|
const exported = (pinned.exportedTypes ?? []).find((t) => t.id === name || t.id.toLowerCase() === name.toLowerCase());
|
|
17232
|
-
|
|
17365
|
+
if (exported && pinned.types.some((t) => t.id === exported.type)) return exported.type;
|
|
17366
|
+
if (pinned.types.some((t) => t.id === name)) return name;
|
|
17367
|
+
return idHit(name, pinned.types.map((t) => t.id));
|
|
17368
|
+
};
|
|
17369
|
+
const aliasOf = (identity2) => [...externals].find(([, s]) => projectIdentity2(s) === identity2)?.[0] ?? identity2;
|
|
17370
|
+
const record = (identity2, id, preferred, reached) => {
|
|
17371
|
+
const identityKey = `${identity2}\0${id}`;
|
|
17372
|
+
const held = registry.keyOf.get(identityKey);
|
|
17373
|
+
if (held !== void 0) return held;
|
|
17374
|
+
let key2 = componentKey(preferred);
|
|
17375
|
+
for (let n = 2; registry.reached.has(key2); n++) key2 = componentKey(`${preferred}_${n}`);
|
|
17376
|
+
registry.keyOf.set(identityKey, key2);
|
|
17377
|
+
registry.reached.set(key2, reached);
|
|
17378
|
+
return key2;
|
|
17233
17379
|
};
|
|
17234
17380
|
const external = (from, name) => {
|
|
17235
17381
|
const pinned = externals.get(from);
|
|
17236
17382
|
const id = pinned ? externalId(pinned, name) : void 0;
|
|
17237
17383
|
if (!pinned || id === void 0) return void 0;
|
|
17238
|
-
const
|
|
17239
|
-
|
|
17240
|
-
|
|
17384
|
+
const identity2 = projectIdentity2(pinned);
|
|
17385
|
+
const keyAlias = aliasOf(identity2);
|
|
17386
|
+
const surface = externals.get(keyAlias) ?? pinned;
|
|
17387
|
+
const def = surface.types.find((t) => t.id === id) ?? pinned.types.find((t) => t.id === id);
|
|
17388
|
+
return record(identity2, id, `${keyAlias}.${id}`, { alias: externals.has(keyAlias) ? keyAlias : from, def });
|
|
17241
17389
|
};
|
|
17390
|
+
const own2 = (id) => record("", id, id, { def: ownDefs.find((t) => t.id === id) });
|
|
17242
17391
|
return {
|
|
17243
17392
|
componentOf(name) {
|
|
17244
|
-
const
|
|
17245
|
-
if (
|
|
17393
|
+
const sep18 = name.indexOf("::");
|
|
17394
|
+
if (sep18 > 0 && externals.has(name.slice(0, sep18))) return external(name.slice(0, sep18), name.slice(sep18 + 2));
|
|
17395
|
+
if (alias === void 0 && ownIds.includes(name)) return own2(name);
|
|
17246
17396
|
if (alias !== void 0) return external(alias, name);
|
|
17247
|
-
|
|
17397
|
+
const hit = idHit(name, ownIds);
|
|
17398
|
+
return hit !== void 0 ? own2(hit) : void 0;
|
|
17248
17399
|
},
|
|
17249
17400
|
isObject(typeRef2) {
|
|
17250
17401
|
const expr = expressionOf(typeRef2);
|
|
17251
17402
|
if (!expr || expr.form !== "named" && expr.form !== "applied") return false;
|
|
17252
17403
|
const name = expr.name ?? "";
|
|
17253
|
-
const
|
|
17254
|
-
const from =
|
|
17404
|
+
const sep18 = name.indexOf("::");
|
|
17405
|
+
const from = sep18 > 0 && externals.has(name.slice(0, sep18)) ? name.slice(0, sep18) : alias;
|
|
17255
17406
|
const pinned = from !== void 0 ? externals.get(from) : void 0;
|
|
17256
|
-
const local =
|
|
17257
|
-
const def = pinned ? pinned.types.find((t) => t.id === externalId(pinned, local)) : ownDefs.find((t) => t.id === idHit(name, ownIds));
|
|
17407
|
+
const local = sep18 > 0 && from !== alias ? name.slice(sep18 + 2) : name;
|
|
17408
|
+
const def = pinned ? pinned.types.find((t) => t.id === externalId(pinned, local)) : ownDefs.find((t) => t.id === (ownIds.includes(name) ? name : idHit(name, ownIds)));
|
|
17258
17409
|
return !!def && def.kind !== "enum" && def.kind !== "signature" && def.holds === void 0;
|
|
17259
17410
|
},
|
|
17260
17411
|
within(next) {
|
|
17261
|
-
return schemaScope(ownDefs, externals,
|
|
17412
|
+
return schemaScope(ownDefs, externals, registry, next);
|
|
17262
17413
|
}
|
|
17263
17414
|
};
|
|
17264
17415
|
}
|
|
@@ -17317,14 +17468,30 @@ function creates(method2) {
|
|
|
17317
17468
|
if (method2.effect === "none" || method2.effect === "read") return false;
|
|
17318
17469
|
return CREATING_NAME.test(method2.name);
|
|
17319
17470
|
}
|
|
17471
|
+
var STATUS_TEXT = { "200": "Success", "201": "Created", "202": "Accepted", "203": "Success", "204": "Success", "206": "Partial content" };
|
|
17472
|
+
function conventionalStatus(method2, httpVerb) {
|
|
17473
|
+
const returns = method2.returns ?? "";
|
|
17474
|
+
if (!returns || returnsNothing(returns)) return 204;
|
|
17475
|
+
return httpVerb === "post" && creates(method2) ? 201 : 200;
|
|
17476
|
+
}
|
|
17477
|
+
function admitsNoValue(typeRef2) {
|
|
17478
|
+
return expressionOf(typeRef2)?.form === "optional";
|
|
17479
|
+
}
|
|
17480
|
+
function described(schema, description) {
|
|
17481
|
+
return description && schema.description === void 0 ? { ...schema, description } : schema;
|
|
17482
|
+
}
|
|
17320
17483
|
function responsesFor(method2, httpVerb, scope) {
|
|
17321
17484
|
const returns = method2.returns ?? "";
|
|
17322
17485
|
const nothing = !returns || returnsNothing(returns);
|
|
17323
|
-
const
|
|
17486
|
+
const stated = method2.endpoint?.transport === "HTTP" ? method2.endpoint.status : void 0;
|
|
17487
|
+
const code = String(stated ?? conventionalStatus(method2, httpVerb));
|
|
17488
|
+
const redirect = /^3/.test(code);
|
|
17489
|
+
const bodyless = nothing || redirect || code === "204";
|
|
17324
17490
|
const responses = {
|
|
17325
17491
|
[code]: {
|
|
17326
|
-
description: nothing ? "Success, with no content
|
|
17327
|
-
...
|
|
17492
|
+
description: redirect ? `Redirect${returns && !nothing ? `: the Location header is the ${returns} the method answers` : ""}` : nothing ? `${STATUS_TEXT[code] ?? "Success"}, with no content` : `${STATUS_TEXT[code] ?? "Success"}: ${returns}`,
|
|
17493
|
+
...redirect ? { headers: { Location: { description: "Where the client is sent", schema: { type: "string" } } } } : {},
|
|
17494
|
+
...bodyless ? {} : { content: { "application/json": { schema: schemaFor(returns, scope) } } }
|
|
17328
17495
|
}
|
|
17329
17496
|
};
|
|
17330
17497
|
const failure = failureOf(returns);
|
|
@@ -17340,14 +17507,14 @@ function enumDescription(values) {
|
|
|
17340
17507
|
return ["Values:", ...values.map((v) => `- \`${v.name}\`${v.description ? `: ${v.description}` : ""}`)].join("\n");
|
|
17341
17508
|
}
|
|
17342
17509
|
function enumValuesFrom(schema) {
|
|
17343
|
-
const
|
|
17510
|
+
const described2 = /* @__PURE__ */ new Map();
|
|
17344
17511
|
if (typeof schema.description === "string") {
|
|
17345
17512
|
for (const line of schema.description.split("\n")) {
|
|
17346
17513
|
const m = /^- `([^`]+)`: (.+)$/.exec(line.trim());
|
|
17347
|
-
if (m)
|
|
17514
|
+
if (m) described2.set(m[1], m[2]);
|
|
17348
17515
|
}
|
|
17349
17516
|
}
|
|
17350
|
-
return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...
|
|
17517
|
+
return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described2.has(name) ? { description: described2.get(name) } : {} }));
|
|
17351
17518
|
}
|
|
17352
17519
|
function openApiPath(basePath, endpointPath) {
|
|
17353
17520
|
const join31 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
|
|
@@ -17404,7 +17571,7 @@ function operationFor(method2, scope, entry) {
|
|
|
17404
17571
|
const parameter = (p, where) => ({
|
|
17405
17572
|
name: p.name,
|
|
17406
17573
|
in: where,
|
|
17407
|
-
required: where === "path" ? true : !p.optional,
|
|
17574
|
+
required: where === "path" ? true : !p.optional && !admitsNoValue(p.type),
|
|
17408
17575
|
...p.description ? { description: p.description } : {},
|
|
17409
17576
|
schema: schemaFor(p.type, scope)
|
|
17410
17577
|
});
|
|
@@ -17413,7 +17580,7 @@ function operationFor(method2, scope, entry) {
|
|
|
17413
17580
|
const bodyParam = rest.length === 1 ? rest[0] : void 0;
|
|
17414
17581
|
if (bodyVerbs.has(httpVerb) && bodyParam && scope.isObject(bodyParam.type)) {
|
|
17415
17582
|
op.requestBody = {
|
|
17416
|
-
required: !bodyParam.optional,
|
|
17583
|
+
required: !bodyParam.optional && !admitsNoValue(bodyParam.type),
|
|
17417
17584
|
...bodyParam.description ? { description: bodyParam.description } : {},
|
|
17418
17585
|
content: { "application/json": { schema: schemaFor(bodyParam.type, scope) } }
|
|
17419
17586
|
};
|
|
@@ -17425,8 +17592,8 @@ function operationFor(method2, scope, entry) {
|
|
|
17425
17592
|
"application/json": {
|
|
17426
17593
|
schema: {
|
|
17427
17594
|
type: "object",
|
|
17428
|
-
properties: Object.fromEntries(rest.map((p) => [p.name, schemaFor(p.type, scope)])),
|
|
17429
|
-
required: rest.filter((p) => !p.optional).map((p) => p.name)
|
|
17595
|
+
properties: Object.fromEntries(rest.map((p) => [p.name, described(schemaFor(p.type, scope), p.description)])),
|
|
17596
|
+
required: rest.filter((p) => !p.optional && !admitsNoValue(p.type)).map((p) => p.name)
|
|
17430
17597
|
}
|
|
17431
17598
|
}
|
|
17432
17599
|
}
|
|
@@ -17448,8 +17615,9 @@ function typeComponent(t, scope) {
|
|
|
17448
17615
|
return {
|
|
17449
17616
|
type: "object",
|
|
17450
17617
|
title: t.name,
|
|
17451
|
-
|
|
17452
|
-
|
|
17618
|
+
// A field's description travels on its property; a `T?` field may be left out, like an optional one.
|
|
17619
|
+
properties: Object.fromEntries(t.fields.map((f) => [f.name, described(schemaFor(f.type, scope), f.description)])),
|
|
17620
|
+
required: t.fields.filter((f) => !f.optional && !admitsNoValue(f.type)).map((f) => f.name)
|
|
17453
17621
|
};
|
|
17454
17622
|
}
|
|
17455
17623
|
var SIGNATURE_EXTENSION = "x-wairon-signature";
|
|
@@ -17554,8 +17722,8 @@ var OpenApiRouteCollisionError = class extends Error {
|
|
|
17554
17722
|
};
|
|
17555
17723
|
function renderDoc(snapshot, entries, context, opts = {}) {
|
|
17556
17724
|
const { schemes, securityByEntry } = buildSecurity(entries);
|
|
17557
|
-
const
|
|
17558
|
-
const scope = schemaScope(snapshot.types, context.externals ?? /* @__PURE__ */ new Map(),
|
|
17725
|
+
const registry = { reached: /* @__PURE__ */ new Map(), keyOf: /* @__PURE__ */ new Map() };
|
|
17726
|
+
const scope = schemaScope(snapshot.types, context.externals ?? /* @__PURE__ */ new Map(), registry);
|
|
17559
17727
|
const paths = {};
|
|
17560
17728
|
const taken = /* @__PURE__ */ new Map();
|
|
17561
17729
|
for (const entry of entries) {
|
|
@@ -17577,15 +17745,12 @@ function renderDoc(snapshot, entries, context, opts = {}) {
|
|
|
17577
17745
|
}
|
|
17578
17746
|
}
|
|
17579
17747
|
const schemas = {};
|
|
17580
|
-
for (const t of snapshot.types) schemas[t.id] = typeComponent(t, scope);
|
|
17581
|
-
const rendered = /* @__PURE__ */ new Set();
|
|
17582
17748
|
for (let more = true; more; ) {
|
|
17583
17749
|
more = false;
|
|
17584
|
-
for (const [key2, { alias, def }] of [...reached]) {
|
|
17585
|
-
if (
|
|
17586
|
-
rendered.add(key2);
|
|
17750
|
+
for (const [key2, { alias, def }] of [...registry.reached]) {
|
|
17751
|
+
if (key2 in schemas) continue;
|
|
17587
17752
|
more = true;
|
|
17588
|
-
schemas[key2] = typeComponent(def, scope.within(alias));
|
|
17753
|
+
schemas[key2] = typeComponent(def, alias !== void 0 ? scope.within(alias) : scope);
|
|
17589
17754
|
}
|
|
17590
17755
|
}
|
|
17591
17756
|
const components = {};
|
|
@@ -17699,6 +17864,13 @@ function describedWith(description, note) {
|
|
|
17699
17864
|
const text3 = [description, note].filter((x) => !!x).join(" ");
|
|
17700
17865
|
return text3 ? { description: text3 } : {};
|
|
17701
17866
|
}
|
|
17867
|
+
function leftOut(required, type) {
|
|
17868
|
+
return required || type.endsWith("?") ? {} : { optional: true };
|
|
17869
|
+
}
|
|
17870
|
+
function statedStatus(code, method2, verb) {
|
|
17871
|
+
if (code === void 0 || !/^[23]\d\d$/.test(code)) return {};
|
|
17872
|
+
return Number(code) === conventionalStatus(method2, verb) ? {} : { status: Number(code) };
|
|
17873
|
+
}
|
|
17702
17874
|
function authFromSecurityScheme(scheme) {
|
|
17703
17875
|
const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
|
|
17704
17876
|
if (scheme.type === "apiKey") {
|
|
@@ -17757,10 +17929,11 @@ function fromOpenApi(document, projectName) {
|
|
|
17757
17929
|
for (const p of op.parameters ?? []) {
|
|
17758
17930
|
if (typeof p.name !== "string") continue;
|
|
17759
17931
|
const schema = p.schema;
|
|
17932
|
+
const type = typeRefFromSchema(schema);
|
|
17760
17933
|
params.push({
|
|
17761
17934
|
name: p.name,
|
|
17762
|
-
type
|
|
17763
|
-
...p.required === true
|
|
17935
|
+
type,
|
|
17936
|
+
...leftOut(p.required === true, type),
|
|
17764
17937
|
...describedWith(typeof p.description === "string" ? p.description : void 0, inlineEnumNote(schema))
|
|
17765
17938
|
});
|
|
17766
17939
|
}
|
|
@@ -17779,10 +17952,11 @@ function fromOpenApi(document, projectName) {
|
|
|
17779
17952
|
const required = new Set(bodySchema.required ?? []);
|
|
17780
17953
|
if (Object.keys(props).length) {
|
|
17781
17954
|
for (const [pname, pschema] of Object.entries(props)) {
|
|
17955
|
+
const type = typeRefFromSchema(pschema);
|
|
17782
17956
|
params.push({
|
|
17783
17957
|
name: pname,
|
|
17784
|
-
type
|
|
17785
|
-
...required.has(pname)
|
|
17958
|
+
type,
|
|
17959
|
+
...leftOut(required.has(pname), type),
|
|
17786
17960
|
...describedWith(typeof pschema.description === "string" ? pschema.description : void 0, inlineEnumNote(pschema))
|
|
17787
17961
|
});
|
|
17788
17962
|
}
|
|
@@ -17792,7 +17966,7 @@ function fromOpenApi(document, projectName) {
|
|
|
17792
17966
|
}
|
|
17793
17967
|
const responses = op.responses ?? {};
|
|
17794
17968
|
const jsonSchema = (r) => r?.content?.["application/json"]?.schema;
|
|
17795
|
-
const successCode = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0];
|
|
17969
|
+
const successCode = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0] ?? Object.keys(responses).filter((c) => /^3\d\d$/.test(c)).sort()[0];
|
|
17796
17970
|
const responseSchema = successCode !== void 0 ? jsonSchema(responses[successCode]) : void 0;
|
|
17797
17971
|
const failureCode = ["default", ...Object.keys(responses).filter((c) => /^[45](\d\d|XX)$/i.test(c)).sort()].find((c) => jsonSchema(responses[c]) !== void 0);
|
|
17798
17972
|
const success = responseSchema ? typeRefFromSchema(responseSchema) : "void";
|
|
@@ -17809,7 +17983,7 @@ function fromOpenApi(document, projectName) {
|
|
|
17809
17983
|
signature: `${name}(${params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${returns}`,
|
|
17810
17984
|
returns,
|
|
17811
17985
|
params,
|
|
17812
|
-
endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath },
|
|
17986
|
+
endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath, ...statedStatus(successCode, { name, returns, ...effect ? { effect } : {} }, verb) },
|
|
17813
17987
|
...guarantees.length ? { guarantees } : {},
|
|
17814
17988
|
...effect ? { effect } : {},
|
|
17815
17989
|
...ext ? { ext } : {}
|
|
@@ -17838,11 +18012,15 @@ function fromOpenApi(document, projectName) {
|
|
|
17838
18012
|
id,
|
|
17839
18013
|
name: typeof schema.title === "string" ? schema.title : id,
|
|
17840
18014
|
kind: "value-object",
|
|
17841
|
-
fields: Object.entries(props).map(([fname, fschema]) =>
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
18015
|
+
fields: Object.entries(props).map(([fname, fschema]) => {
|
|
18016
|
+
const type = typeRefFromSchema(fschema);
|
|
18017
|
+
return {
|
|
18018
|
+
name: fname,
|
|
18019
|
+
type,
|
|
18020
|
+
...leftOut(required.has(fname), type),
|
|
18021
|
+
...typeof fschema.description === "string" ? { description: fschema.description } : {}
|
|
18022
|
+
};
|
|
18023
|
+
})
|
|
17846
18024
|
});
|
|
17847
18025
|
}
|
|
17848
18026
|
const securitySchemes = parsed.components?.securitySchemes ?? {};
|
|
@@ -18596,7 +18774,7 @@ function listPinnedExternals() {
|
|
|
18596
18774
|
const config = loadProjectConfig();
|
|
18597
18775
|
declared = config ? [
|
|
18598
18776
|
...declaredExternals(config),
|
|
18599
|
-
...declaredMembers(config).filter((m) => !m.problem && m.storage !== "contained").map((m) => ({ alias: m.alias, project: m.alias }))
|
|
18777
|
+
...declaredMembers(config).filter((m) => !m.problem && m.storage !== "contained" && m.storage !== "path").map((m) => ({ alias: m.alias, project: m.alias }))
|
|
18600
18778
|
] : [];
|
|
18601
18779
|
} catch {
|
|
18602
18780
|
return [];
|
|
@@ -18715,7 +18893,7 @@ function foreignSurfaces() {
|
|
|
18715
18893
|
}
|
|
18716
18894
|
const root = getProjectRoot();
|
|
18717
18895
|
for (const m of members) {
|
|
18718
|
-
if (m.problem || m.storage !== "contained" || m.path === void 0 || out.has(m.alias)) continue;
|
|
18896
|
+
if (m.problem || m.storage !== "contained" && m.storage !== "path" || m.path === void 0 || out.has(m.alias)) continue;
|
|
18719
18897
|
try {
|
|
18720
18898
|
const surface = runWithProjectRoot(path15.resolve(root, m.path), () => projectOwnSurface("project"));
|
|
18721
18899
|
out.set(m.alias, surface);
|
|
@@ -18730,7 +18908,7 @@ function withForeignTypes(snapshot, foreign) {
|
|
|
18730
18908
|
const have = new Set(types.map((t) => t.id));
|
|
18731
18909
|
const queue = [];
|
|
18732
18910
|
const scan2 = (text3) => {
|
|
18733
|
-
for (const token of (text3 ?? "").match(/[A-Za-z0-9_-]
|
|
18911
|
+
for (const token of (text3 ?? "").match(/[A-Za-z0-9_-]+(?:::[A-Za-z0-9_-]+)+/g) ?? []) queue.push(token);
|
|
18734
18912
|
};
|
|
18735
18913
|
for (const e of snapshot.interfaces) for (const m of e.methods) {
|
|
18736
18914
|
for (const p of m.params ?? []) scan2(p.type);
|
|
@@ -18749,12 +18927,14 @@ function withForeignTypes(snapshot, foreign) {
|
|
|
18749
18927
|
const name = ref.slice(cut + 2);
|
|
18750
18928
|
const pin2 = foreign.get(alias);
|
|
18751
18929
|
if (!pin2) continue;
|
|
18752
|
-
const
|
|
18930
|
+
const segment = (id) => id.includes("::") ? id.slice(id.lastIndexOf("::") + 2) : id;
|
|
18931
|
+
const shared = (id) => pin2.types.filter((d) => segment(d.id) === segment(id)).length > 1;
|
|
18932
|
+
const local = new Map(pin2.types.map((d) => [d.id, `${alias}::${(pin2.exportedTypes ?? []).find((t) => t.type === d.id)?.id ?? (shared(d.id) ? d.id : segment(d.id))}`]));
|
|
18933
|
+
const defId = (pin2.exportedTypes ?? []).find((t) => t.id === name)?.type ?? (pin2.types.some((d) => d.id === name) ? name : [...local].find(([, spelled]) => spelled === ref)?.[0]);
|
|
18753
18934
|
const def = pin2.types.find((d) => d.id === defId);
|
|
18754
18935
|
if (!def) continue;
|
|
18755
|
-
const local = new Map(pin2.types.map((d) => [d.id, `${alias}::${(pin2.exportedTypes ?? []).find((t) => t.type === d.id)?.id ?? d.id}`]));
|
|
18756
18936
|
local.set(def.id, ref);
|
|
18757
|
-
const respell = (text3) => text3?.replace(/[A-Za-z0-9_][A-Za-z0-9_-]*/g, (token) => local.get(token) ?? token);
|
|
18937
|
+
const respell = (text3) => text3?.replace(/[A-Za-z0-9_][A-Za-z0-9_-]*(?:::[A-Za-z0-9_][A-Za-z0-9_-]*)*/g, (token) => local.get(token) ?? token);
|
|
18758
18938
|
const expanded = {
|
|
18759
18939
|
...def,
|
|
18760
18940
|
id: ref,
|
|
@@ -19016,6 +19196,10 @@ function pinExternals(aliases) {
|
|
|
19016
19196
|
const declared = bindings.map((b) => b.external.alias);
|
|
19017
19197
|
const named2 = aliases && aliases.length > 0 ? aliases : void 0;
|
|
19018
19198
|
const unknown = (named2 ?? []).filter((a) => !declared.includes(a));
|
|
19199
|
+
const live = unknown.filter((a) => declaredMembers(loadProjectConfig() ?? {}).some((m) => m.alias === a && !m.problem && (m.storage === "contained" || m.storage === "path")));
|
|
19200
|
+
if (live.length > 0 && live.length === unknown.length) {
|
|
19201
|
+
throw new WaironError(`${live.map((a) => `"${a}"`).join(", ")} ${live.length === 1 ? "is a member" : "are members"} of this project, read live where ${live.length === 1 ? "it is" : "they are"} stored (inside it, or a \`../\` sibling checkout) and composed in the family run like any contained member \u2014 a member is never pinned. Pin only an external (\`externals add\`), or a git or hosted member. Nothing was written.`);
|
|
19202
|
+
}
|
|
19019
19203
|
if (unknown.length) throw new UnknownExternalAliasError(unknown, declared);
|
|
19020
19204
|
const kept = named2 ? bindings.filter((b) => named2.includes(b.external.alias)) : bindings;
|
|
19021
19205
|
const lock = externalsRepository.readLock() ?? { externals: {} };
|
|
@@ -19323,7 +19507,23 @@ function listExternals() {
|
|
|
19323
19507
|
return [...declared, ...orphans];
|
|
19324
19508
|
}
|
|
19325
19509
|
function listConsumers2(search) {
|
|
19326
|
-
|
|
19510
|
+
const consumers = listConsumers(search);
|
|
19511
|
+
if (consumers.length === 0) return consumers;
|
|
19512
|
+
let own2;
|
|
19513
|
+
try {
|
|
19514
|
+
own2 = projectOwnSurface("project");
|
|
19515
|
+
} catch {
|
|
19516
|
+
return consumers;
|
|
19517
|
+
}
|
|
19518
|
+
const held = new Map(own2.interfaces.map((e) => [e.id, new Set(e.methods.map((m) => m.name))]));
|
|
19519
|
+
return consumers.map((c) => {
|
|
19520
|
+
const gone = (c.uses ?? []).flatMap((u) => {
|
|
19521
|
+
const methods = held.get(u.publicName);
|
|
19522
|
+
return methods ? u.members.filter((m) => m !== "type" && !m.startsWith("capability:") && !methods.has(m)).map((m) => `${u.publicName}.${m}`) : [];
|
|
19523
|
+
});
|
|
19524
|
+
if (gone.length === 0) return c;
|
|
19525
|
+
return { ...c, broken: [.../* @__PURE__ */ new Set([...c.broken ?? [], ...gone])].sort() };
|
|
19526
|
+
});
|
|
19327
19527
|
}
|
|
19328
19528
|
function diff(against) {
|
|
19329
19529
|
const saved = against !== void 0 && fs9.existsSync(against) && fs9.statSync(against).isFile();
|
|
@@ -19819,6 +20019,7 @@ function readMembers(body, mode) {
|
|
|
19819
20019
|
if (seg.startsWith("[") || seg.startsWith("(") || seg.startsWith("<") || seg.startsWith("...") || /^new\b/.test(seg)) continue;
|
|
19820
20020
|
const name = IDENT.exec(seg)?.[0];
|
|
19821
20021
|
if (name === void 0 || name === "constructor") continue;
|
|
20022
|
+
if (mode !== "type" && name.startsWith("_")) continue;
|
|
19822
20023
|
let i = name.length;
|
|
19823
20024
|
while (seg[i] === "?" || seg[i] === "!") i++;
|
|
19824
20025
|
if (accessor) {
|
|
@@ -19881,7 +20082,7 @@ function tagBefore(clean, docs, start) {
|
|
|
19881
20082
|
function lineAt(clean, index) {
|
|
19882
20083
|
return clean.slice(0, index).split("\n").length;
|
|
19883
20084
|
}
|
|
19884
|
-
function commonJsDeclaration(value, base) {
|
|
20085
|
+
function commonJsDeclaration(value, base, clean = "", docs = []) {
|
|
19885
20086
|
const t = value.trim();
|
|
19886
20087
|
if (t.startsWith("{")) return { ...base, kind: "object", members: readMembers(t.slice(1, closeOf(t, 0) - 1), "object") };
|
|
19887
20088
|
const params = functionValueParams(t);
|
|
@@ -19891,25 +20092,68 @@ function commonJsDeclaration(value, base) {
|
|
|
19891
20092
|
const open = cls[0].length - 1;
|
|
19892
20093
|
return { ...base, kind: "class", members: readMembers(t.slice(open + 1, closeOf(t, open) - 1), "class") };
|
|
19893
20094
|
}
|
|
20095
|
+
const ident = /^([A-Za-z_$][\w$]*)\s*$/.exec(t);
|
|
20096
|
+
if (ident && clean) {
|
|
20097
|
+
const local = localDeclaration(clean, docs, ident[1]);
|
|
20098
|
+
const tag = base.tag ?? local?.tag;
|
|
20099
|
+
if (local) return { ...local, name: base.name, ...tag ? { tag } : {} };
|
|
20100
|
+
}
|
|
20101
|
+
return null;
|
|
20102
|
+
}
|
|
20103
|
+
function localDeclaration(clean, docs, name) {
|
|
20104
|
+
const escaped = name.replace(/\$/g, "\\$");
|
|
20105
|
+
const re = new RegExp(`(?<![\\w$.])(?:declare\\s+)?(?:(?:abstract\\s+)?(class)|(?:async\\s+)?(function)\\s*\\*?|(interface)|(?:const|let|var))\\s+${escaped}(?![\\w$])`, "g");
|
|
20106
|
+
for (const match2 of clean.matchAll(re)) {
|
|
20107
|
+
const start = match2.index ?? 0;
|
|
20108
|
+
const tag = tagBefore(clean, docs, start);
|
|
20109
|
+
const base = { name, line: lineAt(clean, start), ...tag ? { tag } : {} };
|
|
20110
|
+
let i = start + match2[0].length;
|
|
20111
|
+
if (match2[1] || match2[3]) {
|
|
20112
|
+
const open = clean.indexOf("{", i);
|
|
20113
|
+
if (open < 0) continue;
|
|
20114
|
+
return { ...base, kind: match2[1] ? "class" : "interface", members: readMembers(clean.slice(open + 1, closeOf(clean, open) - 1), match2[1] ? "class" : "type") };
|
|
20115
|
+
}
|
|
20116
|
+
if (match2[2]) {
|
|
20117
|
+
i = skipTypeParams(clean, i);
|
|
20118
|
+
i = skipSpace(clean, i);
|
|
20119
|
+
return { ...base, kind: "function", params: clean[i] === "(" ? paramNames(clean.slice(i + 1, closeOf(clean, i) - 1)) : [], members: [] };
|
|
20120
|
+
}
|
|
20121
|
+
const eq = clean.indexOf("=", i);
|
|
20122
|
+
if (eq < 0) continue;
|
|
20123
|
+
const decl = commonJsDeclaration(valueAt2(clean, eq + 1), base);
|
|
20124
|
+
if (decl) return decl;
|
|
20125
|
+
}
|
|
19894
20126
|
return null;
|
|
19895
20127
|
}
|
|
19896
20128
|
var CJS_NAMED_RE = /(?<![\w$.])(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=(?!=)/g;
|
|
19897
|
-
var CJS_WHOLE_RE = /(?<![\w$.])module\.exports\s*=(
|
|
20129
|
+
var CJS_WHOLE_RE = /(?<![\w$.])(?:module\.exports|export)\s*=(?![=>])/g;
|
|
19898
20130
|
function valueAt2(clean, from) {
|
|
19899
20131
|
return splitTop(clean.slice(from), ";")[0] ?? "";
|
|
19900
20132
|
}
|
|
20133
|
+
function wholeExportName(value) {
|
|
20134
|
+
const named2 = /^(?:class|(?:async\s+)?function\s*\*?)\s+([A-Za-z_$][\w$]*)/.exec(value)?.[1] ?? /^([A-Za-z_$][\w$]*)\s*$/.exec(value)?.[1];
|
|
20135
|
+
return named2 === "require" ? void 0 : named2;
|
|
20136
|
+
}
|
|
19901
20137
|
function readCommonJs(clean, docs) {
|
|
19902
20138
|
const out = [];
|
|
19903
20139
|
for (const match2 of clean.matchAll(CJS_NAMED_RE)) {
|
|
19904
20140
|
const start = match2.index ?? 0;
|
|
19905
20141
|
const tag = tagBefore(clean, docs, start);
|
|
19906
|
-
const decl = commonJsDeclaration(valueAt2(clean, start + match2[0].length), { name: match2[1], line: lineAt(clean, start), ...tag ? { tag } : {} });
|
|
20142
|
+
const decl = commonJsDeclaration(valueAt2(clean, start + match2[0].length), { name: match2[1], line: lineAt(clean, start), ...tag ? { tag } : {} }, clean, docs);
|
|
19907
20143
|
if (decl) out.push(decl);
|
|
19908
20144
|
}
|
|
19909
20145
|
for (const match2 of clean.matchAll(CJS_WHOLE_RE)) {
|
|
19910
20146
|
const start = match2.index ?? 0;
|
|
19911
20147
|
const open = skipSpace(clean, start + match2[0].length);
|
|
19912
|
-
|
|
20148
|
+
const wholeTag = tagBefore(clean, docs, start);
|
|
20149
|
+
if (clean[open] !== "{") {
|
|
20150
|
+
const value = valueAt2(clean, open).trim();
|
|
20151
|
+
const named2 = wholeExportName(value);
|
|
20152
|
+
if (named2 === void 0) continue;
|
|
20153
|
+
const decl = commonJsDeclaration(value, { name: named2, line: lineAt(clean, start), ...wholeTag ? { tag: wholeTag } : {} }, clean, docs);
|
|
20154
|
+
if (decl) out.push(decl);
|
|
20155
|
+
continue;
|
|
20156
|
+
}
|
|
19913
20157
|
const body = clean.slice(open + 1, closeOf(clean, open) - 1);
|
|
19914
20158
|
let offset = open + 1;
|
|
19915
20159
|
for (const raw of splitTop(body, ",")) {
|
|
@@ -19918,31 +20162,60 @@ function readCommonJs(clean, docs) {
|
|
|
19918
20162
|
if (found >= 0) offset = found + raw.length;
|
|
19919
20163
|
const seg = raw.replace(/^async\s+/, "");
|
|
19920
20164
|
const name = IDENT.exec(seg)?.[0];
|
|
19921
|
-
if (name === void 0) continue;
|
|
19922
|
-
const
|
|
20165
|
+
if (name === void 0 || name.startsWith("_")) continue;
|
|
20166
|
+
const own2 = tagBefore(clean, docs, at);
|
|
20167
|
+
const tag = own2 ?? wholeTag;
|
|
19923
20168
|
const base = { name, line: lineAt(clean, at), ...tag ? { tag } : {} };
|
|
19924
20169
|
const rest = seg.slice(name.length).trimStart();
|
|
19925
20170
|
if (rest.startsWith("(")) {
|
|
19926
20171
|
out.push({ ...base, kind: "function", params: paramNames(rest.slice(1, closeOf(rest, 0) - 1)), members: [] });
|
|
19927
20172
|
} else if (rest.startsWith(":")) {
|
|
19928
|
-
const decl = commonJsDeclaration(rest.slice(1), base);
|
|
20173
|
+
const decl = commonJsDeclaration(rest.slice(1), base, clean, docs);
|
|
19929
20174
|
if (decl) out.push(decl);
|
|
20175
|
+
} else if (rest === "") {
|
|
20176
|
+
const local = localDeclaration(clean, docs, name);
|
|
20177
|
+
const localTag = own2 ?? local?.tag ?? wholeTag;
|
|
20178
|
+
if (local) out.push({ ...local, ...localTag ? { tag: localTag } : {} });
|
|
19930
20179
|
}
|
|
19931
20180
|
}
|
|
19932
20181
|
}
|
|
19933
20182
|
return out;
|
|
19934
20183
|
}
|
|
20184
|
+
function readTypedefs(text3, clean) {
|
|
20185
|
+
const out = [];
|
|
20186
|
+
for (const comment of text3.matchAll(/\/\*\*[\s\S]*?\*\//g)) {
|
|
20187
|
+
const body = comment[0];
|
|
20188
|
+
const typedef = /@typedef\s+(?:\{[^}]*\}\s*)?([A-Za-z_$][\w$]*)/.exec(body);
|
|
20189
|
+
if (!typedef) continue;
|
|
20190
|
+
const members = [];
|
|
20191
|
+
for (const prop of body.matchAll(/@prop(?:erty)?\s+(?:\{([^}]*)\}\s*)?\[?([A-Za-z_$][\w$]*)/g)) {
|
|
20192
|
+
members.push(prop[1] ? { name: prop[2], type: prop[1].trim() } : { name: prop[2] });
|
|
20193
|
+
}
|
|
20194
|
+
const tag = TAG_RE.exec(body);
|
|
20195
|
+
out.push({
|
|
20196
|
+
name: typedef[1],
|
|
20197
|
+
kind: members.length > 0 ? "type" : "alias",
|
|
20198
|
+
members,
|
|
20199
|
+
line: lineAt(clean, (comment.index ?? 0) + body.indexOf("@typedef")),
|
|
20200
|
+
...tag ? { tag: `${tag[1]}::${tag[2]}` } : {}
|
|
20201
|
+
});
|
|
20202
|
+
}
|
|
20203
|
+
return out;
|
|
20204
|
+
}
|
|
19935
20205
|
function readUnreadForms(text3) {
|
|
19936
|
-
const { clean } = scan(text3);
|
|
20206
|
+
const { clean, docs } = scan(text3);
|
|
19937
20207
|
const forms = /* @__PURE__ */ new Set();
|
|
19938
20208
|
const shown2 = (value) => /^require\s*\(/.test(value) ? "require(\u2026)" : `${IDENT.exec(value)?.[0] ?? "\u2026"}${/^[A-Za-z_$][\w$.]*\s*\(/.test(value) ? "(\u2026)" : ""}`;
|
|
19939
20209
|
for (const match2 of clean.matchAll(CJS_WHOLE_RE)) {
|
|
19940
20210
|
const value = valueAt2(clean, (match2.index ?? 0) + match2[0].length).trim();
|
|
19941
|
-
if (
|
|
20211
|
+
if (value.startsWith("{")) continue;
|
|
20212
|
+
const named2 = wholeExportName(value);
|
|
20213
|
+
if (named2 !== void 0 && commonJsDeclaration(value, { name: named2, line: 0 }, clean, docs) !== null) continue;
|
|
20214
|
+
forms.add(`${match2[0].startsWith("export") ? "export" : "module.exports"} = ${shown2(value)}`);
|
|
19942
20215
|
}
|
|
19943
20216
|
for (const match2 of clean.matchAll(CJS_NAMED_RE)) {
|
|
19944
20217
|
const value = valueAt2(clean, (match2.index ?? 0) + match2[0].length).trim();
|
|
19945
|
-
if (commonJsDeclaration(value, { name: match2[1], line: 0 }) === null) forms.add(`exports.${match2[1]} = ${shown2(value)}`);
|
|
20218
|
+
if (commonJsDeclaration(value, { name: match2[1], line: 0 }, clean, docs) === null) forms.add(`exports.${match2[1]} = ${shown2(value)}`);
|
|
19946
20219
|
}
|
|
19947
20220
|
return [...forms];
|
|
19948
20221
|
}
|
|
@@ -19984,7 +20257,7 @@ function readDeclarations(text3) {
|
|
|
19984
20257
|
}
|
|
19985
20258
|
}
|
|
19986
20259
|
const seen = new Set(out.map((d) => d.name));
|
|
19987
|
-
for (const decl of readCommonJs(clean, docs)) {
|
|
20260
|
+
for (const decl of [...readCommonJs(clean, docs), ...readTypedefs(text3, clean)]) {
|
|
19988
20261
|
if (!seen.has(decl.name)) out.push(decl);
|
|
19989
20262
|
seen.add(decl.name);
|
|
19990
20263
|
}
|
|
@@ -20115,6 +20388,13 @@ function releaseStampAgainst(record, running) {
|
|
|
20115
20388
|
if (isNewerVersion(running, stamp)) return "newer";
|
|
20116
20389
|
return isNewerVersion(stamp, running) ? "older" : "same";
|
|
20117
20390
|
}
|
|
20391
|
+
function releaseStampProblem(record, running) {
|
|
20392
|
+
const reading = releaseStampAgainst(record, running);
|
|
20393
|
+
if (reading === "older" || reading === "same") return null;
|
|
20394
|
+
const stamp = record.validatorVersion;
|
|
20395
|
+
if (reading === "newer") return `its release stamp says wairon ${String(stamp)} took it, a newer release than this one (${running})`;
|
|
20396
|
+
return typeof stamp === "string" && stamp !== "" ? `its release stamp "${stamp}" is no wairon version` : "it carries no release stamp";
|
|
20397
|
+
}
|
|
20118
20398
|
function stampRecordsGateParts(stamp) {
|
|
20119
20399
|
return stamp === GATE_PARTS_SINCE || isNewerVersion(GATE_PARTS_SINCE, stamp);
|
|
20120
20400
|
}
|
|
@@ -20872,6 +21152,18 @@ function specsUsing(family, alias, publicName) {
|
|
|
20872
21152
|
const specs = family.authoredReferences.filter((r) => (family.owners.get(r.specId) ?? "") === "" && (r.authored.startsWith(`${alias}::`) || (r.importedVia ?? "").split(", ").includes(alias)) && (publicName === void 0 || (r.publicName ?? r.authored.split("::")[r.form === "import" ? 0 : 1]) === publicName)).map((r) => r.specId);
|
|
20873
21153
|
return [...new Set(specs)].sort();
|
|
20874
21154
|
}
|
|
21155
|
+
function bindingModulesOf(specs) {
|
|
21156
|
+
if (specs.length === 0) return [];
|
|
21157
|
+
const named2 = new Set(specs);
|
|
21158
|
+
const componentOf = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
|
|
21159
|
+
const out = /* @__PURE__ */ new Set();
|
|
21160
|
+
for (const impl of loadImplementationSpecs()) {
|
|
21161
|
+
if (impl.id.includes("::") || !impl.bindings?.length) continue;
|
|
21162
|
+
if (!named2.has(impl.id) && !named2.has(impl.contract) && !named2.has(componentOf.get(impl.contract) ?? "")) continue;
|
|
21163
|
+
for (const b of impl.bindings) out.add(b.replace(/\\/g, "/"));
|
|
21164
|
+
}
|
|
21165
|
+
return [...out].sort();
|
|
21166
|
+
}
|
|
20875
21167
|
function movedUse(use) {
|
|
20876
21168
|
return `${useName(use)} ${howMoved(use).replace(/^was renamed/, "renamed").replace(/^is gone/, "gone")}`;
|
|
20877
21169
|
}
|
|
@@ -20892,10 +21184,11 @@ function adviseOn(status, family, config) {
|
|
|
20892
21184
|
const broken = status.uses.filter(isBroken);
|
|
20893
21185
|
const users = [...new Set(broken.flatMap((u) => specsUsing(family, status.alias, u.publicName)))].sort();
|
|
20894
21186
|
const renamed2 = broken.some((u) => u.state === "renamed" || u.detail?.includes("follow the rename"));
|
|
21187
|
+
const bindings = bindingModulesOf(users);
|
|
20895
21188
|
return advisory(
|
|
20896
21189
|
config,
|
|
20897
21190
|
"EXTERNAL_LIVE_INCOMPATIBLE",
|
|
20898
|
-
`${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
|
|
21191
|
+
`${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. ` + (bindings.length ? `Binding module(s) declared there: ${bindings.map((b) => `"${b}"`).join(", ")} \u2014 compared with the pin, not the live producer, so ${bindings.length === 1 ? "it draws" : "they draw"} BINDING_DRIFT once the re-pin lands; follow the move there too. ` : "") + `Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
|
|
20899
21192
|
);
|
|
20900
21193
|
}
|
|
20901
21194
|
case "drifted":
|
|
@@ -21146,10 +21439,13 @@ function approvalAt(node, ceiling) {
|
|
|
21146
21439
|
if (!record) return base;
|
|
21147
21440
|
const same = (id) => !!id && record.stateId.algorithm === id.algorithm && record.stateId.digest === id.digest;
|
|
21148
21441
|
const matches = same(current2) || same(current2.asRecorded);
|
|
21149
|
-
const partsIgnored = gatePartsProblem(record, current2) ?? void 0;
|
|
21442
|
+
const partsIgnored = gatePartsProblem(record, current2) ?? (matches && record.gateParts === void 0 && current2.parts ? `the record carries none, though every record wairon ${GATE_PARTS_SINCE} and later writes carries them` : void 0);
|
|
21150
21443
|
const parts = usableGateParts(record, current2);
|
|
21151
21444
|
const moved = matches || !parts ? null : movedGateParts({ gateParts: parts }, current2.parts);
|
|
21152
21445
|
const release = matches ? void 0 : releaseVerdict(record, current2);
|
|
21446
|
+
const stampClause = releaseStampProblem(record, WAIRON_VERSION);
|
|
21447
|
+
const stampNamed = release?.reason !== void 0 && release.reason.includes(String(record.validatorVersion ?? "no release stamp"));
|
|
21448
|
+
const stampProblem = stampClause !== null && !stampNamed ? stampClause : void 0;
|
|
21153
21449
|
const owed = owedBy(record);
|
|
21154
21450
|
const covered = (matches || release?.carried === true) && owed === void 0;
|
|
21155
21451
|
return {
|
|
@@ -21160,7 +21456,8 @@ function approvalAt(node, ceiling) {
|
|
|
21160
21456
|
...release ? { release } : {},
|
|
21161
21457
|
...moved && moved.length > 0 && !release ? { inputsMoved: moved } : {},
|
|
21162
21458
|
...owed !== void 0 ? { owed } : {},
|
|
21163
|
-
...partsIgnored !== void 0 ? { partsIgnored } : {}
|
|
21459
|
+
...partsIgnored !== void 0 ? { partsIgnored } : {},
|
|
21460
|
+
...stampProblem !== void 0 ? { stampProblem } : {}
|
|
21164
21461
|
};
|
|
21165
21462
|
});
|
|
21166
21463
|
}
|
|
@@ -21251,8 +21548,23 @@ function owedBy(record) {
|
|
|
21251
21548
|
for (const [alias, uses] of Object.entries(unrecorded())) {
|
|
21252
21549
|
reasons.push(`external "${alias}" is used beyond its pin: ${uses.join(", ")} (used now, but not in the lock)`);
|
|
21253
21550
|
}
|
|
21551
|
+
const unloadable = unloadablePacks();
|
|
21552
|
+
if (unloadable.length > 0) reasons.push(`extension pack(s) selected in .wai/project.yaml cannot load \u2014 ${unloadable.join("; ")}`);
|
|
21254
21553
|
return reasons.length > 0 ? reasons.join("; ") : void 0;
|
|
21255
21554
|
}
|
|
21555
|
+
function unloadablePacks() {
|
|
21556
|
+
let loaded;
|
|
21557
|
+
try {
|
|
21558
|
+
loaded = loadProjectExtensions();
|
|
21559
|
+
} catch {
|
|
21560
|
+
return [];
|
|
21561
|
+
}
|
|
21562
|
+
return [
|
|
21563
|
+
...loaded.errors,
|
|
21564
|
+
// A selection that resolved with a warning (unpinned, unfetchable origin) still loaded.
|
|
21565
|
+
...(loaded.selectionFailures ?? []).filter((f) => f.severity === "error").map((f) => f.message)
|
|
21566
|
+
];
|
|
21567
|
+
}
|
|
21256
21568
|
function pinOf(parent, alias, subject) {
|
|
21257
21569
|
if (!parent) return "unpinned";
|
|
21258
21570
|
const recorded = parent.members ? ownHas(parent.members, alias) ? ownGet(parent.members, alias).subject ?? "never" : void 0 : ownGet(parent.children, alias);
|
|
@@ -23425,9 +23737,9 @@ function draftContext(ctx, interfaceId, component) {
|
|
|
23425
23737
|
const intf = ctx.interfaceMap.get(interfaceId);
|
|
23426
23738
|
return ctx.isComponentDraft(component) || intf?.status === "draft" || intf?.status === "design";
|
|
23427
23739
|
}
|
|
23428
|
-
function projectOf(
|
|
23429
|
-
const at =
|
|
23430
|
-
return at < 0 ? "" :
|
|
23740
|
+
function projectOf(componentKey2) {
|
|
23741
|
+
const at = componentKey2.lastIndexOf("::");
|
|
23742
|
+
return at < 0 ? "" : componentKey2.slice(0, at);
|
|
23431
23743
|
}
|
|
23432
23744
|
function targetComponent(target) {
|
|
23433
23745
|
return target.slice(0, target.lastIndexOf("."));
|
|
@@ -28604,7 +28916,7 @@ function writeCandidates(ctx) {
|
|
|
28604
28916
|
}
|
|
28605
28917
|
}
|
|
28606
28918
|
};
|
|
28607
|
-
const receiving = (call.receivers ?? []).flatMap((r) => realization.componentsAt(r.path));
|
|
28919
|
+
const receiving = (call.receivers ?? []).flatMap((r) => realization.componentsAt(r.path)).filter((c) => c.id !== caller.id);
|
|
28608
28920
|
if (receiving.length > 0) {
|
|
28609
28921
|
for (const callee of receiving) take(callee);
|
|
28610
28922
|
return out;
|
|
@@ -28711,12 +29023,13 @@ function describeMiss(m) {
|
|
|
28711
29023
|
var callConformanceRule = {
|
|
28712
29024
|
name: "call-conformance",
|
|
28713
29025
|
judges: "code",
|
|
28714
|
-
description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. Where that claim's target has no code yet \u2014 no file any of its implementations names exists, the normal state after the first component of a service is implemented from its brief through a port to a collaborator still planned \u2014 the call is planned rather than unresolved (CALL_TARGET_PLANNED, a notice, as SOURCE_FILE_PLANNED is), and it is judged like any other claim once the target's realization begins. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Where a type checker read the file, a claim is also realized by any call or reference in the bodies the function reaches \u2014 and in the unowned code they call \u2014 that the checker lands on the target: its file under a name it is realized by, a class realizing the port the call was typed by, or what the target forwards to by identity \u2014 whatever the receiver's spelling (a type-only or `export *` barrel, a port declared in a shared module, a dependency bag typed inline); a claim it does not land keeps the shape tier's answer, since the checker's landing on a declaration says what a receiver is TYPED as, never where the call went instead. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. And a call \u2014 or a reference the code takes as a value \u2014 that the TypeScript type checker resolves into ANOTHER file, onto a write- or lifecycle-effect contract method of another component, or, from a Portal method whose body is its own, onto a verb of an Orchestrator, Supervisor or Actor whose effect is a write \u2014 declared (write, lifecycle or io), or, where its contract declares none, read off the verb's own narrative transitively: a call step or declared call reaching a method that changes state makes it a write, only reads or none a read, and a verb that settles nothing (no narrative and no declared calls, a dispatch step, a target outside this tree, a non-workflow method declaring no effect) is reported as an unnarrated call whose effect is undeclared, never as a write (a Portal changes state only by dispatching to such a verb, so its narrative is the list of those it may reach), that the method claims nowhere is a mutation the design says the method never makes (UNDECLARED_WRITE_CALL): narrate it, declare it in `calls`, or remove it \u2014 judged in the bodies the method's function reaches and in the UNOWNED code they call (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops \u2014 but a member of the CLASS a workflow component (Orchestrator, Supervisor, Actor) is realized by, one of the classes holding its modelled methods, is that component's code even where its contract does not declare it, so the walk stops there: a claim the function answers by calling such a member in place of the claimed method is CALL_STEP_UNREALIZED naming the method the code calls instead, the shape a rename made in the code alone leaves, and a Portal's call to one no missed claim names is an unnarrated call to a method whose effect no narrative settles, never read through its body as the Portal's own write), never for a Portal's call into a data component (PORTAL_WRITE_SHORTCUT_IN_CODE's subject), and only where a type checker was loaded. A call there the checker cannot follow \u2014 its receiver typed any or unknown, cast to either or to an index signature, typed by an index signature naming no member of that name, or its member picked by a computed key \u2014 under the name of such a write the method claims nowhere (where the checker recorded the receiver's original type \u2014 every cast, non-null mark and parenthesis taken off, an `await (x)` written outside an async function read as the x it means, nullability set aside so an optional field is the class it holds, a name bound once by a const read through its initializer, and an interface or shape no class realizes landing on the file declaring it \u2014 a write of the component that type lands on, any of its writes when a computed key names none; else a write of that name in the method's own subsystem or one its component depends into) fails closed as CALL_ORIGIN_UNRESOLVED: neither proven an unnarrated write nor cleared. A member picked by a key that names no single member, on a receiver that was one of this project's classes before any cast, is such a call however the receiver or the key is cast and whether it is invoked on the spot or taken as a value and invoked later (`const f = store[k]; f.call(store, \u2026)`); a key the checker types as one literal names its member by that literal's value, never by the variable holding it. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial
|
|
29026
|
+
description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. Where that claim's target has no code yet \u2014 no file any of its implementations names exists, the normal state after the first component of a service is implemented from its brief through a port to a collaborator still planned \u2014 the call is planned rather than unresolved (CALL_TARGET_PLANNED, a notice, as SOURCE_FILE_PLANNED is), and it is judged like any other claim once the target's realization begins. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Where a type checker read the file, a claim is also realized by any call or reference in the bodies the function reaches \u2014 and in the unowned code they call \u2014 that the checker lands on the target: its file under a name it is realized by, a class realizing the port the call was typed by, or what the target forwards to by identity \u2014 whatever the receiver's spelling (a type-only or `export *` barrel, a port declared in a shared module, a dependency bag typed inline); a claim it does not land keeps the shape tier's answer, since the checker's landing on a declaration says what a receiver is TYPED as, never where the call went instead. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. And a call \u2014 or a reference the code takes as a value \u2014 that the TypeScript type checker resolves into ANOTHER file, onto a write- or lifecycle-effect contract method of another component, or, from a Portal method whose body is its own, onto a verb of an Orchestrator, Supervisor or Actor whose effect is a write \u2014 declared (write, lifecycle or io), or, where its contract declares none, read off the verb's own narrative transitively: a call step or declared call reaching a method that changes state makes it a write, only reads or none a read, and a verb that settles nothing (no narrative and no declared calls, a dispatch step, a target outside this tree, a non-workflow method declaring no effect) is reported as an unnarrated call whose effect is undeclared, never as a write (a Portal changes state only by dispatching to such a verb, so its narrative is the list of those it may reach), that the method claims nowhere is a mutation the design says the method never makes (UNDECLARED_WRITE_CALL): narrate it, declare it in `calls`, or remove it \u2014 judged in the bodies the method's function reaches and in the UNOWNED code they call (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops \u2014 but a member of the CLASS a workflow component (Orchestrator, Supervisor, Actor) is realized by, one of the classes holding its modelled methods, is that component's code even where its contract does not declare it, so the walk stops there: a claim the function answers by calling such a member in place of the claimed method is CALL_STEP_UNREALIZED naming the method the code calls instead, the shape a rename made in the code alone leaves, and a Portal's call to one no missed claim names is an unnarrated call to a method whose effect no narrative settles, never read through its body as the Portal's own write), never for a Portal's call into a data component (PORTAL_WRITE_SHORTCUT_IN_CODE's subject), and only where a type checker was loaded. A call there the checker cannot follow, or a member read by name off such a receiver and taken as a value (`const f = (store as any).save; f(x)`) \u2014 its receiver typed any or unknown however it came to be (a cast, an untyped or any-typed parameter of a closure, a function result typed any), cast to either or to an index signature, typed by an index signature naming no member of that name, or its member picked by a computed key \u2014 under the name of such a write the method claims nowhere (where the checker recorded the receiver's original type \u2014 every cast, non-null mark and parenthesis taken off, an `await (x)` written outside an async function read as the x it means, nullability set aside so an optional field is the class it holds, a name bound once by a const read through its initializer, and an interface or shape no class realizes landing on the file declaring it \u2014 a write of the component that type lands on, any of its writes when a computed key names none; else a write of that name in the method's own subsystem or one its component depends into \u2014 the design's write set, which a data component whose code is still planned has too; a receiver landing only on the caller's own file, a port it declares for itself, leaves it to that name as well) fails closed as CALL_ORIGIN_UNRESOLVED: neither proven an unnarrated write nor cleared. A member picked by a key that names no single member, on a receiver that was one of this project's classes before any cast, is such a call however the receiver or the key is cast and whether it is invoked on the spot or taken as a value and invoked later (`const f = store[k]; f.call(store, \u2026)`); a key the checker types as one literal names its member by that literal's value, never by the variable holding it. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial set off skips the forward direction \u2014 whether the claims are found in the code \u2014 but never the converse, so off is no way past an unnarrated write or a call the analysis cannot follow; a claim whose target's code is in ANOTHER language than the caller's file (every file the target's implementations name is in a language a TypeScript call graph does not read, or, naming none, the member or project it belongs to declares such a target language) is not judged by this call graph at all, and the language boundary is said once per project and language (CALL_ACROSS_LANGUAGE, a notice) \u2014 never as an unrealized call no code could ever clear, since what crosses that boundary is the binding; and weaker analysis grades never guess.",
|
|
28715
29027
|
codes: [
|
|
28716
29028
|
{ code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step or declared call realized by no call that resolves to the target method's own source file \u2014 the call is absent, or it lands in another module", carryable: true },
|
|
28717
29029
|
{ code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused; or, the fail-closed half of UNDECLARED_WRITE_CALL, a call through a receiver the analysis cannot follow (typed any or unknown, cast to either or to an index signature, a computed key) under the name of a write the method claims nowhere. Where a type checker read the file, whatever the checker resolves; without one, what resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a this.<field> receiver whose declared type names one, a receiver chain (this.<field>.<property>, <name>.<property>) each of whose links the code declares a type for, a new Class(\u2026) receiver whose class does, a receiver name the file annotates with such a type, a fn(\u2026) receiver whose return the code settles, and this inside a class's own method \u2014 a declared type read through Pick/Omit/Partial/Required/Readonly and one alias hop. What does not: a third-party package import, a value the module assembled, and a receiver the file declares nothing for", carryable: true },
|
|
28718
29030
|
{ code: "UNDECLARED_COLOCATED_CALL", defaultSeverity: "warning", summary: "The realized function calls a modelled method of another component living in the same source file, and neither a narrative step nor a declared call names it", carryable: true },
|
|
28719
29031
|
{ code: "UNDECLARED_WRITE_CALL", defaultSeverity: "warning", summary: "The realized function, or unowned code it calls, calls or takes as a value a write- or lifecycle-effect method of another component in another file \u2014 or, from a Portal, a workflow verb whose effect, declared or read off its own narrative, is a write, or that nothing settles (said as an unnarrated call whose effect is undeclared), or a method of a workflow component's class its contract does not declare \u2014 as the type checker resolves it, and neither a narrative step nor a declared call names it \u2014 an unnarrated mutation", carryable: true },
|
|
29032
|
+
{ code: "CALL_ACROSS_LANGUAGE", defaultSeverity: "notice", summary: "Claimed calls from TypeScript or JavaScript code reach a component whose code is in another language (a member or project written in Rust, say): the call graph cannot resolve into it, so the claims are not judged by it, and the boundary is said once per project and language" },
|
|
28720
29033
|
{ code: "CALL_TARGET_PLANNED", defaultSeverity: "notice", summary: "A narrative call step or declared call written by name through a receiver nothing realizes yet, whose target component has no code yet (no file its implementations name exists) \u2014 planned, not unresolved; judged like any other claim once the target's realization begins" }
|
|
28721
29034
|
],
|
|
28722
29035
|
check(ctx) {
|
|
@@ -28815,10 +29128,26 @@ var callConformanceRule = {
|
|
|
28815
29128
|
for (const claim of claimsOf(entry.method)) refs.add(`${claim.component}.${claim.method}`);
|
|
28816
29129
|
claimsByBody.set(key2, refs);
|
|
28817
29130
|
}
|
|
29131
|
+
const script = (language) => language === "typescript" || language === "javascript";
|
|
29132
|
+
const foreignLanguageOf = (callerFile, target, componentId) => {
|
|
29133
|
+
if (!script(languageOfSourcePath(callerFile))) return void 0;
|
|
29134
|
+
const sep18 = componentId.indexOf("::");
|
|
29135
|
+
const project3 = sep18 > 0 ? componentId.slice(0, sep18) : "this project";
|
|
29136
|
+
if (target.files.size > 0) {
|
|
29137
|
+
const languages = new Set([...target.files].map((f) => languageOfSourcePath(f)));
|
|
29138
|
+
if ([...languages].some((l) => l === void 0 || script(l))) return void 0;
|
|
29139
|
+
return { project: project3, language: [...languages].sort().join(" and ") };
|
|
29140
|
+
}
|
|
29141
|
+
if (sep18 <= 0) return void 0;
|
|
29142
|
+
const node = (ctx.projectFamily?.nodes ?? []).find((n) => n.namespace === project3 || n.mountAlias === project3 || n.id === project3);
|
|
29143
|
+
const declared = node?.targetLanguage?.toLowerCase();
|
|
29144
|
+
if (!node || !declared || /typescript|javascript/.test(declared)) return void 0;
|
|
29145
|
+
return { project: project3, language: node.targetLanguage };
|
|
29146
|
+
};
|
|
29147
|
+
const boundaries = /* @__PURE__ */ new Map();
|
|
28818
29148
|
for (const entry of methods) {
|
|
28819
29149
|
const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
|
|
28820
|
-
const
|
|
28821
|
-
if (tier === "off") continue;
|
|
29150
|
+
const off = (implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component)) === "off";
|
|
28822
29151
|
if (!implMethod.narrative.length && !implMethod.calls?.length) continue;
|
|
28823
29152
|
if (!file) continue;
|
|
28824
29153
|
const facts = code.factsAt(file);
|
|
@@ -28848,10 +29177,18 @@ var callConformanceRule = {
|
|
|
28848
29177
|
const missed = [];
|
|
28849
29178
|
const plannedClaims = [];
|
|
28850
29179
|
const hinted = /* @__PURE__ */ new Set();
|
|
28851
|
-
for (const claim of claimsOf(implMethod)) {
|
|
29180
|
+
for (const claim of off ? [] : claimsOf(implMethod)) {
|
|
28852
29181
|
if (!ctx.componentMap.has(claim.component)) continue;
|
|
28853
29182
|
if (isRemoteLink(component, ctx.componentMap.get(claim.component))) continue;
|
|
28854
29183
|
const target = resolveCallTarget(ctx, claim.component, claim.method);
|
|
29184
|
+
const foreign = foreignLanguageOf(file, target, claim.component);
|
|
29185
|
+
if (foreign) {
|
|
29186
|
+
const key2 = `${foreign.project}|${foreign.language}`;
|
|
29187
|
+
const boundary = boundaries.get(key2) ?? { ...foreign, from: languageOfSourcePath(file) ?? "unknown", anchor: impl.id, draft: entry.draftContext, units: [] };
|
|
29188
|
+
boundary.units.push(`${impl.id}.${implMethod.name} \u2192 ${claim.component}.${claim.method}`);
|
|
29189
|
+
boundaries.set(key2, boundary);
|
|
29190
|
+
continue;
|
|
29191
|
+
}
|
|
28855
29192
|
if (target.bodies.has(bodyKey(here, fnSymbol, implMethod.exportedVia))) continue;
|
|
28856
29193
|
const sharedBody = targetForwards(code, target);
|
|
28857
29194
|
if (code.forwardsOf(file, fnSymbol, implMethod.exportedVia).some((p) => p.file !== here && sharedBody.has(`${p.file}#${p.name}`))) continue;
|
|
@@ -29032,7 +29369,7 @@ var callConformanceRule = {
|
|
|
29032
29369
|
if (call.unresolved) {
|
|
29033
29370
|
for (const ref of unresolvedCandidates(call, component)) {
|
|
29034
29371
|
if (!unclaimed(ref) || unresolvedWrites.has(ref)) continue;
|
|
29035
|
-
unresolvedWrites.set(ref, `\`${call.written}(\u2026)
|
|
29372
|
+
unresolvedWrites.set(ref, `${call.reference ? `\`${call.written}\` (taken as a value)` : `\`${call.written}(\u2026)\``}${throughText(through)}`);
|
|
29036
29373
|
}
|
|
29037
29374
|
}
|
|
29038
29375
|
}
|
|
@@ -29071,6 +29408,18 @@ var callConformanceRule = {
|
|
|
29071
29408
|
}
|
|
29072
29409
|
}
|
|
29073
29410
|
}
|
|
29411
|
+
for (const boundary of boundaries.values()) {
|
|
29412
|
+
const where = boundary.project === "this project" ? "components of this project" : `the project "${boundary.project}"`;
|
|
29413
|
+
ctx.addIssue(
|
|
29414
|
+
"notice",
|
|
29415
|
+
"CALL_ACROSS_LANGUAGE",
|
|
29416
|
+
`${boundary.units.length} claimed call(s) from ${boundary.from} code reach ${where}, whose code is ${boundary.language} \u2014 ${boundary.units.join("; ")}. A ${boundary.from} call graph never resolves into a ${boundary.language} file, so these claims are not judged by it, and calling them unrealized would be a finding no code could ever clear. What crosses the boundary is the binding: declare the module that carries it in the implementation's \`bindings\`, which is compared with what the other side exports.`,
|
|
29417
|
+
boundary.anchor,
|
|
29418
|
+
boundary.draft,
|
|
29419
|
+
void 0,
|
|
29420
|
+
{ at: boundary.project, covers: boundary.units }
|
|
29421
|
+
);
|
|
29422
|
+
}
|
|
29074
29423
|
}
|
|
29075
29424
|
};
|
|
29076
29425
|
|
|
@@ -29136,13 +29485,14 @@ var UNOWNED_IMPORT_DEPTH = 8;
|
|
|
29136
29485
|
var dependencyConformanceRule = {
|
|
29137
29486
|
name: "dependency-conformance",
|
|
29138
29487
|
judges: "code",
|
|
29139
|
-
description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). A type-only import alone never accuses \u2014 type coupling is allowed \u2014 but a CALL of a modelled contract method through a collaborator whose declared type comes from another component's file is runtime collaboration whatever the import's form, and the same justification is owed (UNDECLARED_DEPENDENCY on that edge). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target, or \u2014 where the type checker read a file of the source \u2014 as a call or reference the checker lands in a file of the target, through every barrel, port declaration, dependency bag and unowned helper (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat the import reading, hence warning); a type-only import realizes it, since `import type` is what dependency injection writes down. An Adapter's edge to a Portal reached over an out-of-process transport (HTTP, gRPC, a bus, a CLI\u2026) is the link the design models: the Adapter's transport client realizes it, never an import, so it is never UNREALIZED_DEPENDENCY \u2014 and nothing asks for an import across that boundary, which would couple two deployables' builds. Two twins of design rules are judged on the same code: a Portal file calling a write- or lifecycle-effect contract method of a Repository, Index, Store or Registry is the persistence shortcut the design rule PORTAL_WRITE_SHORTCUT refuses, unless the Portal's own narrative already claims that call (the design rule's finding then) (PORTAL_WRITE_SHORTCUT_IN_CODE). A Portal's calls are resolved by the TypeScript type checker: every call written in a file realizing only Portals \u2014 in a file shared with other components, every call in a body the Portal's own methods reach \u2014 and every reference there to a function or method the code takes as a value (passed as an argument, assigned, returned, bound, handed to Reflect.apply), together with every call the UNOWNED code they reach makes (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops), lands on the declaration its signature resolves to, whatever the receiver's spelling, and a declaration in an interface or type shape, a port the Portal declares for itself included, lands on each class of the project that realizes it (the classes whose implements clause names it when any does, else every class the checker finds assignable to it); such a landing on a component the file neither imports at runtime nor is justified to reach is UNDECLARED_DEPENDENCY on that edge. Where no type checker could be loaded, the receiver is followed through the shape facts instead: a non-null assertion, parentheses, `satisfies`, a cast to a named type and a local alias or destructuring of a field exactly as through the field itself. A call in a Portal's own code the analysis cannot resolve at all \u2014 its receiver typed any or unknown, cast to either or to an index signature, its member picked by a computed key, or typed nothing \u2014 under the name of a write- or lifecycle-effect method of a data component, or reading a member by a key that names no single member off a receiver that was a data component before any cast \u2014 invoked on the spot or taken as a value, however the receiver or the key is cast (where the checker recorded the receiver's ORIGINAL type, before any cast \u2014 an `await (x)` written outside an async function read as the x it means, nullability set aside, a name bound once by a const read through its initializer, an interface no class realizes landing on the file declaring it \u2014 a write of the data component that type lands on \u2014 any of its writes when a computed key names none; else one in the Portal's own subsystem or one it depends into), which the Portal does not already claim, fails closed: it is neither proven a shortcut nor cleared, and it is a warning, so a CI gate never passes what it could not read (PORTAL_CALL_UNRESOLVED). So does a call landing only on a PORT no written class realizes \u2014 a member of an interface or shape declared where no component's contract answers for it \u2014 under the name of such a write the Portal claims nowhere and no narrative of the Portal names on any component: the design says what that port will be before the code realizing it is written, and the finding says when the data component has no code yet. And an import, runtime or type-only, from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml that is not this project's root \u2014 that the type checker resolves to nothing, the module gone or the name no longer exported, is CROSS_PROJECT_IMPORT_UNRESOLVED: another project's code moves on its own release, and a type-only import of it is erased at run time, so only a compiler would notice. And a file importing a technology's package \u2014 itself, or through the UNOWNED modules it imports (a module no component realizes, read as if inlined in its importer, transitively to a bounded depth, the finding naming the hops; a module a component realizes is judged as its own) \u2014 while no component it realizes is that technology's home is the leak TECH_LEAKAGE refuses in the design (TECH_LEAKAGE_IN_CODE): a technology's packages are its declared tokens (its name, or its `matches`), the curated built-in table of the common packages its name is known by (postgres: pg, postgres, @neondatabase/serverless, @vercel/postgres; mysql: mysql2, mysql; redis: redis, ioredis; mongodb: mongodb, mongoose; sqlite: better-sqlite3, sqlite3; kafka: kafkajs; rabbitmq: amqplib \u2014 an HTTP client is never a technology leak), and the packages a loaded pack contributes for it, each compared exactly with an import's package name. A package of the built-in table or of a loaded pack whose technology NO implementation binds is reported too, since the design never says where that technology lives: bind it on the data-layer component that owns the vendor call. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
|
|
29488
|
+
description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). A type-only import alone never accuses \u2014 type coupling is allowed \u2014 but a CALL of a modelled contract method through a collaborator whose declared type comes from another component's file is runtime collaboration whatever the import's form, and the same justification is owed (UNDECLARED_DEPENDENCY on that edge). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target, or \u2014 where the type checker read a file of the source \u2014 as a call or reference the checker lands in a file of the target, through every barrel, port declaration, dependency bag and unowned helper (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat the import reading, hence warning); a type-only import realizes it, since `import type` is what dependency injection writes down. An Adapter's edge to a Portal reached over an out-of-process transport (HTTP, gRPC, a bus, a CLI\u2026) is the link the design models: the Adapter's transport client realizes it, never an import, so it is never UNREALIZED_DEPENDENCY \u2014 and nothing asks for an import across that boundary, which would couple two deployables' builds. Two twins of design rules are judged on the same code: a Portal file calling a write- or lifecycle-effect contract method of a Repository, Index, Store or Registry is the persistence shortcut the design rule PORTAL_WRITE_SHORTCUT refuses, unless the Portal's own narrative already claims that call (the design rule's finding then) (PORTAL_WRITE_SHORTCUT_IN_CODE). A Portal's calls are resolved by the TypeScript type checker: every call written in a file realizing only Portals \u2014 in a file shared with other components, every call in a body the Portal's own methods reach \u2014 and every reference there to a function or method the code takes as a value (passed as an argument, assigned, returned, bound, handed to Reflect.apply), together with every call the UNOWNED code they reach makes (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops), lands on the declaration its signature resolves to, whatever the receiver's spelling, and a declaration in an interface or type shape, a port the Portal declares for itself included, lands on each class of the project that realizes it (the classes whose implements clause names it when any does, else every class the checker finds assignable to it); such a landing on a component the file neither imports at runtime nor is justified to reach is UNDECLARED_DEPENDENCY on that edge. Where no type checker could be loaded, the receiver is followed through the shape facts instead: a non-null assertion, parentheses, `satisfies`, a cast to a named type and a local alias or destructuring of a field exactly as through the field itself. A call in a Portal's own code the analysis cannot resolve at all \u2014 its receiver typed any or unknown however it came to be (a cast, an untyped or any-typed parameter of a closure, a local typed any, a function result typed any), cast to either or to an index signature, its member picked by a computed key, or typed nothing \u2014 under the name of a write- or lifecycle-effect method of a data component, or reading a member by a key that names no single member off a receiver that was a data component before any cast \u2014 invoked on the spot or taken as a value, however the receiver or the key is cast (where the checker recorded the receiver's ORIGINAL type, before any cast \u2014 an `await (x)` written outside an async function read as the x it means, nullability set aside, a name bound once by a const read through its initializer, an interface no class realizes landing on the file declaring it \u2014 a write of the data component that type lands on \u2014 any of its writes when a computed key names none; else one in the Portal's own subsystem or one it depends into \u2014 the design's write set, which a data component whose code is still planned has too, and what a receiver landing only on the Portal's own file, a port it declares for itself, is judged by), which the Portal does not already claim, fails closed \u2014 one finding per site and per function it is written in, so one shortcut never hides another of the same name: it is neither proven a shortcut nor cleared, and it is a warning, so a CI gate never passes what it could not read (PORTAL_CALL_UNRESOLVED). So does a call landing only on a PORT no written class realizes \u2014 a member of an interface or shape declared where no component's contract answers for it \u2014 under the name of such a write the Portal claims nowhere and no narrative of the Portal names on any component: the design says what that port will be before the code realizing it is written, and the finding says when the data component has no code yet. And an import, runtime or type-only, from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml that is not this project's root \u2014 that the type checker resolves to nothing, the module gone or the name no longer exported, is CROSS_PROJECT_IMPORT_UNRESOLVED: another project's code moves on its own release, and a type-only import of it is erased at run time, so only a compiler would notice. One that DOES resolve is judged like any other edge, because it is one: a name it imports that the other project does not publish \u2014 not in its L0 export table as a member exports it now, nor in the pinned snapshot of the external it is (an exported type by its id or code name, an exported component by its contract methods and their code symbols; a project that is neither publishes nothing to this one) \u2014 is code reaching into that project's internals past its surface, which no pin and no binding compares (CROSS_PROJECT_SOURCE_IMPORT, a warning; the finding says when the module it lands in realizes a data component of that project, since a write through it is the persistence shortcut past the other project's surface). A binding module an implementation declares is the sanctioned place for exactly that reach, compared by binding conformance, so an import written in one is never reported here. And a file importing a technology's package \u2014 itself, or through the UNOWNED modules it imports (a module no component realizes, read as if inlined in its importer, transitively to a bounded depth, the finding naming the hops; a module a component realizes is judged as its own) \u2014 while no component it realizes is that technology's home is the leak TECH_LEAKAGE refuses in the design (TECH_LEAKAGE_IN_CODE): a technology's packages are its declared tokens (its name, or its `matches`), the curated built-in table of the common packages its name is known by (postgres: pg, postgres, @neondatabase/serverless, @vercel/postgres; mysql: mysql2, mysql; redis: redis, ioredis; mongodb: mongodb, mongoose; sqlite: better-sqlite3, sqlite3; kafka: kafkajs; rabbitmq: amqplib \u2014 an HTTP client is never a technology leak), and the packages a loaded pack contributes for it, each compared exactly with an import's package name. A package of the built-in table or of a loaded pack whose technology NO implementation binds is reported too, since the design never says where that technology lives: bind it on the data-layer component that owns the vendor call. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
|
|
29140
29489
|
codes: [
|
|
29141
29490
|
{ code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import, a call through a type-only-imported collaborator, or a Portal's call the type checker resolves into another component's file, between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
|
|
29142
29491
|
{ code: "PORTAL_WRITE_SHORTCUT_IN_CODE", defaultSeverity: "error", summary: "A Portal's code calls, or takes as a value, a write- or lifecycle-effect method of a data component (Repository, Index, Store, Registry) \u2014 directly or through unowned code it calls \u2014 the code twin of PORTAL_WRITE_SHORTCUT: mutations route through an Orchestrator" },
|
|
29143
29492
|
{ code: "TECH_LEAKAGE_IN_CODE", defaultSeverity: "warning", summary: "A source file imports a technology's package although no component it realizes binds that technology \u2014 the code twin of TECH_LEAKAGE" },
|
|
29144
29493
|
{ code: "UNREALIZED_DEPENDENCY", defaultSeverity: "warning", summary: "A declared dependsOn/owns edge between components in different files is realized by no import between any of their files, nor by any call the type checker lands across it" },
|
|
29145
29494
|
{ code: "PORTAL_CALL_UNRESOLVED", defaultSeverity: "warning", summary: "A call in a Portal's own code, under the name of a data component's write- or lifecycle-effect method \u2014 or with its member picked by a computed key on a receiver that was a data component before a cast \u2014 goes through a receiver the analysis cannot resolve (typed any or unknown, cast to either or to an index signature, or typed nothing), or lands only on a port no written class realizes \u2014 neither proven a shortcut nor cleared, so it fails closed until the receiver carries its type" },
|
|
29495
|
+
{ code: "CROSS_PROJECT_SOURCE_IMPORT", defaultSeverity: "warning", summary: "A source file imports a name from ANOTHER project's source that the project does not export \u2014 code reaching into its internals past its surface, which no pin and no binding compares (a write into its Store through it included); a declared binding module is the sanctioned place for that reach", carryable: true },
|
|
29146
29496
|
{ code: "CROSS_PROJECT_IMPORT_UNRESOLVED", defaultSeverity: "warning", summary: "A source file imports from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml \u2014 a module that is gone or a name it no longer exports: a type-only import is erased at run time, so only a compiler would notice, and the gate reads the file with one", carryable: true }
|
|
29147
29497
|
],
|
|
29148
29498
|
check(ctx) {
|
|
@@ -29437,8 +29787,8 @@ var dependencyConformanceRule = {
|
|
|
29437
29787
|
const near = /* @__PURE__ */ new Set([portal.subsystem, ...portal.dependsOn.map((d) => ctx.componentMap.get(d)?.subsystem)]);
|
|
29438
29788
|
const candidates = (call.call ? candidatesOf(call.call, portal, (callee) => DATA_COMPONENTS2.has(callee.componentType)) : (dataWrites.get(call.name) ?? []).filter((w) => near.has(ctx.componentMap.get(w.component)?.subsystem))).filter((w) => !claims(portal.id, w.component, w.method));
|
|
29439
29789
|
const at = `${fromPath} -> ${call.name || candidates.map((w) => `${w.component}.${w.method}`).join(", ")}`;
|
|
29440
|
-
if (candidates.length > 0 && !reportedUnresolved.has(at)) {
|
|
29441
|
-
reportedUnresolved.add(at);
|
|
29790
|
+
if (candidates.length > 0 && !reportedUnresolved.has(`${at}|${call.where}`)) {
|
|
29791
|
+
reportedUnresolved.add(`${at}|${call.where}`);
|
|
29442
29792
|
ctx.addIssue(
|
|
29443
29793
|
"warning",
|
|
29444
29794
|
"PORTAL_CALL_UNRESOLVED",
|
|
@@ -29610,6 +29960,7 @@ var dependencyConformanceRule = {
|
|
|
29610
29960
|
}
|
|
29611
29961
|
for (const facts of ctx.codeModel.files) {
|
|
29612
29962
|
for (const broken of facts.crossProjectImports ?? []) {
|
|
29963
|
+
if (broken.resolved) continue;
|
|
29613
29964
|
const file = pathKey(facts.path);
|
|
29614
29965
|
const what = broken.name !== void 0 ? `"${broken.name}" from "${broken.specifier}"` : `"${broken.specifier}"`;
|
|
29615
29966
|
ctx.addIssue(
|
|
@@ -29623,8 +29974,97 @@ var dependencyConformanceRule = {
|
|
|
29623
29974
|
);
|
|
29624
29975
|
}
|
|
29625
29976
|
}
|
|
29977
|
+
const bindingFiles = new Set(ctx.implementations.flatMap((impl) => impl.bindings ?? []).map(pathKey));
|
|
29978
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
29979
|
+
const surfaceOf = (project3) => {
|
|
29980
|
+
if (surfaces.has(project3)) return surfaces.get(project3);
|
|
29981
|
+
const answer = projectSurface(ctx, project3);
|
|
29982
|
+
surfaces.set(project3, answer);
|
|
29983
|
+
return answer;
|
|
29984
|
+
};
|
|
29985
|
+
for (const facts of ctx.codeModel.files) {
|
|
29986
|
+
const file = pathKey(facts.path);
|
|
29987
|
+
if (bindingFiles.has(file)) continue;
|
|
29988
|
+
const reaches = (facts.crossProjectImports ?? []).filter((fact) => fact.resolved);
|
|
29989
|
+
const bySpecifier = /* @__PURE__ */ new Map();
|
|
29990
|
+
for (const reach3 of reaches) bySpecifier.set(reach3.specifier, [...bySpecifier.get(reach3.specifier) ?? [], reach3]);
|
|
29991
|
+
for (const [specifier, list] of bySpecifier) {
|
|
29992
|
+
const first = list[0];
|
|
29993
|
+
const surface = surfaceOf(first.project);
|
|
29994
|
+
const internal = list.filter((reach3) => !(surface && reach3.name !== void 0 && reach3.name !== "*" && surface.names.has(nameKey2(reach3.name))));
|
|
29995
|
+
if (internal.length === 0) continue;
|
|
29996
|
+
const names = internal.map((reach3) => reach3.name === void 0 ? "the module itself" : reach3.name === "*" ? "the whole module" : `"${reach3.name}"`);
|
|
29997
|
+
const owner = first.module !== void 0 ? surface?.data.get(first.module) : void 0;
|
|
29998
|
+
const writes = owner !== void 0 ? ` "${first.module}" realizes ${owner} of that project: a write through it is the persistence shortcut past the other project's surface, and at least as serious as an undeclared dependency.` : "";
|
|
29999
|
+
const label7 = surface?.label ?? `"${first.project}"`;
|
|
30000
|
+
ctx.addIssue(
|
|
30001
|
+
"warning",
|
|
30002
|
+
"CROSS_PROJECT_SOURCE_IMPORT",
|
|
30003
|
+
`"${file}" imports ${names.join(", ")} from "${specifier}" (line ${first.line}), the SOURCE of another project, ${label7}, which ${surface ? "does not export that name" : "exports nothing this project can reach"} \u2014 code reaching into that project's internals past its surface, which no pin and no binding compares.${writes} Reach what the project exports (its L0 export table: declare it there, then use it through the member alias or a pinned external), or carry the reach in a binding module the implementation declares (\`bindings\`), which is compared with what the project exports.`,
|
|
30004
|
+
realization.implementationsAt(file)[0]?.id,
|
|
30005
|
+
draftAt(file),
|
|
30006
|
+
void 0,
|
|
30007
|
+
{ at: `${file} -> ${specifier}`, covers: internal.map((reach3) => reach3.name ?? "<module>") }
|
|
30008
|
+
);
|
|
30009
|
+
}
|
|
30010
|
+
}
|
|
29626
30011
|
}
|
|
29627
30012
|
};
|
|
30013
|
+
var nameKey2 = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
30014
|
+
function projectSurface(ctx, project3) {
|
|
30015
|
+
const root = import_path.posix.normalize(ctx.codeModel.projectRoot.replace(/\\/g, "/"));
|
|
30016
|
+
const directory = import_path.posix.normalize(import_path.posix.join(root, project3));
|
|
30017
|
+
const same = (dir) => !!dir && import_path.posix.normalize(dir.replace(/\\/g, "/")).toLowerCase() === directory.toLowerCase();
|
|
30018
|
+
const names = /* @__PURE__ */ new Set();
|
|
30019
|
+
const data = /* @__PURE__ */ new Map();
|
|
30020
|
+
const DATA = /* @__PURE__ */ new Set(["Repository", "Index", "Store", "Registry"]);
|
|
30021
|
+
const family = ctx.projectFamily?.nodes ?? [];
|
|
30022
|
+
const member = family.find((node) => node.namespace !== "" && same(node.directory));
|
|
30023
|
+
if (member) {
|
|
30024
|
+
const ns = member.namespace;
|
|
30025
|
+
const table = (ctx.exportTables ?? []).find((t) => t.level === "project" && t.owner === ns);
|
|
30026
|
+
const local = (id) => id.slice(id.lastIndexOf(":") + 1).replace(/^.*\./, "");
|
|
30027
|
+
for (const entry of table?.entries ?? []) {
|
|
30028
|
+
names.add(nameKey2(entry.publicName));
|
|
30029
|
+
if (entry.kind === "type" && entry.typeDef) {
|
|
30030
|
+
const type = ctx.types.find((t) => t.id === entry.typeDef || local(t.id) === local(entry.typeDef));
|
|
30031
|
+
if (type) {
|
|
30032
|
+
names.add(nameKey2(type.name));
|
|
30033
|
+
names.add(nameKey2(type.symbol ?? type.name));
|
|
30034
|
+
names.add(nameKey2(local(type.id)));
|
|
30035
|
+
}
|
|
30036
|
+
} else if (entry.component) {
|
|
30037
|
+
for (const contract of ctx.interfacesByComponent.get(entry.component) ?? []) {
|
|
30038
|
+
for (const method2 of contract.methods) names.add(nameKey2(method2.name));
|
|
30039
|
+
}
|
|
30040
|
+
for (const impl of ctx.implementations) {
|
|
30041
|
+
if (ctx.interfaceMap.get(impl.contract)?.component !== entry.component) continue;
|
|
30042
|
+
for (const method2 of impl.methods) if (method2.symbol) names.add(nameKey2(method2.symbol));
|
|
30043
|
+
}
|
|
30044
|
+
}
|
|
30045
|
+
}
|
|
30046
|
+
for (const impl of ctx.implementations) {
|
|
30047
|
+
const component = ctx.componentMap.get(ctx.interfaceMap.get(impl.contract)?.component ?? "");
|
|
30048
|
+
if (!component || !component.id.startsWith(`${ns}::`) || !DATA.has(component.componentType)) continue;
|
|
30049
|
+
for (const file of implementationSourceFiles(impl)) data.set(pathKey(file), `${component.id} (a ${component.componentType})`);
|
|
30050
|
+
}
|
|
30051
|
+
return { label: `the member "${member.mountAlias ?? ns}"`, names, data };
|
|
30052
|
+
}
|
|
30053
|
+
const bound2 = family.find((node) => node.namespace === "");
|
|
30054
|
+
const external = bound2?.externals.find((e) => same(e.directory));
|
|
30055
|
+
const pin2 = external ? ctx.pinnedExternals.find((p) => p.alias === external.alias) : void 0;
|
|
30056
|
+
if (!external || !pin2?.snapshot) return null;
|
|
30057
|
+
for (const type of pin2.snapshot.types) {
|
|
30058
|
+
names.add(nameKey2(type.id));
|
|
30059
|
+
names.add(nameKey2(type.name));
|
|
30060
|
+
}
|
|
30061
|
+
for (const exported of pin2.snapshot.exportedTypes ?? []) names.add(nameKey2(exported.id));
|
|
30062
|
+
for (const entry of pin2.snapshot.interfaces) {
|
|
30063
|
+
names.add(nameKey2(entry.id));
|
|
30064
|
+
for (const method2 of entry.methods) names.add(nameKey2(method2.name));
|
|
30065
|
+
}
|
|
30066
|
+
return { label: `the external "${external.alias}"`, names, data };
|
|
30067
|
+
}
|
|
29628
30068
|
|
|
29629
30069
|
// src/core/rules/conformance/portal-imports.ts
|
|
29630
30070
|
var portalImportsRule = {
|
|
@@ -30201,8 +30641,17 @@ function bare(name) {
|
|
|
30201
30641
|
return name.replace(/^_+/, "") || name;
|
|
30202
30642
|
}
|
|
30203
30643
|
var HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "res", "response", "reply", "url", "ctx", "context", "next", "event", "socket"]);
|
|
30644
|
+
var TRANSPORT_HANDLES = /* @__PURE__ */ new Set(["req", "res", "ctx", "request", "response", "next", "reply"]);
|
|
30645
|
+
var REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event"]);
|
|
30204
30646
|
var PRIMITIVE_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
|
|
30205
30647
|
var aKind = (kind) => kind === "object" ? "an object" : `a ${kind}`;
|
|
30648
|
+
var fieldKey = (name) => name.toLowerCase().replace(/[_-]/g, "");
|
|
30649
|
+
function structural(fields, realized) {
|
|
30650
|
+
if (!fields || fields.length === 0 || realized.kind !== "object" || !realized.fields || realized.fields.length === 0) return void 0;
|
|
30651
|
+
const has = new Set(realized.fields.map(fieldKey));
|
|
30652
|
+
const present = fields.filter((f) => has.has(fieldKey(f))).length;
|
|
30653
|
+
return present === 0 ? "none" : present === fields.length ? "all" : "some";
|
|
30654
|
+
}
|
|
30206
30655
|
var PAIRING_BUDGET = 5e3;
|
|
30207
30656
|
function* subsets(n, k) {
|
|
30208
30657
|
const pick = [];
|
|
@@ -30242,28 +30691,37 @@ function pairUp(declared, realized, score) {
|
|
|
30242
30691
|
}
|
|
30243
30692
|
return align(best);
|
|
30244
30693
|
}
|
|
30245
|
-
function judge2(declared, realized, injected, typeAgrees,
|
|
30246
|
-
let wiring = 0;
|
|
30247
|
-
while (wiring < realized.length && realized[wiring].name !== void 0 && injected.has(realized[wiring].name)) wiring++;
|
|
30248
|
-
let taken = realized.map((param, at) => ({ param, at })).slice(wiring);
|
|
30694
|
+
function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
30249
30695
|
const declaredNames = new Set(declared.map((param) => bare(param.name)));
|
|
30696
|
+
const isInjected = (param) => param.name !== void 0 && injected.has(bare(param.name));
|
|
30697
|
+
let lead = 0;
|
|
30698
|
+
while (lead < realized.length && isInjected(realized[lead])) lead++;
|
|
30699
|
+
let end = realized.length;
|
|
30700
|
+
while (end > lead && isInjected(realized[end - 1]) && !declaredNames.has(bare(realized[end - 1].name))) end--;
|
|
30701
|
+
let taken = realized.map((param, at) => ({ param, at })).slice(lead, end);
|
|
30250
30702
|
taken = taken.filter(({ param }) => !(param.name !== void 0 && param.name.startsWith("_") && param.unused === true && !declaredNames.has(bare(param.name))));
|
|
30251
30703
|
const score = (d, r) => {
|
|
30252
30704
|
let total = 0;
|
|
30253
30705
|
if (r.name !== void 0 && bare(r.name) === bare(d.name)) total += 4;
|
|
30254
30706
|
if (typeAgrees(d, r)) total += 2;
|
|
30255
|
-
const want =
|
|
30256
|
-
if (want && r.type && r.kind) total += want
|
|
30707
|
+
const want = expected(d);
|
|
30708
|
+
if (want?.kinds && r.type && r.kind) total += want.kinds.has(r.kind) ? 1 : -2;
|
|
30709
|
+
const shape = structural(want?.fields, r);
|
|
30710
|
+
if (shape === "all") total += 2;
|
|
30711
|
+
else if (shape === "none") total -= 2;
|
|
30257
30712
|
return total;
|
|
30258
30713
|
};
|
|
30259
30714
|
const pairs = pairUp(declared, taken.map((entry) => entry.param), score);
|
|
30715
|
+
const wiringParams = [...realized.slice(0, lead), ...realized.slice(end)];
|
|
30260
30716
|
const found = {
|
|
30261
30717
|
unrealized: /* @__PURE__ */ new Map(),
|
|
30262
30718
|
undeclared: /* @__PURE__ */ new Map(),
|
|
30263
30719
|
renamed: /* @__PURE__ */ new Map(),
|
|
30264
30720
|
optionality: /* @__PURE__ */ new Map(),
|
|
30265
30721
|
transport: /* @__PURE__ */ new Map(),
|
|
30266
|
-
wiring: realized.slice(0,
|
|
30722
|
+
wiring: realized.slice(0, lead).map((param) => param.name),
|
|
30723
|
+
trailing: realized.slice(end).map((param) => param.name),
|
|
30724
|
+
carried: /* @__PURE__ */ new Map()
|
|
30267
30725
|
};
|
|
30268
30726
|
const pairedDeclared = new Set(pairs.map(([d]) => d));
|
|
30269
30727
|
const pairedTaken = new Set(pairs.map(([, r]) => r));
|
|
@@ -30272,21 +30730,34 @@ function judge2(declared, realized, injected, typeAgrees, kindOf4) {
|
|
|
30272
30730
|
const param = declared[d];
|
|
30273
30731
|
const { param: at, at: position2 } = taken[r];
|
|
30274
30732
|
const sameName = at.name !== void 0 && bare(at.name) === bare(param.name);
|
|
30275
|
-
const want =
|
|
30276
|
-
|
|
30733
|
+
const want = expected(param);
|
|
30734
|
+
const agrees = typeAgrees(param, at);
|
|
30735
|
+
const shape = structural(want?.fields, at);
|
|
30736
|
+
const handleName = at.name !== void 0 && TRANSPORT_HANDLES.has(bare(at.name).toLowerCase()) && !TRANSPORT_HANDLES.has(bare(param.name).toLowerCase());
|
|
30737
|
+
let instead;
|
|
30738
|
+
if (!agrees) {
|
|
30739
|
+
if (!sameName && want?.kinds && at.type && at.kind && !want.kinds.has(at.kind)) instead = aKind(at.kind);
|
|
30740
|
+
else if (at.transport && want?.fields) instead = "a transport handle";
|
|
30741
|
+
else if (shape === "none") instead = `an object sharing none of its fields (${(want?.fields ?? []).join(", ")})`;
|
|
30742
|
+
else if (handleName && (!at.kind || at.transport)) instead = "a transport handle";
|
|
30743
|
+
}
|
|
30744
|
+
if (instead !== void 0) {
|
|
30277
30745
|
substituted.add(r);
|
|
30746
|
+
const declares2 = want?.told ?? "an argument of its own";
|
|
30278
30747
|
found.unrealized.set(param.name, {
|
|
30279
30748
|
unit: param.name,
|
|
30280
|
-
told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${
|
|
30749
|
+
told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${instead}, where the contract declares ${declares2})`
|
|
30281
30750
|
});
|
|
30282
30751
|
const unit = unitOf2(at, position2);
|
|
30283
|
-
found.undeclared.set(unit, { unit, told: `"${unit}" (${
|
|
30752
|
+
found.undeclared.set(unit, { unit, told: `"${unit}" (${instead} in the place of the contract's "${param.name}", ${declares2})` });
|
|
30284
30753
|
continue;
|
|
30285
30754
|
}
|
|
30286
|
-
|
|
30755
|
+
const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
|
|
30756
|
+
if (at.name !== void 0 && !sameName && (agrees || sameKind || shape === "all")) {
|
|
30757
|
+
const former = (param.previousNames ?? []).some((name) => bare(name) === bare(at.name));
|
|
30287
30758
|
found.renamed.set(`${param.name}\u2192${at.name}`, {
|
|
30288
30759
|
unit: param.name,
|
|
30289
|
-
told: `"${param.name}" (the code calls it "${at.name}")`
|
|
30760
|
+
told: `"${param.name}" (the code calls it "${at.name}"${former ? ", the name the contract retired" : ""})`
|
|
30290
30761
|
});
|
|
30291
30762
|
}
|
|
30292
30763
|
if (!!param.optional !== at.optional) {
|
|
@@ -30296,8 +30767,14 @@ function judge2(declared, realized, injected, typeAgrees, kindOf4) {
|
|
|
30296
30767
|
});
|
|
30297
30768
|
}
|
|
30298
30769
|
}
|
|
30770
|
+
const requestReads = new Set(wiringParams.filter((param) => param.name !== void 0 && REQUEST_HANDLES.has(bare(param.name).toLowerCase())).flatMap((param) => (param.reads ?? []).map(bare)));
|
|
30299
30771
|
declared.forEach((param, d) => {
|
|
30300
|
-
if (
|
|
30772
|
+
if (pairedDeclared.has(d)) return;
|
|
30773
|
+
if (requestReads.has(bare(param.name))) {
|
|
30774
|
+
found.carried.set(param.name, { unit: param.name, told: param.name });
|
|
30775
|
+
return;
|
|
30776
|
+
}
|
|
30777
|
+
found.unrealized.set(param.name, { unit: param.name, told: `"${param.name}"` });
|
|
30301
30778
|
});
|
|
30302
30779
|
const firstPaired = Math.min(...pairs.filter(([, r]) => !substituted.has(r)).map(([, r]) => r), taken.length);
|
|
30303
30780
|
taken.forEach(({ param, at }, r) => {
|
|
@@ -30358,7 +30835,7 @@ function typeKind(type) {
|
|
|
30358
30835
|
var paramConformanceRule = {
|
|
30359
30836
|
name: "param-conformance",
|
|
30360
30837
|
judges: "code",
|
|
30361
|
-
description: "Code-to-contract for the SIGNATURE, the last of the three readings a spec-driven gate never made: a contract declares `params`, and nothing ever compared them to the parameters of the function that realizes the method. A contract could promise an argument the code does not take, take one the contract never mentions \u2014 including a secret \u2014 or name the same argument two different things, and the brief handed to an implementer would carry the contract's version. What a realization takes
|
|
30838
|
+
description: "Code-to-contract for the SIGNATURE, the last of the three readings a spec-driven gate never made: a contract declares `params`, and nothing ever compared them to the parameters of the function that realizes the method. A contract could promise an argument the code does not take, take one the contract never mentions \u2014 including a secret \u2014 or name the same argument two different things, and the brief handed to an implementer would carry the contract's version. What a realization takes besides the contract's own parameters, at either end of its list, is wiring, declared on the implementation as `injectedParams` rather than inferred, because an inferred prefix cannot be told from a renamed first argument; a leading run and a trailing run of the names it declares are dropped, each name matching with or without its leading underscore, and a trailing one only where the contract declares no parameter of that name. A parameter whose name starts with an underscore means UNUSED, and it is set aside only where the analysis proves it is \u2014 named, with no identifier of that name in the body or in another parameter's default value and no read of `arguments`, as the request and URL a transport hands a handler that never reads them: a used `_secret` is judged like any parameter, and a name compared for a rename is read without its leading underscores. The rest are PAIRED in order, since a caller passes arguments in order: among the order-keeping pairings, the one where the most names agree, then the most declared types, then the most kinds of value, and on a tie the one aligned against the TAIL of the realization's list \u2014 so an inserted first argument is the one named undeclared, never the declared one it pushed along. The declared type is what tells a rename from a substitution. Types agree when the code's annotation, read through the dialect of the language the file was analyzed as (type_dialect.agrees), is the contract's canonical type \u2014 so `string[]` agrees with `list<string>`, TypeScript's `number` with int and float alike, and an annotation the dialect cannot read agrees with nothing; a named type agrees through its code-level name, an EXTERNAL one (`alias::name`) through the name its pinned snapshot gives it, else its public name's last segment in the code's type casing. Where a type checker read the file, each parameter the code annotates also carries the KIND of value it takes (string, number, boolean, list, object or function; a platform class the checker's fixed library leaves out \u2014 a URL, a Request, node:http's IncomingMessage \u2014 is an object), compared with the kind the contract's type is (a primitive's, a list, a map or set as an object, an enum as a string, a named scalar as what it holds, a record type as an object, a signature as a function; a date or a datetime as a string or an object, a duration as a number or a string, bytes as an object, a list or a string; anything unsettled compares with nothing). A paired parameter whose name differs and whose type agrees \u2014 or whose kind is the same primitive, or one of the kinds a date or bytes is written as \u2014 is a rename, said with the contract's former name when the code still uses it; one whose name differs and whose kind of value differs is a SUBSTITUTION, a different argument in the declared one's place, so the declared parameter is unrealized and the code's undeclared: `getRoute(params: Record<string, string>)` realizing `getRoute(id: string)` is never a silent pairing. Where the contract's type is a record, an object is compared STRUCTURALLY, by the property names the checker gives its type: one holding every field of the record is a rename, one sharing none of them a substitution whatever either side calls it, and a platform transport class (node:http's IncomingMessage, a fetch Request or Response, a URL, a socket) where a record is declared is a substitution too \u2014 so `planRoute(req: IncomingMessage)` and `planRoute(session: { user: string })` realizing `planRoute(request: plan_request)` are never silent pairings. Where the code settles no kind \u2014 unannotated JavaScript, a parameter typed any \u2014 a paired parameter named as a transport handle (req, res, ctx, request, response, next, reply) standing where the contract declares an argument of its own is a substitution: `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is a handler's shape, not the contract's. Where neither side settles a type and no handle name speaks, nothing is said about the name. A parameter standing ahead of every kept pair under a name transports give their handles (req, request, res, response, reply, url, ctx, context, next, event, socket) is how a transport handle arrives, and the finding names the one green path for it: the handle in `injectedParams`, the contract's own parameters after it in order, the router unpacking the request into them \u2014 or, where the framework hands the function the handles alone, the contract's parameters read off the injected request by their own names: a contract parameter the code does not take but reads off an injected request handle (req, request, ctx, context, event) by its own name \u2014 `req.params.code`, `req.query['limit']`, a destructuring of such a read \u2014 is realized through that handle. An injected name none of the implementation's realizing functions read in this run takes is stale linkage (UNUSED_INJECTED_PARAM): it would wave through the next parameter of that name unread. A name with several bodies is judged on what they all agree on, and a method the named file only CALLS is left to `methodRealization`, which already reports that the body is not here; a method whose conformance dial is off (its own, else its implementation's) is not judged.",
|
|
30362
30839
|
codes: [
|
|
30363
30840
|
{
|
|
30364
30841
|
code: "UNREALIZED_PARAM",
|
|
@@ -30380,6 +30857,12 @@ var paramConformanceRule = {
|
|
|
30380
30857
|
summary: "A contract parameter and the one realizing it agree on position and type but not on name \u2014 the contract, the ERD and every brief say one word while the code says another",
|
|
30381
30858
|
carryable: true
|
|
30382
30859
|
},
|
|
30860
|
+
{
|
|
30861
|
+
code: "UNUSED_INJECTED_PARAM",
|
|
30862
|
+
defaultSeverity: "warning",
|
|
30863
|
+
summary: "An implementation declares an injected parameter that none of its realizing functions takes \u2014 stale linkage that would wave through the next parameter of that name, wherever it appears",
|
|
30864
|
+
carryable: true
|
|
30865
|
+
},
|
|
30383
30866
|
{
|
|
30384
30867
|
code: "PARAM_OPTIONALITY",
|
|
30385
30868
|
defaultSeverity: "warning",
|
|
@@ -30426,11 +30909,34 @@ var paramConformanceRule = {
|
|
|
30426
30909
|
}
|
|
30427
30910
|
return dialect.agrees(realized.type, stated.expression, names);
|
|
30428
30911
|
};
|
|
30429
|
-
const
|
|
30430
|
-
|
|
30912
|
+
const expected = (declared) => {
|
|
30913
|
+
let expression = parseTypePosition(declared.type, "param", !!declared.optional).expression;
|
|
30914
|
+
while (expression && (expression.form === "optional" || expression.form === "result")) expression = expression.args[0];
|
|
30915
|
+
if (!expression) return void 0;
|
|
30916
|
+
if (expression.form === "primitive") {
|
|
30917
|
+
const loose = {
|
|
30918
|
+
date: ["string", "object"],
|
|
30919
|
+
datetime: ["string", "object"],
|
|
30920
|
+
duration: ["number", "string"],
|
|
30921
|
+
bytes: ["object", "list", "string"]
|
|
30922
|
+
};
|
|
30923
|
+
if (expression.name && loose[expression.name]) return { kinds: new Set(loose[expression.name]), loose: true, told: `a ${expression.name}` };
|
|
30924
|
+
}
|
|
30925
|
+
const kind = expressionKind(expression, (name) => typeKind(typeNamed.get(name)));
|
|
30926
|
+
if (expression.form === "named" && expression.name) {
|
|
30927
|
+
const type = typeNamed.get(expression.name);
|
|
30928
|
+
const fields = (type?.fields ?? []).map((field) => field && typeof field === "object" ? field.name : void 0).filter((name) => typeof name === "string");
|
|
30929
|
+
if (fields.length > 0 && kind === "object") return { kinds: /* @__PURE__ */ new Set(["object"]), fields, told: `the record "${expression.name}"` };
|
|
30930
|
+
}
|
|
30931
|
+
return kind ? { kinds: /* @__PURE__ */ new Set([kind]), told: aKind(kind) } : void 0;
|
|
30932
|
+
};
|
|
30933
|
+
const takenBy = /* @__PURE__ */ new Map();
|
|
30934
|
+
for (const { implementation, method: method2, component, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
|
|
30935
|
+
const tier = method2.conformance ?? implementation.conformance ?? defaultConformanceTier(component);
|
|
30936
|
+
if (tier === "off") continue;
|
|
30431
30937
|
const contract = ctx.interfaceMap.get(implementation.contract);
|
|
30938
|
+
if (!sourceFile) continue;
|
|
30432
30939
|
const declared = contract?.methods.find((m) => m.name === method2.name)?.params ?? [];
|
|
30433
|
-
if (declared.length === 0 || !sourceFile) continue;
|
|
30434
30940
|
const file = pathKey(sourceFile);
|
|
30435
30941
|
const facts = code.factsAt(file);
|
|
30436
30942
|
if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
|
|
@@ -30439,14 +30945,19 @@ var paramConformanceRule = {
|
|
|
30439
30945
|
if (!signatures || !Object.prototype.hasOwnProperty.call(signatures, symbol)) continue;
|
|
30440
30946
|
const candidates = publishedBodies(signatures[symbol], facts.functionBodies?.[symbol], new Set(facts.exportedNames), new Set(Object.keys(facts.typeShapes ?? {})));
|
|
30441
30947
|
if (candidates.length === 0) continue;
|
|
30442
|
-
const
|
|
30948
|
+
const taken = takenBy.get(implementation.id) ?? /* @__PURE__ */ new Set();
|
|
30949
|
+
for (const candidate of candidates) for (const param of candidate) if (param.name !== void 0) taken.add(bare(param.name));
|
|
30950
|
+
takenBy.set(implementation.id, taken);
|
|
30951
|
+
if (declared.length === 0) continue;
|
|
30952
|
+
const injected = new Set((implementation.injectedParams ?? []).map(bare));
|
|
30443
30953
|
const typeAgrees = typeAgreesIn(dialectOf(facts));
|
|
30444
|
-
const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees,
|
|
30954
|
+
const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected));
|
|
30445
30955
|
const subject = candidates.length > 1 ? `every function called "${symbol}" in "${file}"` : `the function "${symbol}" in "${file}"`;
|
|
30446
30956
|
const opening = subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
30447
30957
|
const transport = agreed(judgements, "transport").map((found) => found.told);
|
|
30448
30958
|
const wiring = judgements[0].wiring;
|
|
30449
|
-
const
|
|
30959
|
+
const trailing = judgements[0].trailing;
|
|
30960
|
+
const greenPath = transport.length > 0 ? ` If ${transport.map((name) => `"${name}"`).join(" and ")} ${transport.length === 1 ? "is a handle" : "are handles"} whatever serves this method hands it (a request, a URL, a response) rather than an argument of the contract's caller, the one green path is: name ${transport.length === 1 ? "it" : "them"} in the implementation's \`injectedParams\` \u2014 [${[...wiring, ...transport].join(", ")}] \u2014 and take the contract's own parameters after ${transport.length === 1 ? "it" : "them"}, in order: ${symbol}(${[...wiring, ...transport, ...declared.map((param) => `${param.name}${param.optional ? "?" : ""}`), ...trailing].join(", ")}), the router unpacking the path, query and body into them \u2014 or, where the framework hands the function the handles alone, read each of the contract's parameters off the injected request by its own name (\`${transport[0]}.params.${declared[0]?.name ?? "id"}\`), which realizes it through the handle. An injected name matches with or without its leading underscore, and a handle the function never reads may carry one instead.` : "";
|
|
30450
30961
|
const unrealized = agreed(judgements, "unrealized");
|
|
30451
30962
|
if (unrealized.length > 0) {
|
|
30452
30963
|
ctx.addIssue(
|
|
@@ -30464,7 +30975,7 @@ var paramConformanceRule = {
|
|
|
30464
30975
|
ctx.addIssue(
|
|
30465
30976
|
"warning",
|
|
30466
30977
|
"UNDECLARED_PARAM",
|
|
30467
|
-
`${opening} realizing method "${method2.name}" of contract "${implementation.contract}" takes ${undeclared.length} parameter(s) the contract does not declare and no declared injection accounts for \u2014 ${undeclared.map((found) => found.told).join(", ")}. An argument a caller must supply that the design never mentions is how a credential ends up in a signature nobody has read against its contract. Declare it on the contract, name it in the implementation's \`injectedParams\` when whatever wires this component up supplies it (a
|
|
30978
|
+
`${opening} realizing method "${method2.name}" of contract "${implementation.contract}" takes ${undeclared.length} parameter(s) the contract does not declare and no declared injection accounts for \u2014 ${undeclared.map((found) => found.told).join(", ")}. An argument a caller must supply that the design never mentions is how a credential ends up in a signature nobody has read against its contract. Declare it on the contract, name it in the implementation's \`injectedParams\` when whatever wires this component up supplies it (a leading or trailing run, with or without its underscore), or take it out of the signature.` + greenPath,
|
|
30468
30979
|
implementation.id,
|
|
30469
30980
|
draftContext4,
|
|
30470
30981
|
void 0,
|
|
@@ -30496,6 +31007,22 @@ var paramConformanceRule = {
|
|
|
30496
31007
|
);
|
|
30497
31008
|
}
|
|
30498
31009
|
}
|
|
31010
|
+
for (const implementation of ctx.implementations) {
|
|
31011
|
+
const injected = implementation.injectedParams ?? [];
|
|
31012
|
+
const taken = takenBy.get(implementation.id);
|
|
31013
|
+
if (injected.length === 0 || !taken) continue;
|
|
31014
|
+
const stale = injected.filter((name) => !taken.has(bare(name)));
|
|
31015
|
+
if (stale.length === 0) continue;
|
|
31016
|
+
ctx.addIssue(
|
|
31017
|
+
"warning",
|
|
31018
|
+
"UNUSED_INJECTED_PARAM",
|
|
31019
|
+
`Implementation "${implementation.id}" declares ${stale.length} injected parameter(s) none of its realizing functions takes \u2014 ${stale.map((name) => `"${name}"`).join(", ")}. An injection is wiring the parameter check sets aside wherever it stands at either end of a signature, so one that no function takes waves through the next parameter of that name unread \u2014 a credential included. Drop it from injectedParams, or take it in the handler that is handed it.`,
|
|
31020
|
+
implementation.id,
|
|
31021
|
+
ctx.isImplementationDraft(implementation),
|
|
31022
|
+
void 0,
|
|
31023
|
+
{ at: "injectedParams", covers: stale }
|
|
31024
|
+
);
|
|
31025
|
+
}
|
|
30499
31026
|
}
|
|
30500
31027
|
};
|
|
30501
31028
|
|
|
@@ -30503,7 +31030,7 @@ var paramConformanceRule = {
|
|
|
30503
31030
|
var asyncConformanceRule = {
|
|
30504
31031
|
name: "async-conformance",
|
|
30505
31032
|
judges: "code",
|
|
30506
|
-
description: "Code-to-contract for ASYNC: a contract method whose returns is `async T` is realized by a function that completes later, and one whose returns is not, by one that completes now \u2014 every caller in every language with async/await is shaped by the difference, so a contract that hides it misleads every implementer and translation built from it. Compared with source_file_facts.asyncFunctions for the method's realizing function (under its `symbol`), at exact grade only; where a name has several bodies only what they all agree on is reported, as param-conformance does. A method whose file only calls the function is methodRealization's finding.",
|
|
31033
|
+
description: "Code-to-contract for ASYNC: a contract method whose returns is `async T` is realized by a function that completes later, and one whose returns is not, by one that completes now \u2014 every caller in every language with async/await is shaped by the difference, so a contract that hides it misleads every implementer and translation built from it. Compared with source_file_facts.asyncFunctions for the method's realizing function (under its `symbol`), at exact grade only; where a name has several bodies only what they all agree on is reported, as param-conformance does. A method whose file only calls the function is methodRealization's finding, and a method whose conformance dial is off (its own, else its implementation's) is not judged.",
|
|
30507
31034
|
codes: [
|
|
30508
31035
|
{
|
|
30509
31036
|
code: "ASYNC_MISMATCH",
|
|
@@ -30514,7 +31041,8 @@ var asyncConformanceRule = {
|
|
|
30514
31041
|
],
|
|
30515
31042
|
check(ctx) {
|
|
30516
31043
|
const code = ctx.codeIndex();
|
|
30517
|
-
for (const { implementation, method: method2, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
|
|
31044
|
+
for (const { implementation, method: method2, component, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
|
|
31045
|
+
if ((method2.conformance ?? implementation.conformance ?? defaultConformanceTier(component)) === "off") continue;
|
|
30518
31046
|
const contractMethod2 = ctx.interfaceMap.get(implementation.contract)?.methods.find((m) => m.name === method2.name);
|
|
30519
31047
|
if (!contractMethod2 || typeof contractMethod2.returns !== "string" || !sourceFile) continue;
|
|
30520
31048
|
const file = pathKey(sourceFile);
|
|
@@ -30599,19 +31127,27 @@ function memberSurface(ctx, node, alias) {
|
|
|
30599
31127
|
for (const f of type.fields ?? []) pending3.push(...extractTypeIdentifiers(f.type));
|
|
30600
31128
|
}
|
|
30601
31129
|
}
|
|
30602
|
-
const seen = new Set(types.map((t) => nameKey(t.def.id)));
|
|
31130
|
+
const seen = new Set(types.map((t) => `${t.alias}::${nameKey(t.def.id)}`));
|
|
30603
31131
|
while (pending3.length > 0) {
|
|
30604
31132
|
const ref = pending3.pop();
|
|
30605
31133
|
const type = memberType(ref);
|
|
30606
31134
|
if (!type) continue;
|
|
30607
31135
|
const id = lastSegment4(type.id.replace(/::/g, "."));
|
|
30608
|
-
|
|
30609
|
-
|
|
30610
|
-
|
|
31136
|
+
const owner = type.id.includes("::") ? type.id.slice(0, type.id.indexOf("::")) : ns;
|
|
31137
|
+
const ownerAlias = owner === ns ? alias : ownerAliasOf(ctx, owner);
|
|
31138
|
+
const key2 = `${ownerAlias}::${nameKey(id)}`;
|
|
31139
|
+
if (seen.has(key2)) continue;
|
|
31140
|
+
seen.add(key2);
|
|
31141
|
+
types.push({ alias: ownerAlias, def: asDef(type, id) });
|
|
30611
31142
|
for (const f of type.fields ?? []) pending3.push(...extractTypeIdentifiers(f.type));
|
|
30612
31143
|
}
|
|
30613
31144
|
return { entries, types };
|
|
30614
31145
|
}
|
|
31146
|
+
function ownerAliasOf(ctx, namespace) {
|
|
31147
|
+
const node = (ctx.projectFamily?.nodes ?? []).find((n) => n.namespace === namespace);
|
|
31148
|
+
const declared = [...boundRoot(ctx)?.aliases.entries() ?? []].find(([, key2]) => key2 === namespace)?.[0];
|
|
31149
|
+
return node?.mountAlias ?? declared ?? node?.id ?? namespace;
|
|
31150
|
+
}
|
|
30615
31151
|
function stem(name) {
|
|
30616
31152
|
return nameKey(name).replace(/(?:api|port|client)$/, "") || nameKey(name);
|
|
30617
31153
|
}
|
|
@@ -30726,10 +31262,12 @@ function matchOf(decl, entries, types, tagAlias) {
|
|
|
30726
31262
|
const name = rawName.split(".")[0];
|
|
30727
31263
|
const entry = entries.find((e) => e.alias === alias && (nameKey(e.entry.id) === nameKey(name) || nameKey(e.entry.name) === nameKey(name)));
|
|
30728
31264
|
if (entry) return { entry };
|
|
30729
|
-
const
|
|
30730
|
-
if (
|
|
31265
|
+
const byId = types.find((t) => t.alias === alias && nameKey(t.def.id) === nameKey(name));
|
|
31266
|
+
if (byId) return { type: byId };
|
|
30731
31267
|
const renamed2 = types.find((t) => t.alias === alias && (t.def.formerly ?? []).some((f) => nameKey(f) === nameKey(name)));
|
|
30732
31268
|
if (renamed2) return { renamedType: renamed2 };
|
|
31269
|
+
const byName2 = types.find((t) => t.alias === alias && nameKey(t.def.name) === nameKey(name));
|
|
31270
|
+
if (byName2) return { type: byName2 };
|
|
30733
31271
|
}
|
|
30734
31272
|
const methods = decl.members.filter((m) => m.params !== void 0);
|
|
30735
31273
|
const byName = stem(decl.name);
|
|
@@ -30774,7 +31312,8 @@ function declarationDrift(decl, entries, types, tagAlias) {
|
|
|
30774
31312
|
const type = match2.type ?? match2.renamedType;
|
|
30775
31313
|
if (type) {
|
|
30776
31314
|
const renamedFrom = formerType(decl.name, [type]) ?? (match2.renamedType ? type : void 0);
|
|
30777
|
-
const
|
|
31315
|
+
const now = nameKey(type.def.name) === nameKey(decl.name) ? type.def.id : type.def.name;
|
|
31316
|
+
const named2 = renamedFrom ? [`the type "${decl.name}" was renamed to "${now}" (${type.alias}::${type.def.id}) \u2014 follow the rename`] : [];
|
|
30778
31317
|
const drift = decl.kind === "alias" || decl.kind === "enum" ? named2 : [...named2, ...fieldDrift(decl.members, type, types)];
|
|
30779
31318
|
return { against: `${type.alias}::${type.def.id}`, drift };
|
|
30780
31319
|
}
|
|
@@ -30831,7 +31370,7 @@ function unreadReason(module2, reach2) {
|
|
|
30831
31370
|
var bindingModulesRule = {
|
|
30832
31371
|
name: "binding-modules",
|
|
30833
31372
|
judges: "code",
|
|
30834
|
-
description: "Code-to-pin for a consumer's hand-written binding module (implementation_spec.bindings): the one file where the consumer's code spells another project's names is compared with what the projects the implementation's component reaches publish, so a re-pin (or a member's edit) that renamed, removed or reshaped a name the binding still declares is reported instead of surviving tsc and the tests on the binding's own stale declarations. A component reaches a project through every `alias::name` its specs write: a dependsOn, owns, narrative or declared call, an implements, and every named type its contract's params and returns write, directly or through the bound project's own types they name (so a binding to a types-only contracts library is reached by the types a contract names). A pinned external is compared with its pinned snapshot. A contained MEMBER project (an alias the bound root declares under `members` \u2014 named by that alias or by the member's project id \u2014 read live and never pinned) is compared the same way against what its L0 export table exports now: each exported contract's methods with their params and rename traces, each exported type's fields with theirs, so a parameter, field or method rename in a member reaches a member consumer at validate time, with the rename to follow. Each binding declaration is matched to a reached interface or type by the `alias::name` its doc comment names, else by name (nameKey of the interface's id or name, a type's id or name, with an Api/Port/Client suffix ignored), else \u2014 for an interface with methods \u2014 by the reached interface most of its member names belong to (a trace in `formerly` counts); an exported function is matched by name against the methods of every reached interface, or, when its doc comment names `alias::name` (or `alias::name.method`), against that interface alone. A matched member that is a former name is reported with the rename to follow; a member of a matched interface it does not hold, a function its tagged interface no longer holds, and a function or member a pinned entry's `retired` list names (a method the producer removed since an earlier pin) are reported removed; a parameter list whose arity or names differ from the method's (a param's previousNames giving the rename) is reported; a type declaration is compared field by field \u2014 a field the matched type does not declare (a field's `formerly` giving the rename), a field it declares that the binding lacks, a declaration still named by the type's former name, and a field whose declared type names a type the producer renamed are each reported with the new name. Names compare by nameKey, so snake_case and camelCase spellings of one name agree. A declaration nothing matches is the binding's own plumbing and is never judged. A binding that cannot be read, is not TypeScript or JavaScript, declares nothing the reader compares (naming each export form it met but cannot see into, such as a CommonJS `module.exports = require(\u2026)`), or whose component reaches neither a pinned external nor a member project is reported once as unread \u2014 a member is named as a member (one that exports nothing is told to export there), and pinning is suggested only for a declared external, never for a member \u2014 a notice, so a planned binding passes a design-only gate.",
|
|
31373
|
+
description: "Code-to-pin for a consumer's hand-written binding module (implementation_spec.bindings): the one file where the consumer's code spells another project's names is compared with what the projects the implementation's component reaches publish, so a re-pin (or a member's edit) that renamed, removed or reshaped a name the binding still declares is reported instead of surviving tsc and the tests on the binding's own stale declarations. A component reaches a project through every `alias::name` its specs write: a dependsOn, owns, narrative or declared call, an implements, and every named type its contract's params and returns write, directly or through the bound project's own types they name (so a binding to a types-only contracts library is reached by the types a contract names). A pinned external is compared with its pinned snapshot. A contained MEMBER project (an alias the bound root declares under `members` \u2014 named by that alias or by the member's project id \u2014 read live and never pinned) is compared the same way against what its L0 export table exports now: each exported contract's methods with their params and rename traces, each exported type's fields with theirs (a type its exports name that another project owns is that project's, compared and named under the alias the bound root reaches it by, never the member's), so a parameter, field or method rename in a member reaches a member consumer at validate time, with the rename to follow. Each binding declaration is matched to a reached interface or type by the `alias::name` its doc comment names (a public name or type id, never a display name: a tag naming a type's former id is that type under its retired name), else by name (nameKey of the interface's id or name, a type's id or name, with an Api/Port/Client suffix ignored), else \u2014 for an interface with methods \u2014 by the reached interface most of its member names belong to (a trace in `formerly` counts); an exported function is matched by name against the methods of every reached interface, or, when its doc comment names `alias::name` (or `alias::name.method`), against that interface alone. A matched member that is a former name is reported with the rename to follow; a member of a matched interface it does not hold, a function its tagged interface no longer holds, and a function or member a pinned entry's `retired` list names (a method the producer removed since an earlier pin) are reported removed; a parameter list whose arity or names differ from the method's (a param's previousNames giving the rename) is reported; a type declaration is compared field by field \u2014 a field the matched type does not declare (a field's `formerly` giving the rename), a field it declares that the binding lacks, a declaration still named by the type's former name, and a field whose declared type names a type the producer renamed are each reported with the new name. Names compare by nameKey, so snake_case and camelCase spellings of one name agree. A declaration nothing matches is the binding's own plumbing and is never judged. A binding that cannot be read, is not TypeScript or JavaScript, declares nothing the reader compares (naming each export form it met but cannot see into, such as a CommonJS `module.exports = require(\u2026)`), or whose component reaches neither a pinned external nor a member project is reported once as unread \u2014 a member is named as a member (one that exports nothing is told to export there), and pinning is suggested only for a declared external, never for a member \u2014 a notice, so a planned binding passes a design-only gate.",
|
|
30835
31374
|
codes: [
|
|
30836
31375
|
{ code: "BINDING_DRIFT", defaultSeverity: "warning", summary: "A binding module declares a producer name its pin (or a member project's live export) renamed (with the rename to follow) or removed, a parameter list that differs from the method's, or a type whose name or fields differ from the producer's type \u2014 code calling into another project through a shape the producer no longer has" },
|
|
30837
31376
|
{ code: "BINDING_UNREAD", defaultSeverity: "notice", summary: "A binding module an implementation names could not be compared: it is not on disk yet, escapes the root, cannot be read, is in a language the reader does not read, declares nothing the reader compares (naming the export forms it cannot see into), or its component reaches neither a pinned external nor a member project that exports something" }
|
|
@@ -30867,7 +31406,7 @@ var bindingModulesRule = {
|
|
|
30867
31406
|
...reach2.pins.flatMap((p) => p.snapshot.types.map((def) => ({ alias: p.alias, def }))),
|
|
30868
31407
|
...reach2.members.flatMap((m) => m.surface.types)
|
|
30869
31408
|
];
|
|
30870
|
-
const live = new Set(reach2.members.
|
|
31409
|
+
const live = new Set(reach2.members.flatMap((m) => [m.alias, ...m.surface.types.map((t) => t.alias)]));
|
|
30871
31410
|
const tagAlias = (alias) => {
|
|
30872
31411
|
if (reach2.pins.some((p) => p.alias === alias)) return alias;
|
|
30873
31412
|
return reach2.members.find((m) => m.alias === alias || m.node.namespace === alias || m.node.id === alias)?.alias ?? alias;
|
|
@@ -30912,7 +31451,7 @@ function answers(route2, endpoint) {
|
|
|
30912
31451
|
var routeCoverageRule = {
|
|
30913
31452
|
name: "route-coverage",
|
|
30914
31453
|
judges: "code",
|
|
30915
|
-
description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract (including a write) could run for months without a single rule noticing. The Portal's own implementation names its router
|
|
31454
|
+
description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract (including a write) could run for months without a single rule noticing. The Portal's own implementation names its router (`router`, code linkage): an entry its own files export, an entry or route table another module exports (`<module>#<name>`, a central routes.ts), or a module alone, every route-bearing export of which is read as one router; the routes are read out of that entry and the functions of its file it calls by name. A router several Portals name is read once against them all: a route is declared when any of them declares it, and an endpoint is unrouted per Portal. Two idioms are read: guards (a method comparison with comparisons on the path's split segments), where a prefix the router strips before splitting the path is folded in front; and a route table \u2014 a const the router names, one it returns or states in place unnamed, or the table the linkage names itself \u2014 (an array of objects pairing a method with a `/a/:b` template or an anchored regular expression, or an object keyed `VERB /path`, a key written as a template literal or a concatenation over constants settling like any other value), read only when every entry settles. A table is written under the ONE prefix its router strips off the path before matching it (`path.slice(BASE.length)`, `.replace(BASE, '')`), which is folded in front of every entry that does not already state it, so a stripped prefix that is not the Portal's basePath is read as the different URL it is, and the finding names both. Every compared value is a literal or one the code's constants, concatenations and template literals settle \u2014 through the type checker, any module's constant and any expression it types as one string literal \u2014 and never a guess. A route that states the whole path is read under the Portal's basePath, which the contract's endpoint paths are written beneath; a leading path segment a guard router never checks is completed from the first segments of the Portal's own HTTP endpoint paths, the segments whatever serves the Portal routes on. Which process serves which Portal is implementation, so no listener is consulted. A router that yields no route in either idiom is reported as unread, never passed: a check that cannot see a router must say so rather than stay quiet.",
|
|
30916
31455
|
codes: [
|
|
30917
31456
|
{
|
|
30918
31457
|
code: "UNDECLARED_ROUTE",
|
|
@@ -30938,40 +31477,42 @@ var routeCoverageRule = {
|
|
|
30938
31477
|
check(ctx) {
|
|
30939
31478
|
const code = ctx.codeIndex();
|
|
30940
31479
|
const realization = ctx.realizationIndex();
|
|
31480
|
+
const groups = /* @__PURE__ */ new Map();
|
|
30941
31481
|
for (const portal of ctx.components) {
|
|
30942
31482
|
if (portal.componentType !== "Portal") continue;
|
|
30943
31483
|
for (const impl of realization.implementationsOf(portal.id)) {
|
|
30944
31484
|
const via = impl.router;
|
|
30945
31485
|
if (!via) continue;
|
|
30946
|
-
const
|
|
30947
|
-
const
|
|
31486
|
+
const linkage = routerLinkage(via);
|
|
31487
|
+
const files = linkage.file ? [pathKey(linkage.file)] : realization.filesOf(portal.id).map(pathKey);
|
|
31488
|
+
const exact = files.filter((file) => {
|
|
30948
31489
|
const facts = code.factsAt(file);
|
|
30949
|
-
return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact"
|
|
31490
|
+
return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
|
|
30950
31491
|
});
|
|
31492
|
+
const holds = (file, name) => {
|
|
31493
|
+
const facts = code.factsAt(file);
|
|
31494
|
+
return !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, name) || !!facts.functionRoutes && Object.prototype.hasOwnProperty.call(facts.functionRoutes, name);
|
|
31495
|
+
};
|
|
31496
|
+
const holders = linkage.name !== void 0 ? exact.filter((file) => holds(file, linkage.name)) : exact;
|
|
30951
31497
|
if (holders.length === 0) continue;
|
|
31498
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
30952
31499
|
const read2 = holders.flatMap((file) => {
|
|
30953
|
-
const
|
|
31500
|
+
const facts = code.factsAt(file);
|
|
31501
|
+
const routes = facts.functionRoutes;
|
|
30954
31502
|
if (!routes) return [];
|
|
30955
|
-
const
|
|
30956
|
-
|
|
30957
|
-
|
|
31503
|
+
const entries = linkage.name !== void 0 ? [linkage.name] : facts.exportedNames.filter((name) => Object.prototype.hasOwnProperty.call(routes, name));
|
|
31504
|
+
const names = new Set(entries);
|
|
31505
|
+
for (const entry of entries) {
|
|
31506
|
+
for (const site of closedCallSites(code, file, entry) ?? []) {
|
|
31507
|
+
if (!site.member && pathKey(site.from ?? file) === file) names.add(site.name);
|
|
31508
|
+
}
|
|
31509
|
+
}
|
|
31510
|
+
for (const name of names) {
|
|
31511
|
+
const stripped = facts.routePrefixes?.[name];
|
|
31512
|
+
if (stripped !== void 0 && Object.prototype.hasOwnProperty.call(facts.routePrefixes, name)) prefixes.add(stripped);
|
|
30958
31513
|
}
|
|
30959
31514
|
return [...names].flatMap((name) => Object.prototype.hasOwnProperty.call(routes, name) ? routes[name] : []);
|
|
30960
31515
|
});
|
|
30961
|
-
const draftContext4 = ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl);
|
|
30962
|
-
const where = holders.map((file) => `"${file}"`).join(", ");
|
|
30963
|
-
if (read2.length === 0) {
|
|
30964
|
-
ctx.addIssue(
|
|
30965
|
-
"warning",
|
|
30966
|
-
"UNREADABLE_ROUTER",
|
|
30967
|
-
`Portal "${portal.id}" names its router "${via}" (in ${where}), but no branch of "${via}" reads as a route \u2014 so none of its routes were checked against the contract at all. Only two idioms are read, in the entry and in the functions of its file it calls by name: an \`if\` whose conditions, together with those of every enclosing \`if\`, compare \`<request>.method\` with a string and \`parts[i]\` / \`parts.length\` with values the code spells out or its constants settle; and a route table it names \u2014 an array of objects pairing a method with a path pattern (a \`/a/:b\` template or an anchored regular expression), or an object keyed \`VERB /path\` \u2014 every entry of which settles. Silence here would read as a clean router; it is only one this analysis cannot see. Write the router in one of those idioms, or carry this finding with the reason it cannot be.`,
|
|
30968
|
-
anchor,
|
|
30969
|
-
draftContext4,
|
|
30970
|
-
void 0,
|
|
30971
|
-
{ at: via }
|
|
30972
|
-
);
|
|
30973
|
-
continue;
|
|
30974
|
-
}
|
|
30975
31516
|
const endpoints = ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
|
|
30976
31517
|
const endpoint = method2.endpoint;
|
|
30977
31518
|
if (endpoint?.transport !== "HTTP") return [];
|
|
@@ -30982,32 +31523,83 @@ var routeCoverageRule = {
|
|
|
30982
31523
|
}];
|
|
30983
31524
|
});
|
|
30984
31525
|
const heads = [...new Set(endpoints.map((endpoint) => endpoint.segments[0]).filter((head2) => !!head2 && head2 !== "*"))].sort();
|
|
30985
|
-
const
|
|
30986
|
-
|
|
30987
|
-
|
|
30988
|
-
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
31000
|
-
|
|
31526
|
+
const router = {
|
|
31527
|
+
portal,
|
|
31528
|
+
impl,
|
|
31529
|
+
via,
|
|
31530
|
+
holders,
|
|
31531
|
+
read: read2,
|
|
31532
|
+
endpoints,
|
|
31533
|
+
heads,
|
|
31534
|
+
base: (portal.basePath ?? "").split("/").filter(Boolean),
|
|
31535
|
+
draftContext: ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
|
|
31536
|
+
...prefixes.size === 1 ? { prefix: [...prefixes][0] } : {}
|
|
31537
|
+
};
|
|
31538
|
+
const key2 = linkage.file ? `${pathKey(linkage.file)}#${linkage.name ?? "*"}` : `${impl.id}#${via}`;
|
|
31539
|
+
groups.set(key2, [...groups.get(key2) ?? [], router]);
|
|
31540
|
+
}
|
|
31541
|
+
}
|
|
31542
|
+
for (const routers of groups.values()) {
|
|
31543
|
+
const [first] = routers;
|
|
31544
|
+
const where = first.holders.map((file) => `"${file}"`).join(", ");
|
|
31545
|
+
const via = first.via;
|
|
31546
|
+
if (first.read.length === 0) {
|
|
31547
|
+
for (const router of routers) {
|
|
31001
31548
|
ctx.addIssue(
|
|
31002
31549
|
"warning",
|
|
31003
|
-
"
|
|
31004
|
-
`Portal "${portal.id}"
|
|
31005
|
-
|
|
31006
|
-
|
|
31550
|
+
"UNREADABLE_ROUTER",
|
|
31551
|
+
`Portal "${router.portal.id}" names its router "${via}" (in ${where}), but no branch of "${via}" reads as a route \u2014 so none of its routes were checked against the contract at all. Only two idioms are read, in the entry and in the functions of its file it calls by name: an \`if\` whose conditions, together with those of every enclosing \`if\`, compare \`<request>.method\` with a string and \`parts[i]\` / \`parts.length\` with values the code spells out or its constants settle; and a route table \u2014 an array of objects pairing a method with a path pattern (a \`/a/:b\` template or an anchored regular expression), or an object keyed \`VERB /path\` (a key may be a template literal over constants), every entry of which settles \u2014 whether a const names it, the function returns it in place, or the router linkage names the table itself (\`<module>#<table>\`, or a module whose exports are its tables). Silence here would read as a clean router; it is only one this analysis cannot see. Write the router in one of those idioms, or carry this finding with the reason it cannot be.`,
|
|
31552
|
+
router.impl.id,
|
|
31553
|
+
router.draftContext,
|
|
31007
31554
|
void 0,
|
|
31008
|
-
{ at: via
|
|
31555
|
+
{ at: via }
|
|
31009
31556
|
);
|
|
31010
31557
|
}
|
|
31558
|
+
continue;
|
|
31559
|
+
}
|
|
31560
|
+
const prefixSegments = first.prefix !== void 0 ? first.prefix.split("/").filter(Boolean) : [];
|
|
31561
|
+
const routes = first.read.map((route2) => {
|
|
31562
|
+
if (!route2.table || prefixSegments.length === 0) return route2;
|
|
31563
|
+
const already = prefixSegments.every((segment, index) => route2.segments[index] === segment);
|
|
31564
|
+
return already ? route2 : { ...route2, segments: [...prefixSegments, ...route2.segments], fullPath: true };
|
|
31565
|
+
});
|
|
31566
|
+
const servedBy = (router) => routes.map((route2) => completed(route2, router.heads, router.base));
|
|
31567
|
+
const strip = (router) => {
|
|
31568
|
+
if (first.prefix === void 0 || !first.read.some((route2) => route2.table)) return "";
|
|
31569
|
+
const basePath = `/${router.base.join("/")}`;
|
|
31570
|
+
if (router.base.length === prefixSegments.length && router.base.every((segment, index) => segment === prefixSegments[index])) return "";
|
|
31571
|
+
return ` The router strips "${first.prefix}" off the path before reading its table, where the Portal's basePath is "${basePath}" \u2014 so every table route is served under "${first.prefix}", which is not where the contract puts it.`;
|
|
31572
|
+
};
|
|
31573
|
+
const portals = routers.map((router) => `"${router.portal.id}"`).join(", ");
|
|
31574
|
+
const declaredBySome = (index) => routers.some((router) => {
|
|
31575
|
+
const variants = servedBy(router)[index];
|
|
31576
|
+
return variants.some((route2) => router.endpoints.some((endpoint) => answers(route2, endpoint)));
|
|
31577
|
+
});
|
|
31578
|
+
const undeclared = [...new Set(routes.map((_, index) => index).filter((index) => !declaredBySome(index)).flatMap((index) => servedBy(first)[index].map((route2) => route2.key)))].sort();
|
|
31579
|
+
if (undeclared.length > 0) {
|
|
31580
|
+
ctx.addIssue(
|
|
31581
|
+
"warning",
|
|
31582
|
+
"UNDECLARED_ROUTE",
|
|
31583
|
+
`Portal "${first.portal.id}"'s router "${via}" (${where}) answers ${undeclared.length} route(s) no contract endpoint of ${routers.length > 1 ? `the Portals reading it (${portals})` : `"${first.portal.id}"`} declares \u2014 ${undeclared.map((key2) => `"${key2}"`).join(", ")}. A surface the code serves and the design never promised is how a write runs with no contract: no brief, no auth review, no rule reading it. Declare each as a contract method with its endpoint, or take the route out of the router.${strip(first)}`,
|
|
31584
|
+
first.impl.id,
|
|
31585
|
+
first.draftContext,
|
|
31586
|
+
void 0,
|
|
31587
|
+
{ at: via, covers: undeclared }
|
|
31588
|
+
);
|
|
31589
|
+
}
|
|
31590
|
+
for (const router of routers) {
|
|
31591
|
+
const served = servedBy(router);
|
|
31592
|
+
const unrouted = [...new Set(router.endpoints.filter((endpoint) => !served.some((variants) => variants.some((route2) => answers(route2, endpoint)))).map((endpoint) => endpoint.key))].sort();
|
|
31593
|
+
if (unrouted.length === 0) continue;
|
|
31594
|
+
ctx.addIssue(
|
|
31595
|
+
"warning",
|
|
31596
|
+
"UNROUTED_ENDPOINT",
|
|
31597
|
+
`Portal "${router.portal.id}" binds ${unrouted.length} HTTP endpoint(s) that no route of its router "${via}" (${where}) answers \u2014 ${unrouted.map((key2) => `"${key2}"`).join(", ")}. The contract promises a route no request can reach. Route it in the router, or drop the endpoint from the contract.${strip(router)}`,
|
|
31598
|
+
router.impl.id,
|
|
31599
|
+
router.draftContext,
|
|
31600
|
+
void 0,
|
|
31601
|
+
{ at: via, covers: unrouted }
|
|
31602
|
+
);
|
|
31011
31603
|
}
|
|
31012
31604
|
}
|
|
31013
31605
|
}
|
|
@@ -31017,7 +31609,7 @@ var routeCoverageRule = {
|
|
|
31017
31609
|
var unclaimedSourceRule = {
|
|
31018
31610
|
name: "unclaimed-source",
|
|
31019
31611
|
judges: "code",
|
|
31020
|
-
description: "Code\u2194spec Level 1, asked the other way round: every source file under the project's declared source roots must be named by some spec \u2014 an implementation's sourcePath, a method's, a simPath, a binding module an implementation declares in `bindings`, or a type's \u2014 or be carried in the frozen `rules.conformance.unclaimed` list. A pure re-export barrel declares nothing of its own and only republishes other modules, so it has no code to claim and is exempt (exact grade only: a weaker grade cannot tell a barrel from a file it failed to parse). Declaring no source roots leaves the check silent, which is what makes it opt-in; the unclaimed list is a one-way debt register, so an entry the walk no longer finds unclaimed must be deleted and the list can only shrink.",
|
|
31612
|
+
description: "Code\u2194spec Level 1, asked the other way round: every source file under the project's declared source roots must be named by some spec \u2014 an implementation's sourcePath, a method's, a simPath, a binding module an implementation declares in `bindings`, the module a Portal's router linkage names (`<module>#<name>`, or the module alone), or a type's \u2014 or be carried in the frozen `rules.conformance.unclaimed` list. A pure re-export barrel declares nothing of its own and only republishes other modules, so it has no code to claim and is exempt (exact grade only: a weaker grade cannot tell a barrel from a file it failed to parse). Declaring no source roots leaves the check silent, which is what makes it opt-in; the unclaimed list is a one-way debt register, so an entry the walk no longer finds unclaimed must be deleted and the list can only shrink.",
|
|
31021
31613
|
codes: [
|
|
31022
31614
|
{ code: "UNCLAIMED_SOURCE_FILE", defaultSeverity: "warning", summary: "A source file under a declared source root that no spec names and the frozen unclaimed list does not carry \u2014 code nobody designed" },
|
|
31023
31615
|
{ code: "STALE_UNCLAIMED_ENTRY", defaultSeverity: "warning", summary: "An entry of the frozen unclaimed list names a file that is now claimed, or that the source-root walk no longer finds \u2014 delete it, the list only shrinks" }
|
|
@@ -31031,6 +31623,8 @@ var unclaimedSourceRule = {
|
|
|
31031
31623
|
for (const file of implementationSourceFiles(impl)) claimed.add(pathKey(file));
|
|
31032
31624
|
if (impl.simPath) claimed.add(pathKey(impl.simPath));
|
|
31033
31625
|
for (const file of impl.bindings ?? []) claimed.add(pathKey(file));
|
|
31626
|
+
const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
|
|
31627
|
+
if (routerFile) claimed.add(pathKey(routerFile));
|
|
31034
31628
|
}
|
|
31035
31629
|
for (const type of ctx.types) {
|
|
31036
31630
|
for (const file of typeSourceFiles(type)) claimed.add(pathKey(file));
|
|
@@ -31066,7 +31660,7 @@ var NOTHING_PROMISED = /* @__PURE__ */ new Set();
|
|
|
31066
31660
|
var exportConformanceRule = {
|
|
31067
31661
|
name: "export-conformance",
|
|
31068
31662
|
judges: "code",
|
|
31069
|
-
description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code (it asks whether the code holds what a spec claims), so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it: the value that composes it, which `symbol` cannot name because `symbol` names the function inside. A Portal implementation's `router` names the entry whatever serves the Portal calls to hand it its requests. Both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
|
|
31663
|
+
description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code (it asks whether the code holds what a spec claims), so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it: the value that composes it, which `symbol` cannot name because `symbol` names the function inside. A Portal implementation's `router` names the entry whatever serves the Portal calls to hand it its requests \u2014 in the Portal's own files, or, written `<module>#<name>`, in the module it names, which must export that name (a module named alone must be on disk). Both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
|
|
31070
31664
|
codes: [
|
|
31071
31665
|
{
|
|
31072
31666
|
code: "UNDECLARED_EXPORT",
|
|
@@ -31132,6 +31726,22 @@ var exportConformanceRule = {
|
|
|
31132
31726
|
const contract = ctx.interfaceMap.get(implementation.contract);
|
|
31133
31727
|
const portal = contract ? ctx.componentMap.get(contract.component) : void 0;
|
|
31134
31728
|
if (!portal || portal.componentType !== "Portal" || ctx.isInChainedSubproject(portal.subsystem)) continue;
|
|
31729
|
+
const linkage = routerLinkage(handle);
|
|
31730
|
+
if (linkage.file) {
|
|
31731
|
+
const file = pathKey(linkage.file);
|
|
31732
|
+
const facts = code.factsAt(file);
|
|
31733
|
+
const missing = !facts || facts.status === "missing";
|
|
31734
|
+
const exactHere = !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
|
|
31735
|
+
if (!missing && (!exactHere || linkage.name === void 0 || facts.exportedNames.includes(linkage.name))) continue;
|
|
31736
|
+
ctx.addIssue(
|
|
31737
|
+
"warning",
|
|
31738
|
+
"UNREALIZED_EXPORT_HANDLE",
|
|
31739
|
+
`Portal "${portal.id}" names its router "${handle}" (implementation "${implementation.id}"), but ` + (missing ? `"${file}" is not on disk.` : `"${file}" exports no "${linkage.name}".`) + " A `router` names the entry or the route table whatever serves the Portal hands its requests to, so one nobody can import promises a route nobody can reach, and it allows nothing either. Name an export the module actually has, or drop `router` if the Portal is served method by method.",
|
|
31740
|
+
implementation.id,
|
|
31741
|
+
ctx.isImplementationDraft(implementation) || ctx.isComponentDraft(portal.id)
|
|
31742
|
+
);
|
|
31743
|
+
continue;
|
|
31744
|
+
}
|
|
31135
31745
|
const files = realization.filesOf(portal.id).map(pathKey);
|
|
31136
31746
|
const exact = files.filter((file) => {
|
|
31137
31747
|
const facts = code.factsAt(file);
|
|
@@ -31388,14 +31998,14 @@ var methodCohesionRule = {
|
|
|
31388
31998
|
if (calledBy.size < 2) continue;
|
|
31389
31999
|
const groups = groupsSharingCollaborators(calledBy).filter((g) => g.length >= 2);
|
|
31390
32000
|
if (groups.length < 2) continue;
|
|
31391
|
-
const
|
|
32001
|
+
const described2 = groups.map((g) => {
|
|
31392
32002
|
const collaborators = [...new Set(g.flatMap((m) => [...calledBy.get(m)]))].sort();
|
|
31393
32003
|
return `${g.join(", ")} (calling ${collaborators.join(", ")})`;
|
|
31394
32004
|
}).join("; ");
|
|
31395
32005
|
ctx.addIssue(
|
|
31396
32006
|
"warning",
|
|
31397
32007
|
"INCOHESIVE_METHODS",
|
|
31398
|
-
`Orchestrator "${component.id}" holds ${groups.length} groups of methods that share no called component: ${
|
|
32008
|
+
`Orchestrator "${component.id}" holds ${groups.length} groups of methods that share no called component: ${described2}. Split it along those groups, move the logic out until every method is a single hand-off (a pure forwarder is exempt), or acknowledge a deliberate facade with a reasoned lint.allow for INCOHESIVE_METHODS.`,
|
|
31399
32009
|
component.id,
|
|
31400
32010
|
ctx.isComponentDraft(component.id)
|
|
31401
32011
|
);
|
|
@@ -32963,6 +33573,7 @@ function compilerIdentity(projectRoot) {
|
|
|
32963
33573
|
}
|
|
32964
33574
|
var HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
32965
33575
|
var METHOD_KEYS = /* @__PURE__ */ new Set(["method", "verb", "httpMethod"]);
|
|
33576
|
+
var ROUTER_TEXT = /[Mm][Ee][Tt][Hh][Oo][Dd]|[Vv][Ee][Rr][Bb]|\b(?:GET|POST|PUT|PATCH|DELETE)\b/;
|
|
32966
33577
|
var PATH_KEYS = /* @__PURE__ */ new Set(["path", "pattern", "route", "url", "template"]);
|
|
32967
33578
|
var SETTLE_DEPTH = 8;
|
|
32968
33579
|
var LITERAL_SEGMENT = /^[\w.~@!$&'+,;=%-]+$/;
|
|
@@ -33142,7 +33753,7 @@ function tableEntry(ts, entry, initializerOf, literalOf) {
|
|
|
33142
33753
|
}
|
|
33143
33754
|
const verb = keyedVerb ?? (verbs.length === 1 ? verbs[0] : void 0);
|
|
33144
33755
|
const route2 = keyedPath ?? (paths.length === 1 ? paths[0] : void 0);
|
|
33145
|
-
return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true } : void 0;
|
|
33756
|
+
return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true, table: true } : void 0;
|
|
33146
33757
|
}
|
|
33147
33758
|
function tableRoutes(ts, value, initializerOf, literalOf) {
|
|
33148
33759
|
const at = bareExpression(ts, value);
|
|
@@ -33159,16 +33770,38 @@ function tableRoutes(ts, value, initializerOf, literalOf) {
|
|
|
33159
33770
|
}
|
|
33160
33771
|
if (ts.isObjectLiteralExpression(at)) {
|
|
33161
33772
|
for (const property of at.properties) {
|
|
33162
|
-
const key2 = property.name ?
|
|
33773
|
+
const key2 = !property.name ? void 0 : ts.isComputedPropertyName(property.name) ? settleString(ts, property.name.expression, initializerOf, literalOf) : propertyKeyText(ts, property.name);
|
|
33163
33774
|
const match2 = key2 ? /^([A-Za-z]+)\s+(\/\S*)$/.exec(key2) : null;
|
|
33164
33775
|
if (!match2 || !HTTP_VERBS.has(match2[1].toUpperCase())) return [];
|
|
33165
33776
|
const segments = templateSegments(match2[2]);
|
|
33166
33777
|
if (!segments) return [];
|
|
33167
|
-
out.push({ verb: match2[1].toUpperCase(), segments, exactLength: true, fullPath: true });
|
|
33778
|
+
out.push({ verb: match2[1].toUpperCase(), segments, exactLength: true, fullPath: true, table: true });
|
|
33168
33779
|
}
|
|
33169
33780
|
}
|
|
33170
33781
|
return out;
|
|
33171
33782
|
}
|
|
33783
|
+
function strippedAnywhere(ts, body, initializerOf, literalOf) {
|
|
33784
|
+
const UNSETTLED = "\0";
|
|
33785
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
33786
|
+
const strip = (node) => {
|
|
33787
|
+
if (namedFunctionNameOf(ts, node) !== void 0 && node !== body) return;
|
|
33788
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
33789
|
+
const name = node.expression.name.text;
|
|
33790
|
+
const [first, second] = node.arguments;
|
|
33791
|
+
if ((name === "slice" || name === "substring" || name === "substr") && node.arguments.length === 1 && first && ts.isPropertyAccessExpression(first) && first.name.text === "length") {
|
|
33792
|
+
prefixes.add(settleString(ts, first.expression, initializerOf, literalOf) ?? UNSETTLED);
|
|
33793
|
+
} else if (name === "replace" && first && second && !ts.isRegularExpressionLiteral(first) && settleString(ts, second, initializerOf, literalOf) === "") {
|
|
33794
|
+
prefixes.add(settleString(ts, first, initializerOf, literalOf) ?? UNSETTLED);
|
|
33795
|
+
}
|
|
33796
|
+
}
|
|
33797
|
+
ts.forEachChild(node, strip);
|
|
33798
|
+
};
|
|
33799
|
+
strip(body);
|
|
33800
|
+
if (prefixes.size !== 1) return void 0;
|
|
33801
|
+
const [prefix] = [...prefixes];
|
|
33802
|
+
if (!prefix.startsWith("/") || prefix.length < 2) return void 0;
|
|
33803
|
+
return prefix.split("/").filter(Boolean).every((segment) => LITERAL_SEGMENT.test(segment)) ? prefix : void 0;
|
|
33804
|
+
}
|
|
33172
33805
|
function strippedPrefix(ts, body, initializerOf, literalOf) {
|
|
33173
33806
|
const sources = [];
|
|
33174
33807
|
const find = (node) => {
|
|
@@ -33279,6 +33912,16 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
|
|
|
33279
33912
|
ts.forEachChild(node, names);
|
|
33280
33913
|
};
|
|
33281
33914
|
names(fn.body);
|
|
33915
|
+
const inPlace = (value) => {
|
|
33916
|
+
for (const route2 of tableRoutes(ts, value, initializerOf, literalOf)) routes.set(routeKey2(route2), route2);
|
|
33917
|
+
};
|
|
33918
|
+
if (!ts.isBlock(fn.body)) inPlace(fn.body);
|
|
33919
|
+
const returns = (node) => {
|
|
33920
|
+
if (namedFunctionNameOf(ts, node) !== void 0) return;
|
|
33921
|
+
if (ts.isReturnStatement(node) && node.expression) inPlace(node.expression);
|
|
33922
|
+
ts.forEachChild(node, returns);
|
|
33923
|
+
};
|
|
33924
|
+
returns(fn.body);
|
|
33282
33925
|
return routes;
|
|
33283
33926
|
}
|
|
33284
33927
|
function walkExact(ts, sourceText, fileName) {
|
|
@@ -33577,6 +34220,47 @@ function walkExact(ts, sourceText, fileName) {
|
|
|
33577
34220
|
const container = containerOf(owner);
|
|
33578
34221
|
return container !== void 0 ? { container } : nested;
|
|
33579
34222
|
};
|
|
34223
|
+
const readsOff = (body, name) => {
|
|
34224
|
+
const out = /* @__PURE__ */ new Set();
|
|
34225
|
+
const visit2 = (node) => {
|
|
34226
|
+
if (out.size >= 64) return;
|
|
34227
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
34228
|
+
const parent = node.parent;
|
|
34229
|
+
const memberName = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node);
|
|
34230
|
+
if (!memberName) {
|
|
34231
|
+
let at = node;
|
|
34232
|
+
for (; ; ) {
|
|
34233
|
+
const up = at.parent;
|
|
34234
|
+
if (!up) break;
|
|
34235
|
+
if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up)) {
|
|
34236
|
+
at = up;
|
|
34237
|
+
continue;
|
|
34238
|
+
}
|
|
34239
|
+
if (ts.isPropertyAccessExpression(up) && up.expression === at) {
|
|
34240
|
+
out.add(up.name.text);
|
|
34241
|
+
at = up;
|
|
34242
|
+
continue;
|
|
34243
|
+
}
|
|
34244
|
+
if (ts.isElementAccessExpression(up) && up.expression === at && ts.isStringLiteralLike(up.argumentExpression)) {
|
|
34245
|
+
out.add(up.argumentExpression.text);
|
|
34246
|
+
at = up;
|
|
34247
|
+
continue;
|
|
34248
|
+
}
|
|
34249
|
+
if (ts.isVariableDeclaration(up) && up.initializer === at && ts.isObjectBindingPattern(up.name)) {
|
|
34250
|
+
for (const element of up.name.elements) {
|
|
34251
|
+
const key2 = element.propertyName ?? element.name;
|
|
34252
|
+
if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
|
|
34253
|
+
}
|
|
34254
|
+
}
|
|
34255
|
+
break;
|
|
34256
|
+
}
|
|
34257
|
+
}
|
|
34258
|
+
}
|
|
34259
|
+
ts.forEachChild(node, visit2);
|
|
34260
|
+
};
|
|
34261
|
+
visit2(body);
|
|
34262
|
+
return [...out].sort();
|
|
34263
|
+
};
|
|
33580
34264
|
const declaredParameters = (fn) => {
|
|
33581
34265
|
const parameters = fn.parameters ?? [];
|
|
33582
34266
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -33598,6 +34282,14 @@ function walkExact(ts, sourceText, fileName) {
|
|
|
33598
34282
|
if (ts.isIdentifier(parameter.name)) {
|
|
33599
34283
|
fact.name = parameter.name.text;
|
|
33600
34284
|
if (!used.has(fact.name) && !used.has("arguments")) fact.unused = true;
|
|
34285
|
+
else if (body) {
|
|
34286
|
+
const read2 = readsOff(body, fact.name);
|
|
34287
|
+
if (read2.length > 0) fact.reads = read2;
|
|
34288
|
+
}
|
|
34289
|
+
}
|
|
34290
|
+
if (parameter.type && ts.isTypeReferenceNode(parameter.type)) {
|
|
34291
|
+
const named2 = ts.isIdentifier(parameter.type.typeName) ? parameter.type.typeName.text : parameter.type.typeName.right.text;
|
|
34292
|
+
if (TRANSPORT_TYPES.has(named2)) fact.transport = true;
|
|
33601
34293
|
}
|
|
33602
34294
|
if (parameter.type) fact.type = parameter.type.getText(sf).replace(/\s+/g, " ").trim();
|
|
33603
34295
|
return fact;
|
|
@@ -33875,14 +34567,24 @@ function walkExact(ts, sourceText, fileName) {
|
|
|
33875
34567
|
ts.forEachChild(node, visit);
|
|
33876
34568
|
};
|
|
33877
34569
|
visit(sf);
|
|
33878
|
-
const
|
|
34570
|
+
const constants = topLevelConstants(ts, sf);
|
|
34571
|
+
const fileValues = fileInitializer(ts, constants);
|
|
34572
|
+
const routePrefixes = /* @__PURE__ */ new Map();
|
|
33879
34573
|
for (const { name, node } of routeBodies) {
|
|
34574
|
+
const body = node.body;
|
|
34575
|
+
const prefix = body ? strippedAnywhere(ts, body, fileValues, void 0) : void 0;
|
|
34576
|
+
if (prefix !== void 0) routePrefixes.set(name, prefix);
|
|
33880
34577
|
const routes = readRoutes(ts, node, fileValues);
|
|
33881
34578
|
if (routes.size === 0) continue;
|
|
33882
34579
|
const known = functionRoutes.get(name) ?? /* @__PURE__ */ new Map();
|
|
33883
34580
|
for (const [key2, route2] of routes) known.set(key2, route2);
|
|
33884
34581
|
functionRoutes.set(name, known);
|
|
33885
34582
|
}
|
|
34583
|
+
for (const [name, initializer] of constants) {
|
|
34584
|
+
if (functionRoutes.has(name)) continue;
|
|
34585
|
+
const table = tableRoutes(ts, initializer, fileValues, void 0);
|
|
34586
|
+
if (table.length > 0) functionRoutes.set(name, new Map(table.map((route2) => [routeKey2(route2), route2])));
|
|
34587
|
+
}
|
|
33886
34588
|
const composedMembers = (expression, seen) => {
|
|
33887
34589
|
const extensions = [];
|
|
33888
34590
|
const compose3 = (members) => {
|
|
@@ -33941,7 +34643,7 @@ function walkExact(ts, sourceText, fileName) {
|
|
|
33941
34643
|
importBindings.set(name, fact);
|
|
33942
34644
|
}
|
|
33943
34645
|
for (const alias of propertyAliases) if (exported.has(alias.container)) exportAliases.push(alias);
|
|
33944
|
-
return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets: aliasTargets2, implementsClauses, typeShapes, functionParams, paramSites, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
|
|
34646
|
+
return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets: aliasTargets2, implementsClauses, typeShapes, functionParams, paramSites, functionRoutes, routePrefixes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
|
|
33945
34647
|
}
|
|
33946
34648
|
var NO_PACKAGES = /* @__PURE__ */ new Map();
|
|
33947
34649
|
function resolveModule(fromFile, specifier, packages) {
|
|
@@ -34276,7 +34978,7 @@ function checkerOptions(ts, projectRoot) {
|
|
|
34276
34978
|
}
|
|
34277
34979
|
function resolveCalls(ts, files, projectRoot) {
|
|
34278
34980
|
const out = /* @__PURE__ */ new Map();
|
|
34279
|
-
if (files.size === 0) return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
|
|
34981
|
+
if (files.size === 0) return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), prefixes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
|
|
34280
34982
|
const options = checkerOptions(ts, projectRoot);
|
|
34281
34983
|
const settings = JSON.stringify(options);
|
|
34282
34984
|
const host = ts.createCompilerHost(options, true);
|
|
@@ -34296,7 +34998,7 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34296
34998
|
try {
|
|
34297
34999
|
program = ts.createProgram({ rootNames: [...files.keys()], options, host });
|
|
34298
35000
|
} catch {
|
|
34299
|
-
return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
|
|
35001
|
+
return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), prefixes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
|
|
34300
35002
|
}
|
|
34301
35003
|
const checker = program.getTypeChecker();
|
|
34302
35004
|
const assignable = checker.isTypeAssignableTo;
|
|
@@ -34691,6 +35393,26 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34691
35393
|
if (where.container !== void 0) fact.enclosingContainer = where.container;
|
|
34692
35394
|
return fact;
|
|
34693
35395
|
};
|
|
35396
|
+
const opaqueReferenceOf = (node) => {
|
|
35397
|
+
if (!ts.isPropertyAccessExpression(node) || !isValuePosition(node)) return void 0;
|
|
35398
|
+
const symbol = symbolOf(node.name);
|
|
35399
|
+
if (symbol?.declarations?.length) return void 0;
|
|
35400
|
+
const receiver = node.expression;
|
|
35401
|
+
if (!(isOpaque(checker.getTypeAtLocation(receiver)) || rootOpaque(receiver))) return void 0;
|
|
35402
|
+
const fact = {
|
|
35403
|
+
name: node.name.text,
|
|
35404
|
+
written: node.getText().replace(/\s+/g, " ").slice(0, 80),
|
|
35405
|
+
targets: [],
|
|
35406
|
+
reference: true,
|
|
35407
|
+
unresolved: true
|
|
35408
|
+
};
|
|
35409
|
+
const receivers = receiversOf(receiver, node.name.text);
|
|
35410
|
+
if (receivers.length > 0) fact.receivers = receivers;
|
|
35411
|
+
const where = enclosingOf(node);
|
|
35412
|
+
if (where.name !== void 0) fact.enclosing = where.name;
|
|
35413
|
+
if (where.container !== void 0) fact.enclosingContainer = where.container;
|
|
35414
|
+
return fact;
|
|
35415
|
+
};
|
|
34694
35416
|
const resolveFile = (sf) => {
|
|
34695
35417
|
const calls = [];
|
|
34696
35418
|
const visit = (node) => {
|
|
@@ -34699,7 +35421,7 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34699
35421
|
const fact = resolveOne2(node);
|
|
34700
35422
|
if (fact) calls.push(fact);
|
|
34701
35423
|
} else if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node)) {
|
|
34702
|
-
const fact = referenceOf(node) ?? (ts.isElementAccessExpression(node) ? computedAccessOf(node) :
|
|
35424
|
+
const fact = referenceOf(node) ?? (ts.isElementAccessExpression(node) ? computedAccessOf(node) : opaqueReferenceOf(node));
|
|
34703
35425
|
if (fact) calls.push(fact);
|
|
34704
35426
|
}
|
|
34705
35427
|
} catch {
|
|
@@ -34768,6 +35490,7 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34768
35490
|
}
|
|
34769
35491
|
const kinds = /* @__PURE__ */ new Map();
|
|
34770
35492
|
const routes = /* @__PURE__ */ new Map();
|
|
35493
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
34771
35494
|
const crossProjectImports = /* @__PURE__ */ new Map();
|
|
34772
35495
|
const literalOf = (node) => {
|
|
34773
35496
|
const type = checker.getTypeAtLocation(node);
|
|
@@ -34798,24 +35521,42 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34798
35521
|
if (!sf) continue;
|
|
34799
35522
|
const fileKinds = [];
|
|
34800
35523
|
const fileRoutes = /* @__PURE__ */ new Map();
|
|
35524
|
+
const filePrefixes = /* @__PURE__ */ new Map();
|
|
34801
35525
|
const visit = (node) => {
|
|
34802
35526
|
const name = namedFunctionNameOf(ts, node);
|
|
34803
35527
|
const body = node.body;
|
|
34804
35528
|
if (name !== void 0 && body) {
|
|
34805
35529
|
fileKinds.push((node.parameters ?? []).map((parameter) => {
|
|
34806
35530
|
try {
|
|
34807
|
-
|
|
35531
|
+
const type = checker.getTypeAtLocation(parameter);
|
|
35532
|
+
const kind = kindOf4(type) ?? platformKind(parameter.type);
|
|
35533
|
+
const reading = {};
|
|
35534
|
+
if (kind) reading.kind = kind;
|
|
35535
|
+
if (kind === "object") {
|
|
35536
|
+
const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).map((p) => p.getName());
|
|
35537
|
+
if (props.length > 0) reading.fields = props.slice(0, 200);
|
|
35538
|
+
}
|
|
35539
|
+
return reading;
|
|
34808
35540
|
} catch {
|
|
34809
|
-
return
|
|
35541
|
+
return {};
|
|
34810
35542
|
}
|
|
34811
35543
|
}));
|
|
34812
|
-
if (
|
|
35544
|
+
if (ROUTER_TEXT.test(body.getText(sf))) try {
|
|
34813
35545
|
const read2 = readRoutes(ts, node, initializerOf, literalOf);
|
|
34814
35546
|
if (read2.size > 0) {
|
|
34815
35547
|
const known = fileRoutes.get(name) ?? /* @__PURE__ */ new Map();
|
|
34816
35548
|
for (const [routeKeyText, route2] of read2) known.set(routeKeyText, route2);
|
|
34817
35549
|
fileRoutes.set(name, known);
|
|
34818
35550
|
}
|
|
35551
|
+
const prefix = strippedAnywhere(ts, body, initializerOf, literalOf);
|
|
35552
|
+
if (prefix !== void 0) filePrefixes.set(name, prefix);
|
|
35553
|
+
} catch {
|
|
35554
|
+
}
|
|
35555
|
+
}
|
|
35556
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && node.parent && ts.isVariableDeclarationList(node.parent) && node.parent.parent && ts.isVariableStatement(node.parent.parent) && node.parent.parent.parent === sf) {
|
|
35557
|
+
try {
|
|
35558
|
+
const table = tableRoutes(ts, node.initializer, initializerOf, literalOf);
|
|
35559
|
+
if (table.length > 0) fileRoutes.set(node.name.text, new Map(table.map((route2) => [routeKey2(route2), route2])));
|
|
34819
35560
|
} catch {
|
|
34820
35561
|
}
|
|
34821
35562
|
}
|
|
@@ -34824,6 +35565,7 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34824
35565
|
visit(sf);
|
|
34825
35566
|
kinds.set(key2, fileKinds);
|
|
34826
35567
|
routes.set(key2, new Map([...fileRoutes].map(([name, read2]) => [name, [...read2.values()]])));
|
|
35568
|
+
if (filePrefixes.size > 0) prefixes.set(key2, filePrefixes);
|
|
34827
35569
|
const broken = [];
|
|
34828
35570
|
for (const statement of sf.statements) {
|
|
34829
35571
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
|
|
@@ -34854,19 +35596,28 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
34854
35596
|
return false;
|
|
34855
35597
|
}
|
|
34856
35598
|
};
|
|
34857
|
-
|
|
34858
|
-
|
|
35599
|
+
const module2 = pathKey(path20.relative(home, path20.resolve(resolved)));
|
|
35600
|
+
const reached = (name, typeOnly) => {
|
|
35601
|
+
broken.push({ specifier, ...name !== void 0 ? { name } : {}, project: project3, line, typeOnly, resolved: true, module: module2 });
|
|
35602
|
+
};
|
|
35603
|
+
if (clause?.name) {
|
|
35604
|
+
if (unresolvedName(clause.name)) broken.push({ specifier, name: "default", project: project3, line, typeOnly: clause.isTypeOnly });
|
|
35605
|
+
else reached(clause.name.text, clause.isTypeOnly);
|
|
34859
35606
|
}
|
|
34860
35607
|
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
34861
35608
|
for (const element of clause.namedBindings.elements) {
|
|
34862
|
-
|
|
34863
|
-
broken.push({ specifier, name
|
|
35609
|
+
const name = (element.propertyName ?? element.name).text;
|
|
35610
|
+
if (unresolvedName(element.name)) broken.push({ specifier, name, project: project3, line, typeOnly: clause.isTypeOnly || element.isTypeOnly });
|
|
35611
|
+
else reached(name, clause.isTypeOnly || element.isTypeOnly);
|
|
34864
35612
|
}
|
|
35613
|
+
} else if (clause?.namedBindings) {
|
|
35614
|
+
reached("*", clause.isTypeOnly);
|
|
34865
35615
|
}
|
|
35616
|
+
if (!clause) reached(void 0, false);
|
|
34866
35617
|
}
|
|
34867
35618
|
if (broken.length > 0) crossProjectImports.set(key2, broken);
|
|
34868
35619
|
}
|
|
34869
|
-
return { calls: out, imports, kinds, routes, crossProjectImports };
|
|
35620
|
+
return { calls: out, imports, kinds, routes, prefixes, crossProjectImports };
|
|
34870
35621
|
function platformKind(annotation) {
|
|
34871
35622
|
if (!annotation || !ts.isTypeReferenceNode(annotation)) return void 0;
|
|
34872
35623
|
const name = ts.isIdentifier(annotation.typeName) ? annotation.typeName.text : annotation.typeName.right.text;
|
|
@@ -34915,6 +35666,22 @@ var PLATFORM_OBJECT_TYPES = /* @__PURE__ */ new Set([
|
|
|
34915
35666
|
"Socket",
|
|
34916
35667
|
"Buffer"
|
|
34917
35668
|
]);
|
|
35669
|
+
var TRANSPORT_TYPES = /* @__PURE__ */ new Set([
|
|
35670
|
+
"URL",
|
|
35671
|
+
"URLSearchParams",
|
|
35672
|
+
"Request",
|
|
35673
|
+
"Response",
|
|
35674
|
+
"Headers",
|
|
35675
|
+
"IncomingMessage",
|
|
35676
|
+
"ServerResponse",
|
|
35677
|
+
"Http2ServerRequest",
|
|
35678
|
+
"Http2ServerResponse",
|
|
35679
|
+
"Socket",
|
|
35680
|
+
"FastifyRequest",
|
|
35681
|
+
"FastifyReply",
|
|
35682
|
+
"NextRequest",
|
|
35683
|
+
"NextResponse"
|
|
35684
|
+
]);
|
|
34918
35685
|
function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], exclude = []) {
|
|
34919
35686
|
const files = [];
|
|
34920
35687
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -34930,6 +35697,8 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
34930
35697
|
for (const impl of implementations) {
|
|
34931
35698
|
declaredPaths.push(...implementationSourceFiles(impl));
|
|
34932
35699
|
if (impl.simPath) declaredPaths.push(impl.simPath);
|
|
35700
|
+
const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
|
|
35701
|
+
if (routerFile) declaredPaths.push(routerFile);
|
|
34933
35702
|
}
|
|
34934
35703
|
for (const type of types) declaredPaths.push(...typeSourceFiles(type));
|
|
34935
35704
|
declaredPaths.push(...rootFiles);
|
|
@@ -34999,6 +35768,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
34999
35768
|
typeShapes: Object.fromEntries(facts.typeShapes),
|
|
35000
35769
|
functionParams: Object.fromEntries(facts.functionParams),
|
|
35001
35770
|
functionRoutes: Object.fromEntries([...facts.functionRoutes].map(([k, v]) => [k, [...v.values()]])),
|
|
35771
|
+
...facts.routePrefixes.size > 0 ? { routePrefixes: Object.fromEntries(facts.routePrefixes) } : {},
|
|
35002
35772
|
asyncFunctions: [...facts.asyncFunctions],
|
|
35003
35773
|
enumValues: Object.fromEntries(facts.enumValues),
|
|
35004
35774
|
aliasTypes: Object.fromEntries(facts.aliasTypes),
|
|
@@ -35045,13 +35815,16 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
35045
35815
|
sites.forEach((site, index) => {
|
|
35046
35816
|
if (settled[index].length !== site.params.length) return;
|
|
35047
35817
|
site.params.forEach((param, at) => {
|
|
35048
|
-
const
|
|
35049
|
-
if (kind) param.kind = kind;
|
|
35818
|
+
const reading2 = settled[index][at];
|
|
35819
|
+
if (reading2.kind) param.kind = reading2.kind;
|
|
35820
|
+
if (reading2.fields) param.fields = reading2.fields;
|
|
35050
35821
|
});
|
|
35051
35822
|
});
|
|
35052
35823
|
}
|
|
35053
35824
|
const read2 = reading.routes.get(facts.path);
|
|
35054
35825
|
if (read2 && read2.size > 0) facts.functionRoutes = { ...facts.functionRoutes ?? {}, ...Object.fromEntries(read2) };
|
|
35826
|
+
const stripped = reading.prefixes.get(facts.path);
|
|
35827
|
+
if (stripped && stripped.size > 0) facts.routePrefixes = { ...facts.routePrefixes ?? {}, ...Object.fromEntries(stripped) };
|
|
35055
35828
|
const broken = reading.crossProjectImports.get(facts.path);
|
|
35056
35829
|
if (broken) facts.crossProjectImports = broken;
|
|
35057
35830
|
}
|
|
@@ -35983,7 +36756,12 @@ var INTRODUCED_CODES = /* @__PURE__ */ new Set([
|
|
|
35983
36756
|
"ARCHITECTURE_VIOLATION_QUERY_DEP",
|
|
35984
36757
|
"ENDPOINT_ROUTE_DUPLICATE"
|
|
35985
36758
|
]);
|
|
35986
|
-
|
|
36759
|
+
var BOUNDARY_CODES = /* @__PURE__ */ new Set([
|
|
36760
|
+
"CROSS_SUBSYSTEM_NON_ADAPTER",
|
|
36761
|
+
"CROSS_SUBSYSTEM_PRIVATE_ACCESS",
|
|
36762
|
+
"CROSS_SUBSYSTEM_TARGET_NON_PORTAL"
|
|
36763
|
+
]);
|
|
36764
|
+
function introducedFindings(kind, candidate, opts = {}, others = []) {
|
|
35987
36765
|
if (kind !== "component" && kind !== "interface") return { errors: [], warnings: [], notices: [] };
|
|
35988
36766
|
const index = scanAllSpecs({ memberDepth: 0 });
|
|
35989
36767
|
registerBuiltinRules();
|
|
@@ -36010,18 +36788,28 @@ function introducedFindings(kind, candidate, opts = {}) {
|
|
|
36010
36788
|
});
|
|
36011
36789
|
entrypointDepsRule.check(ctx);
|
|
36012
36790
|
dataBlockDepsRule.check(ctx);
|
|
36791
|
+
subsystemBoundaryDepsRule.check(ctx);
|
|
36013
36792
|
portalsRule.check(ctx);
|
|
36014
|
-
return issues.filter((i) => INTRODUCED_CODES.has(i.code));
|
|
36793
|
+
return issues.filter((i) => INTRODUCED_CODES.has(i.code) || BOUNDARY_CODES.has(i.code));
|
|
36015
36794
|
};
|
|
36795
|
+
const changed = kind === "component" ? [candidate, ...others] : others;
|
|
36796
|
+
const afterComponents = changed.reduce((list, spec) => swapped(list, spec), index.components);
|
|
36016
36797
|
const before = run3(index.components, index.interfaces);
|
|
36017
|
-
const after = kind === "component" ? run3(
|
|
36798
|
+
const after = kind === "component" ? run3(afterComponents, index.interfaces) : run3(afterComponents, swapped(index.interfaces, candidate));
|
|
36018
36799
|
const key2 = (i) => `${i.code}|${i.specId ?? ""}|${i.message}`;
|
|
36019
36800
|
const held = new Set(before.map(key2));
|
|
36020
36801
|
const added = after.filter((i) => !held.has(key2(i)));
|
|
36802
|
+
const fixedEdges = afterComponents.flatMap((comp) => (comp.dependsOn ?? []).flatMap((ref) => {
|
|
36803
|
+
const target = afterComponents.find((c) => c.id === ref);
|
|
36804
|
+
return target && target.subsystem !== comp.subsystem && target.componentType !== "Portal" ? [{ from: comp.id, to: target.id }] : [];
|
|
36805
|
+
}));
|
|
36806
|
+
const fixed = (i) => !BOUNDARY_CODES.has(i.code) || fixedEdges.some((e) => e.from === i.specId && i.message.includes(`"${e.to}"`));
|
|
36807
|
+
const curable = added.filter((i) => !fixed(i));
|
|
36808
|
+
const decided = added.filter(fixed);
|
|
36021
36809
|
return {
|
|
36022
|
-
errors:
|
|
36023
|
-
warnings:
|
|
36024
|
-
notices:
|
|
36810
|
+
errors: decided.filter((i) => i.severity === "error"),
|
|
36811
|
+
warnings: [...decided.filter((i) => i.severity === "warning"), ...curable.filter((i) => i.severity !== "notice")],
|
|
36812
|
+
notices: [...decided, ...curable].filter((i) => i.severity === "notice")
|
|
36025
36813
|
};
|
|
36026
36814
|
}
|
|
36027
36815
|
|
|
@@ -37203,8 +37991,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
|
|
|
37203
37991
|
const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
|
|
37204
37992
|
for (const step of steps) {
|
|
37205
37993
|
for (const par of parallelArms) {
|
|
37206
|
-
const
|
|
37207
|
-
if (
|
|
37994
|
+
const sep18 = par.sepByStep.get(step.stepNumber);
|
|
37995
|
+
if (sep18 !== void 0) lines.push(` and ${escapeLabel(sep18)}`);
|
|
37208
37996
|
}
|
|
37209
37997
|
switch (step.type) {
|
|
37210
37998
|
case "local":
|
|
@@ -37459,6 +38247,7 @@ function bindReferencedMembers(node, root) {
|
|
|
37459
38247
|
const out = [];
|
|
37460
38248
|
for (const member of declaredMembers(root.config ?? {})) {
|
|
37461
38249
|
if (member.problem || member.storage === "contained" || root.parts.some((p) => p.alias === member.alias)) continue;
|
|
38250
|
+
if (member.storage === "path" && !root.referenced.some((r) => r.alias === member.alias)) continue;
|
|
37462
38251
|
const base = { alias: member.alias, project: member.alias, audience: "instance", role: "member" };
|
|
37463
38252
|
if (member.storage === "hosted") {
|
|
37464
38253
|
out.push({ ...base, sourceKind: "hosted", hosted: member.source.hosted });
|
|
@@ -38552,6 +39341,9 @@ function headAcross(rootDir, dir) {
|
|
|
38552
39341
|
if (ours !== null && key2(ours) === key2(theirs)) return void 0;
|
|
38553
39342
|
return head(dir) ?? void 0;
|
|
38554
39343
|
}
|
|
39344
|
+
function followsLive(decl) {
|
|
39345
|
+
return decl.storage === "contained" || decl.storage === "path";
|
|
39346
|
+
}
|
|
38555
39347
|
var PART_FORBIDDEN_FIELDS = ["members", "externals", "rules", "extensions", "composition", "targets", "network"];
|
|
38556
39348
|
function whereToEdit(decl, dir) {
|
|
38557
39349
|
if (decl.storage === "git") {
|
|
@@ -39525,11 +40317,11 @@ function rewriteSpecRefs(raw, remap) {
|
|
|
39525
40317
|
changed = true;
|
|
39526
40318
|
}
|
|
39527
40319
|
};
|
|
39528
|
-
const rewritePair = (holder,
|
|
39529
|
-
const component = holder?.[
|
|
40320
|
+
const rewritePair = (holder, componentKey2, methodKey2) => {
|
|
40321
|
+
const component = holder?.[componentKey2];
|
|
39530
40322
|
const method2 = holder?.[methodKey2];
|
|
39531
40323
|
rewriteMethod(holder, methodKey2, component);
|
|
39532
|
-
rewrite(holder,
|
|
40324
|
+
rewrite(holder, componentKey2, "component", typeof method2 === "string" ? method2 : void 0);
|
|
39533
40325
|
};
|
|
39534
40326
|
const entries = (list) => Array.isArray(list) ? list : [];
|
|
39535
40327
|
const rewriteEach = (list, position) => {
|
|
@@ -39976,7 +40768,7 @@ function carriesEditMarker(item) {
|
|
|
39976
40768
|
const o = item;
|
|
39977
40769
|
return o.action !== void 0 || o.remove !== void 0;
|
|
39978
40770
|
}
|
|
39979
|
-
function ineffectiveDeltaPaths(delta, merged, stored) {
|
|
40771
|
+
function ineffectiveDeltaPaths(delta, merged, stored, respellings) {
|
|
39980
40772
|
const out = [];
|
|
39981
40773
|
const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
39982
40774
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -39995,8 +40787,9 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
|
|
|
39995
40787
|
if (plain.length > 0) {
|
|
39996
40788
|
const text3 = (v) => v && typeof v === "object" ? String(v.description ?? JSON.stringify(v)) : String(v);
|
|
39997
40789
|
const holds = (list, x) => Array.isArray(list) && list.some((y) => text3(y) === String(x));
|
|
40790
|
+
const order2 = (list) => Array.isArray(list) ? list.map(text3) : [];
|
|
39998
40791
|
if (plain.some((x) => !holds(m, x))) say(path52, "the level does not keep this value, so the write dropped it");
|
|
39999
|
-
else if (plain.every((x) => holds(s, x)) && d.every((x) => plain.includes(x))) say(path52, "the stored spec already held this value");
|
|
40792
|
+
else if (plain.every((x) => holds(s, x)) && d.every((x) => plain.includes(x)) && same(order2(m), order2(s))) say(path52, "the stored spec already held this value");
|
|
40000
40793
|
if (d.every((x) => plain.includes(x) || carriesEditMarker(x))) return;
|
|
40001
40794
|
}
|
|
40002
40795
|
for (const element of d) {
|
|
@@ -40026,8 +40819,9 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
|
|
|
40026
40819
|
walk2(d, m, s, path52, field);
|
|
40027
40820
|
return;
|
|
40028
40821
|
}
|
|
40029
|
-
|
|
40030
|
-
|
|
40822
|
+
const respelled2 = typeof d === "string" && typeof m === "string" && (respellings ?? []).some((r) => r.written === d && r.stored === m);
|
|
40823
|
+
if (!same(m, d) && !respelled2) say(path52, "the write did not store it \u2014 the level's schema does not keep this value");
|
|
40824
|
+
else if (same(s, d) || respelled2 && same(s, m)) say(path52, "the stored spec already held this value");
|
|
40031
40825
|
};
|
|
40032
40826
|
function walk2(d, m, s, path52, container) {
|
|
40033
40827
|
for (const [key2, value] of Object.entries(d)) {
|
|
@@ -40234,7 +41028,8 @@ var SpecWorkspace = class {
|
|
|
40234
41028
|
for (const decl of raw.members) this.readMemberKind(raw, decl);
|
|
40235
41029
|
if (depth >= maxDepth) return;
|
|
40236
41030
|
for (const decl of raw.members) {
|
|
40237
|
-
if (decl.kind === "part" || decl
|
|
41031
|
+
if (decl.kind === "part" || !followsLive(decl)) continue;
|
|
41032
|
+
if (raw.record.referenced.some((r) => r.alias === decl.alias)) continue;
|
|
40238
41033
|
const childDir = this.followableMember(raw, decl, ancestors);
|
|
40239
41034
|
if (!childDir) continue;
|
|
40240
41035
|
const before = raws.length;
|
|
@@ -40258,7 +41053,7 @@ var SpecWorkspace = class {
|
|
|
40258
41053
|
return null;
|
|
40259
41054
|
}
|
|
40260
41055
|
const childDir = path23.resolve(raw.dir, decl.path);
|
|
40261
|
-
if (projectPathEscapesRoot(raw.dir, decl.path, childDir)) {
|
|
41056
|
+
if (decl.storage !== "path" && projectPathEscapesRoot(raw.dir, decl.path, childDir)) {
|
|
40262
41057
|
this.loaderIssues.push({
|
|
40263
41058
|
severity: "error",
|
|
40264
41059
|
code: "PROJECTPATH_ESCAPE",
|
|
@@ -40349,7 +41144,7 @@ var SpecWorkspace = class {
|
|
|
40349
41144
|
this.memberProblem(raw, "kind-mismatch", decl.alias, `the member "${decl.alias}" is asserted \`as: ${asserted}\`, but its content makes it a ${kind}: ${kind === "project" ? "its tree declares an id, holds an L0 or carries a lock \u2014 drop the `as`, or remove that content with `wairon member demote`" : "its tree declares no id, holds no L0 and carries no lock \u2014 drop the `as`, or create the project content with `wairon member promote`"}`);
|
|
40350
41145
|
}
|
|
40351
41146
|
if (kind === "part") this.readPart(raw, decl, located2.dir, located2.commit);
|
|
40352
|
-
else if (decl
|
|
41147
|
+
else if (!followsLive(decl)) this.recordReferenced(raw, decl, located2.dir, located2.commit);
|
|
40353
41148
|
}
|
|
40354
41149
|
/**
|
|
40355
41150
|
* A referenced project member (stage 8): where its root is — the sibling
|
|
@@ -40854,9 +41649,9 @@ var SpecWorkspace = class {
|
|
|
40854
41649
|
return { root: root.dir, project: root.project, subsystemId };
|
|
40855
41650
|
}
|
|
40856
41651
|
/** The local id of a component's subsystem, by the component's key; null when no such component is stored. */
|
|
40857
|
-
componentSubsystem(
|
|
40858
|
-
if (!
|
|
40859
|
-
const subsystem = this.loadComponentSpec(
|
|
41652
|
+
componentSubsystem(componentKey2) {
|
|
41653
|
+
if (!componentKey2) return null;
|
|
41654
|
+
const subsystem = this.loadComponentSpec(componentKey2)?.subsystem;
|
|
40860
41655
|
return subsystem ? splitNamespace(subsystem).localId : null;
|
|
40861
41656
|
}
|
|
40862
41657
|
/**
|
|
@@ -41332,12 +42127,12 @@ var SpecWorkspace = class {
|
|
|
41332
42127
|
return this.relativizeSpec("interface", stored);
|
|
41333
42128
|
}
|
|
41334
42129
|
prepareImplementationForWrite(spec, carry = true) {
|
|
41335
|
-
const
|
|
42130
|
+
const relative27 = this.relativizeSpec("implementation", spec, carry);
|
|
41336
42131
|
const member = this.getSubprojectPrefix(spec.id);
|
|
41337
42132
|
const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
|
|
41338
|
-
if (memberRoot) return childRelativeFilePaths(
|
|
42133
|
+
if (memberRoot) return childRelativeFilePaths(relative27, this.rootDir, memberRoot);
|
|
41339
42134
|
const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
|
|
41340
|
-
return partDir ? partRelativeFilePaths(
|
|
42135
|
+
return partDir ? partRelativeFilePaths(relative27, this.rootDir, partDir) : relative27;
|
|
41341
42136
|
}
|
|
41342
42137
|
/**
|
|
41343
42138
|
* A type written in its stored form — every type position canonical (one
|
|
@@ -41347,9 +42142,9 @@ var SpecWorkspace = class {
|
|
|
41347
42142
|
prepareTypeForWrite(spec) {
|
|
41348
42143
|
const canonical3 = typeCanonicalTypes(spec).spec;
|
|
41349
42144
|
const stored = { ...canonical3, methods: (canonical3.methods ?? []).map((m) => storedTypeMethod(m)) };
|
|
41350
|
-
const
|
|
42145
|
+
const relative27 = this.relativizeSpec("type", stored);
|
|
41351
42146
|
const partDir = this.partHolding(this.getTypePath(spec.id, spec.subsystem, spec.group))?.part.directory;
|
|
41352
|
-
return partDir ? partRelativeTypePaths(
|
|
42147
|
+
return partDir ? partRelativeTypePaths(relative27, this.rootDir, partDir) : relative27;
|
|
41353
42148
|
}
|
|
41354
42149
|
prepareGroupForWrite(spec) {
|
|
41355
42150
|
return this.relativizeSpec("group", spec);
|
|
@@ -41940,7 +42735,8 @@ var SpecWorkspace = class {
|
|
|
41940
42735
|
const rawRewritten = refKind === "component" ? withUniqueTargets(rewritten) : rewritten;
|
|
41941
42736
|
this.carryDisabled = true;
|
|
41942
42737
|
try {
|
|
41943
|
-
|
|
42738
|
+
const prepared = this.prepareForKind(refKind, rawRewritten);
|
|
42739
|
+
if (JSON.stringify(prepared) === asStored && !this.storedTextsStale(refKind, id, prepared)) return false;
|
|
41944
42740
|
this.saveOfKind(refKind, rawRewritten, { preserveUpdatedAt: true });
|
|
41945
42741
|
} finally {
|
|
41946
42742
|
this.carryDisabled = false;
|
|
@@ -41985,10 +42781,25 @@ var SpecWorkspace = class {
|
|
|
41985
42781
|
);
|
|
41986
42782
|
}
|
|
41987
42783
|
if (JSON.stringify(document) === JSON.stringify(stored)) return false;
|
|
42784
|
+
if (kind === "interface" || kind === "type") deriveMergedTexts(kind, document);
|
|
41988
42785
|
this.writeHome(file, document, kind);
|
|
41989
42786
|
registryInvalidateCache();
|
|
41990
42787
|
return true;
|
|
41991
42788
|
}
|
|
42789
|
+
/**
|
|
42790
|
+
* Whether the stored file of one interface or type holds a method text its
|
|
42791
|
+
* prepared form re-derives differently: the text a raw rewrite of its params
|
|
42792
|
+
* left behind.
|
|
42793
|
+
*/
|
|
42794
|
+
storedTextsStale(kind, id, prepared) {
|
|
42795
|
+
if (kind !== "interface" && kind !== "type") return false;
|
|
42796
|
+
const file = this.storedFileOf(kind, id);
|
|
42797
|
+
if (!file || !pathExists(file)) return false;
|
|
42798
|
+
const stored = readSpecFile(file);
|
|
42799
|
+
const storedText = new Map((stored?.methods ?? []).filter((m) => m && typeof m === "object").map((m) => [m.name, m.signature]));
|
|
42800
|
+
const methods = prepared.methods ?? [];
|
|
42801
|
+
return methods.some((m) => m.signature !== void 0 && storedText.has(m.name) && storedText.get(m.name) !== m.signature);
|
|
42802
|
+
}
|
|
41992
42803
|
/** The file holding one stored spec, by its in-memory key from the bound root; a system by its project's key. */
|
|
41993
42804
|
storedFileOf(kind, id) {
|
|
41994
42805
|
const index = this.scanAll();
|
|
@@ -42339,8 +43150,8 @@ var SpecWorkspace = class {
|
|
|
42339
43150
|
const renamedBy = {
|
|
42340
43151
|
component: "rename it with sdd_rename_component, which moves its contract and implementation with it and rewrites every reference",
|
|
42341
43152
|
type: "rename it with sdd_rename_type, which rewrites every reference",
|
|
42342
|
-
interface: "
|
|
42343
|
-
implementation: "
|
|
43153
|
+
interface: "rename it with sdd_rename_spec (kind interface), which rewrites every implementation's contract and every export entry naming it",
|
|
43154
|
+
implementation: "rename it with sdd_rename_spec (kind implementation), which moves its file and keys its findings to the new id",
|
|
42344
43155
|
subsystem: "no tool renames a subsystem yet"
|
|
42345
43156
|
};
|
|
42346
43157
|
if (kind !== "system" && changes("id")) {
|
|
@@ -42351,8 +43162,8 @@ var SpecWorkspace = class {
|
|
|
42351
43162
|
}
|
|
42352
43163
|
if ((kind === "component" || kind === "type") && changes("subsystem")) {
|
|
42353
43164
|
const owner = typeof stored.subsystem === "string" ? stored.subsystem : void 0;
|
|
42354
|
-
if (owner !== void 0 && this.loadSubsystemSpec(owner) !== null) {
|
|
42355
|
-
return `"subsystem" is where the ${kind} lives, not a field a delta edits \u2014
|
|
43165
|
+
if (owner !== void 0 && this.loadSubsystemSpec(owner) !== null || kind === "type" && owner === void 0) {
|
|
43166
|
+
return `"subsystem" is where the ${kind} lives, not a field a delta edits \u2014 a delta would rewrite the field and leave the file, its references and its published entries behind; move it with sdd_move_spec (kind ${kind}), which moves ${kind === "component" ? "the component with the members it owns, its contracts and implementations, and re-homes its export entries and lifecycle entrypoints" : "the type's file, its export entries and every reference qualified by its subsystem"}.`;
|
|
42356
43167
|
}
|
|
42357
43168
|
}
|
|
42358
43169
|
if (kind === "interface" && changes("component")) {
|
|
@@ -42363,6 +43174,42 @@ var SpecWorkspace = class {
|
|
|
42363
43174
|
}
|
|
42364
43175
|
return void 0;
|
|
42365
43176
|
}
|
|
43177
|
+
/**
|
|
43178
|
+
* core_orchestrator.crossProjectWriteRefusal — the sentence refusing a write
|
|
43179
|
+
* any of whose targets lives in another project, or undefined when every one
|
|
43180
|
+
* is the bound project's own. A target lives elsewhere when its key carries a
|
|
43181
|
+
* member project's prefix (a stored spec read through this tree, or an
|
|
43182
|
+
* `alias::name` the bound alias table binds to a member), or when its first
|
|
43183
|
+
* segment is an external's alias. A legacy L1 mount keeps its old cross-tree
|
|
43184
|
+
* writes. Reads only.
|
|
43185
|
+
*/
|
|
43186
|
+
crossProjectWriteRefusal(targets, tool) {
|
|
43187
|
+
const verb = tool.replace(/^sdd_/, "").split("_")[0] || "write";
|
|
43188
|
+
for (const target of targets) {
|
|
43189
|
+
if (target.kind === "system" || typeof target.id !== "string" || !target.id.includes("::")) continue;
|
|
43190
|
+
const writable = ["subsystem", "component", "interface", "implementation", "type"].includes(target.kind);
|
|
43191
|
+
const stored = writable ? this.load(target.kind, target.id)?.id : void 0;
|
|
43192
|
+
const key2 = stored ?? this.bindReference("", target.id);
|
|
43193
|
+
const alias = target.id.slice(0, target.id.indexOf("::"));
|
|
43194
|
+
const aliased = this.cachedRawRoots[0]?.record.aliases.get(alias);
|
|
43195
|
+
const member = this.getSubprojectPrefix(key2) ?? this.getSubprojectPrefix(target.id) ?? (aliased !== void 0 && this.rootCovering(aliased) !== null ? aliased : null);
|
|
43196
|
+
if (member !== null) {
|
|
43197
|
+
const root = this.rootCovering(member);
|
|
43198
|
+
if (root?.legacyMount) continue;
|
|
43199
|
+
const qualified = stored ?? target.id;
|
|
43200
|
+
const local = qualified.includes("::") ? qualified.slice(qualified.lastIndexOf("::") + 2) : qualified;
|
|
43201
|
+
const dir = this.resolveSubprojectForNamespace(member);
|
|
43202
|
+
const folder = dir ? path23.relative(this.rootDir, dir).split(path23.sep).join("/") || "." : void 0;
|
|
43203
|
+
return `chained-spec: "${qualified}" lives in another project; ${verb} it from that project's own root. It belongs to the member project "${member}"${folder ? ` at ${folder}` : ""}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call ${tool} ${target.kind} ${local} there. Nothing was written.`;
|
|
43204
|
+
}
|
|
43205
|
+
const externals = this.cachedRawRoots[0]?.record.config?.externals ?? {};
|
|
43206
|
+
if (Object.prototype.hasOwnProperty.call(externals, alias)) {
|
|
43207
|
+
const producer = externals[alias]?.project ?? alias;
|
|
43208
|
+
return `chained-spec: "${target.id}" names a spec of "${producer}", a project this one declares as the external "${alias}": a project's specs are written from its own root, and this project only reads them through its pin. Open a session in that project's folder and call ${tool} there. Nothing was written.`;
|
|
43209
|
+
}
|
|
43210
|
+
}
|
|
43211
|
+
return void 0;
|
|
43212
|
+
}
|
|
42366
43213
|
updateSpec(kind, id, delta, hooks, dryRun = false) {
|
|
42367
43214
|
const notices = [];
|
|
42368
43215
|
const result = storedFormOf(kind, this.load(kind, id));
|
|
@@ -42370,16 +43217,8 @@ var SpecWorkspace = class {
|
|
|
42370
43217
|
throw new Error(`Spec of kind "${kind}" with ID "${id}" does not exist. Define it first.`);
|
|
42371
43218
|
}
|
|
42372
43219
|
if (kind !== "system") {
|
|
42373
|
-
const
|
|
42374
|
-
|
|
42375
|
-
if (member !== null && this.rootCovering(member)?.legacyMount === null) {
|
|
42376
|
-
const local = stored.startsWith(`${member}::`) ? stored.slice(member.length + 2) : stored;
|
|
42377
|
-
const dir = this.resolveSubprojectForNamespace(member);
|
|
42378
|
-
const folder = dir ? path23.relative(this.rootDir, dir).split(path23.sep).join("/") || "." : void 0;
|
|
42379
|
-
throw new Error(
|
|
42380
|
-
`chained-spec: "${stored}" lives in another project; update it from that project's own root. It belongs to the member project "${member}"${folder ? ` at ${folder}` : ""}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call sdd_update_spec ${kind} ${local} there. Nothing was written.`
|
|
42381
|
-
);
|
|
42382
|
-
}
|
|
43220
|
+
const refusal2 = this.crossProjectWriteRefusal([{ kind, id: String(result.id ?? id) }], "sdd_update_spec");
|
|
43221
|
+
if (refusal2 !== void 0) throw new Error(refusal2);
|
|
42383
43222
|
}
|
|
42384
43223
|
if (kind === "system" && id && id !== "system" && id !== result.name) {
|
|
42385
43224
|
throw new Error(
|
|
@@ -42984,7 +43823,8 @@ var SpecWorkspace = class {
|
|
|
42984
43823
|
const ineffective = ineffectiveDeltaPaths(
|
|
42985
43824
|
{ ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
|
|
42986
43825
|
canonicalMerged,
|
|
42987
|
-
canonicalStored
|
|
43826
|
+
canonicalStored,
|
|
43827
|
+
respellings
|
|
42988
43828
|
);
|
|
42989
43829
|
if (changes.length === 0) {
|
|
42990
43830
|
return {
|
|
@@ -43946,6 +44786,9 @@ function normalizeReferences(kind, id) {
|
|
|
43946
44786
|
function rewriteReferences(kind, id, edits) {
|
|
43947
44787
|
return current().respellReferences(kind, id, edits);
|
|
43948
44788
|
}
|
|
44789
|
+
function crossProjectWriteRefusal(targets, tool) {
|
|
44790
|
+
return current().crossProjectWriteRefusal(targets, tool);
|
|
44791
|
+
}
|
|
43949
44792
|
function assertSpecsInReach(targets, what, projectRung = false) {
|
|
43950
44793
|
if (!getWriteReach()) return;
|
|
43951
44794
|
const ws = current();
|
|
@@ -44253,8 +45096,8 @@ function approvedRevision(against) {
|
|
|
44253
45096
|
if (!commit2) {
|
|
44254
45097
|
throw new Error(against !== void 0 ? `"${against}" names no commit of the repository at ${repo}` : "no approval of this project was ever committed (.wai/lock.json is in no commit) \u2014 lock and commit first, or name a revision (`--against <ref>`)");
|
|
44255
45098
|
}
|
|
44256
|
-
const
|
|
44257
|
-
const directory = fetch(repo, commit2,
|
|
45099
|
+
const relative27 = path25.relative(repo, bound2);
|
|
45100
|
+
const directory = fetch(repo, commit2, relative27 === "" ? void 0 : relative27);
|
|
44258
45101
|
return { directory, commit: commit2, label: against !== void 0 ? `${against} (${commit2.slice(0, 12)})` : `the last approval, committed at ${commit2.slice(0, 12)}` };
|
|
44259
45102
|
}
|
|
44260
45103
|
|
|
@@ -45099,13 +45942,20 @@ function promoteMember(alias, id) {
|
|
|
45099
45942
|
declareCrossingEntries(inside, outside, parentId || parentName, result);
|
|
45100
45943
|
recordCrossingTopics(inside, outside, parentId || parentName, result);
|
|
45101
45944
|
if (crossing.out.length > 0 || qualified.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
|
|
45945
|
+
declareSiblings(family, inside, dir, alias, result);
|
|
45102
45946
|
for (const s of inside.filter((x) => x.kind === "subsystem")) restateReExports(s.id, alias);
|
|
45103
45947
|
exportCrossings([...crossing.out, ...qualified.out], dir, alias, parentId, "out", result);
|
|
45104
45948
|
exportCrossings([...crossing.in, ...qualified.in], dir, alias, parentId, "in", result);
|
|
45105
45949
|
exportReExported(inside, dir, alias, result);
|
|
45106
45950
|
invalidateSpecCache();
|
|
45107
|
-
for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
|
|
45108
45951
|
const memberKey = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias)?.namespace;
|
|
45952
|
+
rederiveSignatures(new Set(result.respelled.map((r) => r.specId)), result.signaturesRederived);
|
|
45953
|
+
runWithProjectRoot(dir, () => {
|
|
45954
|
+
invalidateSpecCache();
|
|
45955
|
+
rederiveSignatures(new Set(result.memberRespelled.map((r) => localIn(r.specId, memberKey ?? alias))), result.signaturesRederived);
|
|
45956
|
+
});
|
|
45957
|
+
invalidateSpecCache();
|
|
45958
|
+
for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
|
|
45109
45959
|
if (memberKey !== void 0) {
|
|
45110
45960
|
for (const spec of memberSide) if (spec.kind !== "system") normalizeReferences(spec.kind, `${memberKey}::${spec.id}`);
|
|
45111
45961
|
} else {
|
|
@@ -45122,14 +45972,52 @@ function promoteMember(alias, id) {
|
|
|
45122
45972
|
}
|
|
45123
45973
|
invalidateSpecCache();
|
|
45124
45974
|
rebaseEscapingPaths(dir, getProjectRoot(), result);
|
|
45125
|
-
rederiveSignatures(new Set(result.respelled.map((r) => r.specId)), result.signaturesRederived);
|
|
45126
|
-
runWithProjectRoot(dir, () => {
|
|
45127
|
-
invalidateSpecCache();
|
|
45128
|
-
rederiveSignatures(new Set(result.memberRespelled.map((r) => localIn(r.specId, memberKey ?? alias))), result.signaturesRederived);
|
|
45129
|
-
});
|
|
45130
|
-
invalidateSpecCache();
|
|
45131
45975
|
return result;
|
|
45132
45976
|
}
|
|
45977
|
+
function declareSiblings(family, inside, dir, alias, result) {
|
|
45978
|
+
const root = getProjectRoot();
|
|
45979
|
+
const parent = projectConfigRepository.load() ?? {};
|
|
45980
|
+
const ids = new Set(inside.map((s) => s.id));
|
|
45981
|
+
const named2 = /* @__PURE__ */ new Map();
|
|
45982
|
+
for (const ref of family.authoredReferences) {
|
|
45983
|
+
if (!ids.has(ref.specId) || (family.owners.get(ref.specId) ?? "") !== "") continue;
|
|
45984
|
+
const via = ref.form === "alias" ? ref.authored.split("::")[0] : ref.importedVia;
|
|
45985
|
+
if (via === void 0 || via === alias || via.includes(",")) continue;
|
|
45986
|
+
const names = named2.get(via) ?? /* @__PURE__ */ new Set();
|
|
45987
|
+
if (ref.form !== "alias") names.add(ref.authored);
|
|
45988
|
+
named2.set(via, names);
|
|
45989
|
+
}
|
|
45990
|
+
const own2 = runWithProjectRoot(dir, () => projectConfigRepository.load());
|
|
45991
|
+
for (const [sibling, imported] of named2) {
|
|
45992
|
+
if (ownGet(own2?.externals, sibling) !== void 0 || ownGet(own2?.members, sibling) !== void 0) continue;
|
|
45993
|
+
const member = declaredMembers(parent).find((m) => m.alias === sibling && !m.problem);
|
|
45994
|
+
const external = ownGet(parent.externals, sibling);
|
|
45995
|
+
let declaration2;
|
|
45996
|
+
if (member) {
|
|
45997
|
+
const at = member.source.path !== void 0 ? path27.resolve(root, member.source.path) : void 0;
|
|
45998
|
+
const id = at !== void 0 ? configAt2(at) : null;
|
|
45999
|
+
const project3 = id ? effectiveProjectId(id) ?? void 0 : void 0;
|
|
46000
|
+
declaration2 = {
|
|
46001
|
+
...project3 !== void 0 && project3 !== sibling ? { project: project3 } : {},
|
|
46002
|
+
source: at !== void 0 ? { path: toPosixPath(path27.relative(dir, at)) || "." } : member.source
|
|
46003
|
+
};
|
|
46004
|
+
} else if (external !== void 0) {
|
|
46005
|
+
const source = externalSourceOf(external);
|
|
46006
|
+
declaration2 = {
|
|
46007
|
+
...external.project !== void 0 ? { project: external.project } : {},
|
|
46008
|
+
...source?.path !== void 0 && !path27.isAbsolute(source.path) ? { source: { path: toPosixPath(path27.relative(dir, path27.resolve(root, source.path))) } } : source ? { source } : {}
|
|
46009
|
+
};
|
|
46010
|
+
} else continue;
|
|
46011
|
+
const use = [...imported].filter((n) => !n.includes("::")).sort();
|
|
46012
|
+
runWithProjectRoot(dir, () => {
|
|
46013
|
+
projectConfigRepository.declareExternal(sibling, declaration2);
|
|
46014
|
+
if (use.length > 0) projectConfigRepository.importNames(sibling, use);
|
|
46015
|
+
});
|
|
46016
|
+
const written = typeof declaration2.source === "string" ? void 0 : declaration2.source;
|
|
46017
|
+
const where = typeof declaration2.source === "string" ? declaration2.source : written?.path ?? written?.git ?? written?.hosted ?? "";
|
|
46018
|
+
(result.externalsDeclared ??= []).push(`${sibling}${declaration2.project !== void 0 ? ` (project ${declaration2.project})` : ""}${where ? ` at ${where}` : ""}${use.length > 0 ? `, use ${use.join(", ")}` : ""}`);
|
|
46019
|
+
}
|
|
46020
|
+
}
|
|
45133
46021
|
function rebaseEscapingPaths(memberDir, familyRoot, result) {
|
|
45134
46022
|
const shownMember = toPosixPath(path27.relative(familyRoot, memberDir)) || ".";
|
|
45135
46023
|
for (const file of specFilesUnder(aiPathsAt(memberDir).specsDir())) {
|
|
@@ -45598,13 +46486,63 @@ var MEMBER_WAI_GROUPS = /* @__PURE__ */ new Map([
|
|
|
45598
46486
|
["generated", "derived outputs"],
|
|
45599
46487
|
["rules", "derived outputs"],
|
|
45600
46488
|
["templates", "derived outputs"],
|
|
45601
|
-
["topology.yaml", "derived outputs"]
|
|
46489
|
+
["topology.yaml", "derived outputs"],
|
|
46490
|
+
["mcp", "derived outputs"],
|
|
46491
|
+
["phased_design.md", "design workbook"],
|
|
46492
|
+
["git.json", "host and producer bindings"],
|
|
46493
|
+
["remote.json", "host and producer bindings"],
|
|
46494
|
+
["producers.json", "host and producer bindings"],
|
|
46495
|
+
["variants", "placed in this project"],
|
|
46496
|
+
["packs", "placed in this project"]
|
|
45602
46497
|
]);
|
|
46498
|
+
var PLACED_WAI_ENTRIES = /* @__PURE__ */ new Set(["variants", "packs"]);
|
|
46499
|
+
function placedWaiFiles(memberDir, homeRoot) {
|
|
46500
|
+
const wai = path27.join(memberDir, ".wai");
|
|
46501
|
+
const home = path27.join(homeRoot, ".wai");
|
|
46502
|
+
return [...PLACED_WAI_ENTRIES].filter((entry) => fs19.existsSync(path27.join(wai, entry))).flatMap((entry) => listFilesRecursive(path27.join(wai, entry), "")).map((from) => {
|
|
46503
|
+
const rel2 = toPosixPath(path27.relative(wai, from));
|
|
46504
|
+
return { rel: rel2, from, to: path27.join(home, rel2) };
|
|
46505
|
+
});
|
|
46506
|
+
}
|
|
46507
|
+
function waiRefusals(memberDir, homeRoot) {
|
|
46508
|
+
const out = [];
|
|
46509
|
+
const wai = path27.join(memberDir, ".wai");
|
|
46510
|
+
for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
|
|
46511
|
+
if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
|
|
46512
|
+
out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
|
|
46513
|
+
}
|
|
46514
|
+
for (const f of placedWaiFiles(memberDir, homeRoot)) {
|
|
46515
|
+
if (fs19.existsSync(f.to) && !fs19.readFileSync(f.to).equals(fs19.readFileSync(f.from))) {
|
|
46516
|
+
out.push(`its .wai/${f.rel} differs from this project's file of that name \u2014 reconcile the two by hand first`);
|
|
46517
|
+
}
|
|
46518
|
+
}
|
|
46519
|
+
return out;
|
|
46520
|
+
}
|
|
46521
|
+
function placeWaiEntries(memberDir, homeRoot, result) {
|
|
46522
|
+
for (const f of placedWaiFiles(memberDir, homeRoot)) {
|
|
46523
|
+
if (fs19.existsSync(f.to)) continue;
|
|
46524
|
+
fs19.mkdirSync(path27.dirname(f.to), { recursive: true });
|
|
46525
|
+
fs19.copyFileSync(f.from, f.to);
|
|
46526
|
+
result.placed.push(`.wai/${f.rel} \u2192 this project's .wai/${f.rel}`);
|
|
46527
|
+
}
|
|
46528
|
+
const workbook = path27.join(memberDir, ".wai", "phased_design.md");
|
|
46529
|
+
if (!fs19.existsSync(workbook)) return;
|
|
46530
|
+
const named2 = (dir, text3) => {
|
|
46531
|
+
const name = configAt2(dir)?.name;
|
|
46532
|
+
return (name ? text3.split(name).join("<project>") : text3).replace(/\r\n/g, "\n");
|
|
46533
|
+
};
|
|
46534
|
+
const homeWorkbook = path27.join(homeRoot, ".wai", "phased_design.md");
|
|
46535
|
+
const theirs = named2(memberDir, fs19.readFileSync(workbook, "utf8"));
|
|
46536
|
+
const ours = fs19.existsSync(homeWorkbook) ? named2(homeRoot, fs19.readFileSync(homeWorkbook, "utf8")) : null;
|
|
46537
|
+
if (ours !== theirs) {
|
|
46538
|
+
result.notCarried.push(".wai/phased_design.md: the member's design workbook differs from this project's \u2014 it is deleted with the boundary (the design itself is its specs, which move); copy what you want to keep from it first");
|
|
46539
|
+
}
|
|
46540
|
+
}
|
|
45603
46541
|
function internalizeMember(alias, destination) {
|
|
45604
46542
|
const family = graph();
|
|
45605
46543
|
const part = family.nodes.find((n) => n.namespace === "")?.parts.find((p) => p.alias === alias);
|
|
45606
46544
|
if (part) return internalizePart(alias, part);
|
|
45607
|
-
if (
|
|
46545
|
+
if (storedAsSibling(alias)) {
|
|
45608
46546
|
const demoted = demoteMember(alias, destination);
|
|
45609
46547
|
const nowPart = graph().nodes.find((n) => n.namespace === "")?.parts.find((p) => p.alias === alias);
|
|
45610
46548
|
if (!nowPart) throw new WaironError(`cannot internalize "${alias}": demoted, it does not read as a part of this project.`);
|
|
@@ -45630,8 +46568,10 @@ function internalizeMember(alias, destination) {
|
|
|
45630
46568
|
carryDeclarations(scan2, result);
|
|
45631
46569
|
carryPacks(scan2, destination, result);
|
|
45632
46570
|
projectConfigRepository.removeMember(alias);
|
|
46571
|
+
placeWaiEntries(scan2.memberDir, getProjectRoot(), result);
|
|
45633
46572
|
result.deleted = deleteMemberWai(scan2.memberDir);
|
|
45634
46573
|
invalidateSpecCache();
|
|
46574
|
+
rederiveSignatures(new Set(rewritten.map((s) => s.id)), result.signaturesRederived ??= []);
|
|
45635
46575
|
for (const spec of rewritten) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
|
|
45636
46576
|
invalidateSpecCache();
|
|
45637
46577
|
return result;
|
|
@@ -45706,11 +46646,7 @@ function internalizeRefusals(scan2, destination) {
|
|
|
45706
46646
|
const held = exported.get(name);
|
|
45707
46647
|
if (held !== void 0 && held !== scan2.alias) out.push(`the export "${name}" is a public name this project already exports for ${held}`);
|
|
45708
46648
|
}
|
|
45709
|
-
|
|
45710
|
-
for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
|
|
45711
|
-
if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
|
|
45712
|
-
out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
|
|
45713
|
-
}
|
|
46649
|
+
out.push(...waiRefusals(scan2.memberDir, getProjectRoot()));
|
|
45714
46650
|
return out;
|
|
45715
46651
|
}
|
|
45716
46652
|
function resolveSubsystemExportsOfProject() {
|
|
@@ -45929,11 +46865,14 @@ function deleteMemberWai(memberDir) {
|
|
|
45929
46865
|
if (fs19.readdirSync(wai).length === 0) fs19.rmdirSync(wai);
|
|
45930
46866
|
return deleted.sort();
|
|
45931
46867
|
}
|
|
46868
|
+
function storedAsSibling(alias) {
|
|
46869
|
+
return declaredMembers(projectConfigRepository.load() ?? {}).find((m) => m.alias === alias)?.storage === "path";
|
|
46870
|
+
}
|
|
45932
46871
|
function memberToDemote(family, alias) {
|
|
45933
46872
|
const bound2 = family.nodes.find((n) => n.namespace === "");
|
|
45934
46873
|
if (bound2?.parts.some((p) => p.alias === alias)) throw new WaironError(`demote refused: "${alias}" is already a part of this project.`);
|
|
45935
46874
|
const node = family.nodes.find((n) => n.parent === "" && n.mountAlias === alias);
|
|
45936
|
-
if (node) return { dir: node.directory, contained:
|
|
46875
|
+
if (node) return { dir: node.directory, contained: !storedAsSibling(alias), key: node.namespace };
|
|
45937
46876
|
const referenced = bound2?.externals.find((e) => e.role === "member" && e.alias === alias);
|
|
45938
46877
|
if (referenced?.sourceKind === "git") {
|
|
45939
46878
|
throw new WaironError(`demote refused: the member "${alias}" is fetched from git \u2014 its files are the fetch cache's. Demote it from a checkout of its repository.`);
|
|
@@ -45970,11 +46909,7 @@ function demoteRefusals(scan2, destination, memberIds, family) {
|
|
|
45970
46909
|
const held = exported.get(name);
|
|
45971
46910
|
if (held !== void 0 && held !== scan2.alias) out.push(`the export "${name}" is a public name this project already exports for ${held}`);
|
|
45972
46911
|
}
|
|
45973
|
-
|
|
45974
|
-
for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
|
|
45975
|
-
if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
|
|
45976
|
-
out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
|
|
45977
|
-
}
|
|
46912
|
+
out.push(...waiRefusals(scan2.memberDir, getProjectRoot()));
|
|
45978
46913
|
return out;
|
|
45979
46914
|
}
|
|
45980
46915
|
function demoteMember(alias, destination) {
|
|
@@ -46026,6 +46961,7 @@ function demoteMember(alias, destination) {
|
|
|
46026
46961
|
patchKeepingStamp(s.file, (raw) => raw.parentSystem === parentName ? false : (raw.parentSystem = parentName, true));
|
|
46027
46962
|
}
|
|
46028
46963
|
}
|
|
46964
|
+
placeWaiEntries(dir, root, result);
|
|
46029
46965
|
result.deleted = deleteProjectContent(dir);
|
|
46030
46966
|
if (!contained && parentId !== void 0) {
|
|
46031
46967
|
runWithProjectRoot(dir, () => projectConfigRepository.setPartOf({ project: parentId, path: toPosixPath(path27.relative(dir, root)) || "." }));
|
|
@@ -46040,9 +46976,9 @@ function demoteMember(alias, destination) {
|
|
|
46040
46976
|
carryPacks(scan2, destination, result);
|
|
46041
46977
|
retireBoundaryExports(family, crossed, key2);
|
|
46042
46978
|
invalidateSpecCache();
|
|
46979
|
+
rederiveSignatures(new Set([...parentSide, ...memberSide].map((s) => s.id)), result.signaturesRederived ??= []);
|
|
46043
46980
|
for (const spec of [...parentSide, ...memberSide]) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
|
|
46044
46981
|
invalidateSpecCache();
|
|
46045
|
-
rederiveSignatures(new Set([...parentSide, ...memberSide].map((s) => s.id)), result.signaturesRederived ??= []);
|
|
46046
46982
|
return result;
|
|
46047
46983
|
}
|
|
46048
46984
|
function respellQualifiedBack(parentSide, memberSide, aliases, tables, crossed) {
|
|
@@ -46400,17 +47336,43 @@ function moveRenamedSpecs(componentId, newId, component, movingInterface, moving
|
|
|
46400
47336
|
saveInterfaceSpec({ ...movedInterface, id: interfaceId, component: newId, previousIds: traced(movedInterface.previousIds, movedInterface.id) });
|
|
46401
47337
|
}
|
|
46402
47338
|
if (movedImplementation) {
|
|
46403
|
-
const
|
|
46404
|
-
if (path27.resolve(getImplementationPath(implementationId, movedImplementation.contract)) === path27.resolve(
|
|
46405
|
-
fs19.unlinkSync(
|
|
47339
|
+
const oldFile2 = getImplementationPath(movedImplementation.id);
|
|
47340
|
+
if (path27.resolve(getImplementationPath(implementationId, movedImplementation.contract)) === path27.resolve(oldFile2)) {
|
|
47341
|
+
fs19.unlinkSync(oldFile2);
|
|
46406
47342
|
}
|
|
46407
47343
|
saveImplementationSpec({ ...movedImplementation, id: implementationId, previousIds: traced(movedImplementation.previousIds, movedImplementation.id) });
|
|
46408
47344
|
}
|
|
47345
|
+
invalidateSpecCache();
|
|
47346
|
+
const oldFile = path27.resolve(getComponentPath(componentId));
|
|
47347
|
+
const oldDir = path27.basename(oldFile) === ".index.yaml" ? path27.dirname(oldFile) : null;
|
|
47348
|
+
const moved = /* @__PURE__ */ new Set([interfaceId, implementationId, ...movingInterface ? [movingInterface.id] : [], ...movingImplementation ? [movingImplementation.id] : []]);
|
|
47349
|
+
const staying = loadInterfaceSpecs().filter((i) => i.component === newId && !moved.has(i.id));
|
|
47350
|
+
const contracts = /* @__PURE__ */ new Set([...staying.map((i) => i.id), interfaceId]);
|
|
47351
|
+
const stayingImplementations = loadImplementationSpecs().filter((impl) => contracts.has(impl.contract) && !moved.has(impl.id));
|
|
47352
|
+
if (oldDir !== null) {
|
|
47353
|
+
for (const contract of staying) relocateSpec("interface", contract, getInterfacePath(contract.id), oldDir);
|
|
47354
|
+
for (const impl of stayingImplementations) relocateSpec("implementation", impl, getImplementationPath(impl.id), oldDir);
|
|
47355
|
+
}
|
|
46409
47356
|
removeSpecFile2(getComponentPath(componentId), componentId);
|
|
46410
47357
|
if (movingInterface) removeSpecFile2(getInterfacePath(movingInterface.id), movingInterface.id);
|
|
46411
47358
|
if (movingImplementation) removeSpecFile2(getImplementationPath(movingImplementation.id), movingImplementation.id);
|
|
46412
47359
|
invalidateSpecCache();
|
|
46413
47360
|
}
|
|
47361
|
+
function relocateSpec(kind, spec, file, fromDir) {
|
|
47362
|
+
if (!fs19.existsSync(file) || path27.dirname(path27.resolve(file)) !== fromDir) return;
|
|
47363
|
+
const backup = fs19.readFileSync(file);
|
|
47364
|
+
fs19.unlinkSync(file);
|
|
47365
|
+
invalidateSpecCache();
|
|
47366
|
+
try {
|
|
47367
|
+
if (kind === "interface") saveInterfaceSpec(spec);
|
|
47368
|
+
else saveImplementationSpec(spec);
|
|
47369
|
+
} catch (e) {
|
|
47370
|
+
fs19.writeFileSync(file, backup);
|
|
47371
|
+
invalidateSpecCache();
|
|
47372
|
+
throw e;
|
|
47373
|
+
}
|
|
47374
|
+
invalidateSpecCache();
|
|
47375
|
+
}
|
|
46414
47376
|
function traced(trace, former) {
|
|
46415
47377
|
const out = [...trace ?? []];
|
|
46416
47378
|
if (!out.includes(former)) out.push(former);
|
|
@@ -46472,6 +47434,281 @@ function removeSpecFile2(file, id) {
|
|
|
46472
47434
|
fs19.rmdirSync(dir);
|
|
46473
47435
|
}
|
|
46474
47436
|
}
|
|
47437
|
+
function renameSpecId(kind, id, newId, dryRun) {
|
|
47438
|
+
if (kind !== "interface" && kind !== "implementation") {
|
|
47439
|
+
const owner = kind === "component" ? "sdd_rename_component" : kind === "type" ? "sdd_rename_type" : "no tool yet";
|
|
47440
|
+
throw new WaironError(`invalid-kind: sdd_rename_spec renames a contract (kind interface) or an implementation on its own; a ${kind} is renamed with ${owner}.`);
|
|
47441
|
+
}
|
|
47442
|
+
const spec = kind === "interface" ? loadInterfaceSpec(id) : loadImplementationSpec(id);
|
|
47443
|
+
if (!spec) throw new WaironError(`spec-missing: no ${kind} has the id "${id}".`);
|
|
47444
|
+
if (id.includes("::")) {
|
|
47445
|
+
throw new WaironError(`chained-spec: "${id}" lives in another project; rename it from that project's own root. ${memberRootWay(id, `sdd_rename_spec ${kind} ${id.slice(id.lastIndexOf("::") + 2)} <new id>`)}`);
|
|
47446
|
+
}
|
|
47447
|
+
if (!SpecIdSchema.safeParse(newId).success || kind === "interface" && !/^i[a-z0-9-_]+$/.test(newId)) {
|
|
47448
|
+
throw new WaironError(`invalid-id: "${newId}" is not a lowercase identifier with no namespace separator${kind === "interface" ? ' beginning with "i", as a contract id does' : ""}.`);
|
|
47449
|
+
}
|
|
47450
|
+
if (newId === id) throw new WaironError(`name-unchanged: ${kind} "${id}" already has that id \u2014 nothing to do.`);
|
|
47451
|
+
const taken = kind === "interface" ? loadInterfaceSpecs().some((i) => i.id === newId) : loadImplementationSpecs().some((i) => i.id === newId);
|
|
47452
|
+
if (taken) throw new WaironError(`id-taken: a ${kind} already has the id "${newId}".`);
|
|
47453
|
+
refuseRetiredIds([{ kind, id: newId }]);
|
|
47454
|
+
const rename = { specs: [{ from: id, to: newId }] };
|
|
47455
|
+
const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
|
|
47456
|
+
const specsDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
47457
|
+
const remap = (ref, position) => kind === "interface" && position === "interface" && ref === id ? newId : ref;
|
|
47458
|
+
const derivesName = (entry) => kind === "interface" && entry.interface === id;
|
|
47459
|
+
const others = (specs) => [...new Set(specs.filter((s) => !(s.kind === kind && s.id === id)).map((s) => s.id))];
|
|
47460
|
+
const keptDry = keepPublicNames(specsDir, derivesName, true);
|
|
47461
|
+
const wouldRewrite = [...keptDry.specs, ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)];
|
|
47462
|
+
assertSpecsInReach([{ kind, id }, ...wouldRewrite], `renaming ${kind} "${id}"`, registerEdits.length > 0);
|
|
47463
|
+
const renamed2 = [{ kind, from: id, to: newId }];
|
|
47464
|
+
if (dryRun) return { kind, renamed: renamed2, rewritten: others(wouldRewrite), carried: registerEdits, keptPublicNames: keptDry.names, dryRun: true };
|
|
47465
|
+
const kept = keepPublicNames(specsDir, derivesName);
|
|
47466
|
+
const rewritten = others([...kept.specs, ...rewriteRefFields(specsDir, remap), ...rekeyLintAllows(specsDir, rename)]);
|
|
47467
|
+
invalidateSpecCache();
|
|
47468
|
+
const oldFile = kind === "interface" ? getInterfacePath(id) : getImplementationPath(id);
|
|
47469
|
+
if (kind === "interface") {
|
|
47470
|
+
const current2 = loadInterfaceSpec(id) ?? spec;
|
|
47471
|
+
const next = { ...current2, id: newId, previousIds: traced(current2.previousIds, id) };
|
|
47472
|
+
if (path27.resolve(getInterfacePath(newId, current2.component)) === path27.resolve(oldFile)) fs19.unlinkSync(oldFile);
|
|
47473
|
+
saveInterfaceSpec(next);
|
|
47474
|
+
} else {
|
|
47475
|
+
const current2 = loadImplementationSpec(id) ?? spec;
|
|
47476
|
+
const next = { ...current2, id: newId, previousIds: traced(current2.previousIds, id) };
|
|
47477
|
+
if (path27.resolve(getImplementationPath(newId, current2.contract)) === path27.resolve(oldFile)) fs19.unlinkSync(oldFile);
|
|
47478
|
+
saveImplementationSpec(next);
|
|
47479
|
+
}
|
|
47480
|
+
invalidateSpecCache();
|
|
47481
|
+
removeSpecFile2(oldFile, id);
|
|
47482
|
+
const carried = projectConfigRepository.rekeyCarried(rename);
|
|
47483
|
+
invalidateSpecCache();
|
|
47484
|
+
return { kind, renamed: renamed2, rewritten, carried, keptPublicNames: kept.names };
|
|
47485
|
+
}
|
|
47486
|
+
function moveSpec(kind, id, subsystem, dryRun) {
|
|
47487
|
+
if (kind !== "component" && kind !== "type") {
|
|
47488
|
+
throw new WaironError(`invalid-kind: sdd_move_spec moves a component or a type to another subsystem; a ${kind} ${kind === "interface" || kind === "implementation" ? "follows its component (sdd_move_methods moves behaviour between components)" : "does not move"}.`);
|
|
47489
|
+
}
|
|
47490
|
+
const subsystems = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
|
|
47491
|
+
const target = subsystem === void 0 || subsystem === "" ? void 0 : subsystem;
|
|
47492
|
+
if (target !== void 0 && !subsystems.some((s) => s.id === target)) {
|
|
47493
|
+
throw new WaironError(`subsystem-missing: this project has no subsystem "${target}"${target.includes("::") ? " \u2014 a spec never moves into another project" : ""}. Known subsystems: ${subsystems.map((s) => s.id).join(", ") || "none"}.`);
|
|
47494
|
+
}
|
|
47495
|
+
return kind === "component" ? moveComponentSpec(id, target, subsystems, dryRun === true) : moveTypeSpec(id, target, subsystems, dryRun === true);
|
|
47496
|
+
}
|
|
47497
|
+
function moveComponentSpec(id, target, subsystems, dryRun) {
|
|
47498
|
+
const components = loadComponentSpecs();
|
|
47499
|
+
const component = components.find((c) => c.id === id);
|
|
47500
|
+
if (!component) throw new WaironError(`spec-missing: no component has the id "${id}".`);
|
|
47501
|
+
if (id.includes("::")) {
|
|
47502
|
+
throw new WaironError(`chained-spec: "${id}" lives in another project; move it from that project's own root. ${memberRootWay(id, `sdd_move_spec component ${id.slice(id.lastIndexOf("::") + 2)} <subsystem>`)}`);
|
|
47503
|
+
}
|
|
47504
|
+
if (target === void 0) throw new WaironError(`subsystem-missing: name the subsystem component "${id}" moves to \u2014 a component always lives in a subsystem.`);
|
|
47505
|
+
if (component.subsystem === target) throw new WaironError(`same-subsystem: component "${id}" already lives in "${target}" \u2014 nothing to move.`);
|
|
47506
|
+
const owner = components.find((c) => (c.owns ?? []).includes(id));
|
|
47507
|
+
if (owner) {
|
|
47508
|
+
throw new WaironError(`owned: "${id}" is a member owned by "${owner.id}", and a pattern's members live with it \u2014 move "${owner.id}", which takes "${id}" along.`);
|
|
47509
|
+
}
|
|
47510
|
+
const movingIds = [];
|
|
47511
|
+
const visit = (cid) => {
|
|
47512
|
+
if (movingIds.includes(cid)) return;
|
|
47513
|
+
movingIds.push(cid);
|
|
47514
|
+
for (const member of components.find((c) => c.id === cid)?.owns ?? []) visit(member);
|
|
47515
|
+
};
|
|
47516
|
+
visit(id);
|
|
47517
|
+
const contracts = loadInterfaceSpecs().filter((i) => movingIds.includes(i.component));
|
|
47518
|
+
const implementations = loadImplementationSpecs().filter((impl) => contracts.some((i) => i.id === impl.contract));
|
|
47519
|
+
const moved = [
|
|
47520
|
+
...movingIds.map((cid) => ({ kind: "component", id: cid })),
|
|
47521
|
+
...contracts.map((i) => ({ kind: "interface", id: i.id })),
|
|
47522
|
+
...implementations.map((impl) => ({ kind: "implementation", id: impl.id }))
|
|
47523
|
+
];
|
|
47524
|
+
const from = component.subsystem;
|
|
47525
|
+
const fromSpec = subsystems.find((s) => s.id === from);
|
|
47526
|
+
const toSpec = subsystems.find((s) => s.id === target);
|
|
47527
|
+
const names = (ref) => ref !== void 0 && movingIds.includes(ref);
|
|
47528
|
+
const exportsMoving = (fromSpec?.publicInterfaces ?? []).filter((e) => names(e.component));
|
|
47529
|
+
const lifecycleMoving = (fromSpec?.lifecycle ?? []).filter((e) => names(e.component));
|
|
47530
|
+
const system = loadSystemSpec();
|
|
47531
|
+
const l0 = system?.publicInterfaces ?? [];
|
|
47532
|
+
const repointed = l0.filter((e) => e.from === from && names(e.component));
|
|
47533
|
+
const whole = l0.filter((e) => e.from === from && e.component === void 0 && e.typeDef === void 0 && e.interface === void 0);
|
|
47534
|
+
const added = whole.length > 0 ? exportsMoving.map((e) => ({
|
|
47535
|
+
from: target,
|
|
47536
|
+
component: e.component,
|
|
47537
|
+
...e.interface ? { interface: e.interface } : {},
|
|
47538
|
+
...e.as ? { as: e.as } : {},
|
|
47539
|
+
...whole[0].audience ? { audience: whole[0].audience } : {}
|
|
47540
|
+
})) : [];
|
|
47541
|
+
const rewritten = [
|
|
47542
|
+
...exportsMoving.length + lifecycleMoving.length > 0 ? [from, target] : [],
|
|
47543
|
+
...repointed.length + added.length > 0 ? ["system"] : []
|
|
47544
|
+
];
|
|
47545
|
+
assertSpecsInReach([...moved, ...rewritten.map((s) => s === "system" ? { kind: "system", id: "system" } : { kind: "subsystem", id: s })], `moving component "${id}"`);
|
|
47546
|
+
const report3 = { kind: "component", id, from, to: target, moved, rewritten: [...new Set(rewritten)], notices: [] };
|
|
47547
|
+
if (dryRun) return { ...report3, dryRun: true };
|
|
47548
|
+
if (exportsMoving.length + lifecycleMoving.length > 0 && fromSpec) {
|
|
47549
|
+
saveSpec("subsystem", {
|
|
47550
|
+
...fromSpec,
|
|
47551
|
+
publicInterfaces: (fromSpec.publicInterfaces ?? []).filter((e) => !exportsMoving.includes(e)),
|
|
47552
|
+
...fromSpec.lifecycle ? { lifecycle: fromSpec.lifecycle.filter((e) => !lifecycleMoving.includes(e)) } : {}
|
|
47553
|
+
});
|
|
47554
|
+
saveSpec("subsystem", {
|
|
47555
|
+
...toSpec,
|
|
47556
|
+
publicInterfaces: [...toSpec.publicInterfaces ?? [], ...exportsMoving],
|
|
47557
|
+
...lifecycleMoving.length > 0 || toSpec.lifecycle ? { lifecycle: [...toSpec.lifecycle ?? [], ...lifecycleMoving] } : {}
|
|
47558
|
+
});
|
|
47559
|
+
}
|
|
47560
|
+
if (system && repointed.length + added.length > 0) {
|
|
47561
|
+
saveSystemSpec({ ...system, publicInterfaces: [...l0.map((e) => repointed.includes(e) ? { ...e, from: target } : e), ...added] });
|
|
47562
|
+
}
|
|
47563
|
+
invalidateSpecCache();
|
|
47564
|
+
for (const cid of movingIds) {
|
|
47565
|
+
const current2 = loadComponentSpec(cid);
|
|
47566
|
+
if (current2) saveComponentSpec({ ...current2, subsystem: target });
|
|
47567
|
+
invalidateSpecCache();
|
|
47568
|
+
}
|
|
47569
|
+
invalidateSpecCache();
|
|
47570
|
+
return report3;
|
|
47571
|
+
}
|
|
47572
|
+
function moveTypeSpec(id, target, subsystems, dryRun) {
|
|
47573
|
+
const types = loadTypeSpecs();
|
|
47574
|
+
const cut = id.lastIndexOf("::");
|
|
47575
|
+
const qualifier = cut >= 0 ? id.slice(0, cut) : void 0;
|
|
47576
|
+
if (qualifier !== void 0 && !subsystems.some((s) => s.id === qualifier)) {
|
|
47577
|
+
throw new WaironError(`chained-spec: "${id}" lives in another project; move it from that project's own root. ${memberRootWay(id, `sdd_move_spec type ${id.slice(cut + 2)} <subsystem>`)}`);
|
|
47578
|
+
}
|
|
47579
|
+
const bare2 = cut >= 0 ? id.slice(cut + 2) : id;
|
|
47580
|
+
const matching = types.filter((t) => t.id === bare2 && (qualifier === void 0 || t.subsystem === qualifier));
|
|
47581
|
+
if (matching.length === 0) throw new WaironError(`spec-missing: no type has the id "${id}" (a type is addressed by its bare id, or as <subsystem>::<id>).`);
|
|
47582
|
+
if (matching.length > 1) throw new WaironError(`ambiguous: ${matching.length} types have the id "${bare2}" \u2014 address it as <subsystem>::${bare2} (${matching.map((t) => qualifiedTypeId(t)).join(", ")}).`);
|
|
47583
|
+
const type = matching[0];
|
|
47584
|
+
const from = type.subsystem ?? "";
|
|
47585
|
+
if ((target ?? "") === from) throw new WaironError(`same-subsystem: type "${id}" already lives ${from ? `in "${from}"` : "at system level"} \u2014 nothing to move.`);
|
|
47586
|
+
if (types.some((t) => t !== type && t.id === bare2 && (t.subsystem ?? "") === (target ?? ""))) {
|
|
47587
|
+
throw new WaironError(`id-taken: ${target ? `subsystem "${target}"` : "the system level"} already holds a type with the id "${bare2}".`);
|
|
47588
|
+
}
|
|
47589
|
+
const fromSpec = subsystems.find((s) => s.id === from);
|
|
47590
|
+
const names = (ref) => ref !== void 0 && (ref === bare2 || ref === `${from}::${bare2}`);
|
|
47591
|
+
const exportsMoving = (fromSpec?.publicInterfaces ?? []).filter((e) => e.component === void 0 && names(e.typeDef));
|
|
47592
|
+
const system = loadSystemSpec();
|
|
47593
|
+
const l0 = system?.publicInterfaces ?? [];
|
|
47594
|
+
const repointed = l0.filter((e) => from !== "" && e.from === from && names(e.typeDef));
|
|
47595
|
+
if (target === void 0 && exportsMoving.length + repointed.length > 0) {
|
|
47596
|
+
throw new WaironError(`exported: subsystem "${from}" exports type "${bare2}" (${exportsMoving.length + repointed.length} entr${exportsMoving.length + repointed.length === 1 ? "y" : "ies"}), and a system-level type is published by the L0 alone \u2014 move its export entries to the L0 first, or move the type to another subsystem.`);
|
|
47597
|
+
}
|
|
47598
|
+
const oldToken = from ? `${from}::${bare2}` : void 0;
|
|
47599
|
+
const newToken = target ? `${target}::${bare2}` : bare2;
|
|
47600
|
+
const specsDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
47601
|
+
const rewriteAll2 = (write6) => {
|
|
47602
|
+
if (oldToken === void 0) return [];
|
|
47603
|
+
const out = [];
|
|
47604
|
+
for (const file of listFilesRecursive(specsDir, ".yaml")) {
|
|
47605
|
+
let raw;
|
|
47606
|
+
try {
|
|
47607
|
+
raw = readYamlFile(file);
|
|
47608
|
+
} catch {
|
|
47609
|
+
continue;
|
|
47610
|
+
}
|
|
47611
|
+
const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
|
|
47612
|
+
if (!kind || !respellQualifiedType(raw, oldToken, newToken)) continue;
|
|
47613
|
+
if (write6) writeYamlFile(file, raw);
|
|
47614
|
+
out.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
|
|
47615
|
+
}
|
|
47616
|
+
return out;
|
|
47617
|
+
};
|
|
47618
|
+
const would = rewriteAll2(false);
|
|
47619
|
+
const rewritten = [.../* @__PURE__ */ new Set([
|
|
47620
|
+
...would.filter((s) => !(s.kind === "type" && s.id === bare2)).map((s) => s.id),
|
|
47621
|
+
...exportsMoving.length > 0 && target !== void 0 ? [from, target] : [],
|
|
47622
|
+
...repointed.length > 0 ? ["system"] : []
|
|
47623
|
+
])];
|
|
47624
|
+
assertSpecsInReach([{ kind: "type", id: qualifiedTypeId(type) }, ...would], `moving type "${id}"`);
|
|
47625
|
+
const report3 = { kind: "type", id: bare2, from, to: target ?? "", moved: [{ kind: "type", id: qualifiedTypeId(type) }], rewritten, notices: [] };
|
|
47626
|
+
if (dryRun) return { ...report3, dryRun: true };
|
|
47627
|
+
rewriteAll2(true);
|
|
47628
|
+
invalidateSpecCache();
|
|
47629
|
+
if (exportsMoving.length > 0 && fromSpec && target !== void 0) {
|
|
47630
|
+
const stated = loadSubsystemSpecs().find((s) => s.id === from) ?? fromSpec;
|
|
47631
|
+
saveSpec("subsystem", { ...stated, publicInterfaces: (stated.publicInterfaces ?? []).filter((e) => !(e.component === void 0 && names(e.typeDef))) });
|
|
47632
|
+
invalidateSpecCache();
|
|
47633
|
+
const toSpec = loadSubsystemSpecs().find((s) => s.id === target);
|
|
47634
|
+
saveSpec("subsystem", { ...toSpec, publicInterfaces: [...toSpec.publicInterfaces ?? [], ...exportsMoving.map((e) => ({ ...e, typeDef: bare2 }))] });
|
|
47635
|
+
}
|
|
47636
|
+
if (system && repointed.length > 0) {
|
|
47637
|
+
const stated = loadSystemSpec() ?? system;
|
|
47638
|
+
const current3 = stated.publicInterfaces ?? [];
|
|
47639
|
+
saveSystemSpec({ ...stated, publicInterfaces: current3.map((e) => e.from === from && names(e.typeDef) ? { ...e, from: target } : e) });
|
|
47640
|
+
}
|
|
47641
|
+
invalidateSpecCache();
|
|
47642
|
+
const current2 = loadTypeSpecs().find((t) => t.id === bare2 && (t.subsystem ?? "") === from) ?? type;
|
|
47643
|
+
const oldFile = typeFileOf(bare2, current2);
|
|
47644
|
+
const backup = fs19.existsSync(oldFile) ? fs19.readFileSync(oldFile) : null;
|
|
47645
|
+
if (backup) fs19.unlinkSync(oldFile);
|
|
47646
|
+
invalidateSpecCache();
|
|
47647
|
+
try {
|
|
47648
|
+
const { subsystem: _old, ...rest } = current2;
|
|
47649
|
+
saveSpec("type", target !== void 0 ? { ...rest, subsystem: target } : rest);
|
|
47650
|
+
} catch (e) {
|
|
47651
|
+
if (backup) fs19.writeFileSync(oldFile, backup);
|
|
47652
|
+
invalidateSpecCache();
|
|
47653
|
+
throw e;
|
|
47654
|
+
}
|
|
47655
|
+
invalidateSpecCache();
|
|
47656
|
+
return report3;
|
|
47657
|
+
}
|
|
47658
|
+
var TYPE_POSITION_KEYS = /* @__PURE__ */ new Set(["type", "returns", "signature", "signatureFrom", "typeDef", "references", "linkedEntity", "holds"]);
|
|
47659
|
+
function respellQualifiedType(raw, oldToken, newToken) {
|
|
47660
|
+
let changed = false;
|
|
47661
|
+
const escaped = oldToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47662
|
+
const walk2 = (node) => {
|
|
47663
|
+
if (Array.isArray(node)) {
|
|
47664
|
+
node.forEach(walk2);
|
|
47665
|
+
return;
|
|
47666
|
+
}
|
|
47667
|
+
if (!node || typeof node !== "object") return;
|
|
47668
|
+
for (const [key2, value] of Object.entries(node)) {
|
|
47669
|
+
if (typeof value === "string" && TYPE_POSITION_KEYS.has(key2)) {
|
|
47670
|
+
const next = value.replace(new RegExp(`(?<![A-Za-z0-9_:-])${escaped}(?![A-Za-z0-9_-])`, "g"), newToken);
|
|
47671
|
+
if (next !== value) {
|
|
47672
|
+
node[key2] = next;
|
|
47673
|
+
changed = true;
|
|
47674
|
+
}
|
|
47675
|
+
} else if (value && typeof value === "object" && key2 !== "previousIds" && key2 !== "previousNames") {
|
|
47676
|
+
walk2(value);
|
|
47677
|
+
}
|
|
47678
|
+
}
|
|
47679
|
+
};
|
|
47680
|
+
walk2(raw);
|
|
47681
|
+
return changed;
|
|
47682
|
+
}
|
|
47683
|
+
function publishedUsesOf(removed, methods) {
|
|
47684
|
+
const contracts = loadInterfaceSpecs();
|
|
47685
|
+
const uses = [];
|
|
47686
|
+
for (const ref of removed) {
|
|
47687
|
+
if (ref.kind === "component") uses.push(...publishingUses(ref.id, contracts.filter((i) => i.component === ref.id), () => true));
|
|
47688
|
+
if (ref.kind === "interface") {
|
|
47689
|
+
const contract = contracts.find((i) => i.id === ref.id);
|
|
47690
|
+
if (contract) uses.push(...publishingUses(contract.component, [contract], () => true));
|
|
47691
|
+
}
|
|
47692
|
+
}
|
|
47693
|
+
const types = loadTypeSpecs();
|
|
47694
|
+
for (const ref of removed.filter((r) => r.kind === "type")) {
|
|
47695
|
+
const type = types.find((t) => t.id === ref.id || qualifiedTypeId(t) === ref.id);
|
|
47696
|
+
if (type) uses.push(...publishedCarriers(type));
|
|
47697
|
+
}
|
|
47698
|
+
for (const entry of methods) {
|
|
47699
|
+
const cut = entry.lastIndexOf(".");
|
|
47700
|
+
const contract = cut > 0 ? contracts.find((i) => i.id === entry.slice(0, cut)) : void 0;
|
|
47701
|
+
if (!contract) continue;
|
|
47702
|
+
const name = entry.slice(cut + 1);
|
|
47703
|
+
const source = `${contract.component}.${name}`;
|
|
47704
|
+
const followers = contracts.filter((i) => i !== contract && i.methods.some((m) => followsSource(m.signatureFrom, source, i.component)));
|
|
47705
|
+
uses.push(
|
|
47706
|
+
...publishingUses(contract.component, [contract], (m) => m.name === name),
|
|
47707
|
+
...followers.flatMap((i) => publishingUses(i.component, [i], (m) => followsSource(m.signatureFrom, source, i.component)))
|
|
47708
|
+
);
|
|
47709
|
+
}
|
|
47710
|
+
return mergeUses(uses);
|
|
47711
|
+
}
|
|
46475
47712
|
var METHOD_CASINGS = {
|
|
46476
47713
|
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
46477
47714
|
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
@@ -46816,12 +48053,14 @@ function renameType(typeId, newId, dryRun) {
|
|
|
46816
48053
|
`renaming type "${typeId}"`,
|
|
46817
48054
|
Array.isArray(registerEdits) && registerEdits.length > 0
|
|
46818
48055
|
);
|
|
48056
|
+
const publishedIn = publishedCarriers(type);
|
|
46819
48057
|
if (dryRun) {
|
|
46820
48058
|
return {
|
|
46821
48059
|
from: qualifiedTypeId(type),
|
|
46822
48060
|
to: qualifiedTypeId({ id: newId, subsystem: type.subsystem }),
|
|
46823
48061
|
rewritten: [...new Set(wouldRewrite.filter((spec) => !(spec.kind === "type" && spec.id === oldId)).map((spec) => spec.id))],
|
|
46824
48062
|
keptPublicNames: keptDry.names,
|
|
48063
|
+
publishedIn,
|
|
46825
48064
|
carried: Array.isArray(registerEdits) ? registerEdits : [],
|
|
46826
48065
|
dryRun: true
|
|
46827
48066
|
};
|
|
@@ -46843,6 +48082,7 @@ function renameType(typeId, newId, dryRun) {
|
|
|
46843
48082
|
to: qualifiedTypeId({ id: newId, subsystem: type.subsystem }),
|
|
46844
48083
|
rewritten,
|
|
46845
48084
|
keptPublicNames: kept.names,
|
|
48085
|
+
publishedIn,
|
|
46846
48086
|
carried
|
|
46847
48087
|
};
|
|
46848
48088
|
}
|
|
@@ -48111,7 +49351,7 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
|
|
|
48111
49351
|
const externals = externalsUsedBy(record);
|
|
48112
49352
|
let readPaths = record.readPaths;
|
|
48113
49353
|
if (externals.length > 0) {
|
|
48114
|
-
const bindings =
|
|
49354
|
+
const bindings = bindingModulesOf2(record);
|
|
48115
49355
|
const bindingNote = bindings.length > 0 ? `
|
|
48116
49356
|
The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as the pin has them (\`validate\` compares them with it and reports a stale name, parameter or field as BINDING_DRIFT, with the rename to follow): ${bindings.map((b) => `\`${b}\``).join(", ")}.
|
|
48117
49357
|
` : "\nWhen the code reaches a producer through a hand-written binding module (a typed binding to a native library, a client stub), name it on the implementation as `bindings` \u2014 code linkage, no re-lock \u2014 so `validate` compares it with the pin.\n";
|
|
@@ -48229,11 +49469,11 @@ function memberHop(agentId) {
|
|
|
48229
49469
|
const alias = agentId.slice(0, at);
|
|
48230
49470
|
const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
|
|
48231
49471
|
if (!member) return null;
|
|
48232
|
-
const
|
|
48233
|
-
return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative:
|
|
49472
|
+
const relative27 = path31.relative(getProjectRoot(), member.directory).split(path31.sep).join("/");
|
|
49473
|
+
return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative27 };
|
|
48234
49474
|
}
|
|
48235
|
-
function throughMount(brief, alias,
|
|
48236
|
-
const under = (p) => path31.posix.join(
|
|
49475
|
+
function throughMount(brief, alias, relative27) {
|
|
49476
|
+
const under = (p) => path31.posix.join(relative27, p);
|
|
48237
49477
|
return {
|
|
48238
49478
|
...brief,
|
|
48239
49479
|
agentId: `${alias}::${brief.agentId}`,
|
|
@@ -48241,7 +49481,7 @@ function throughMount(brief, alias, relative25) {
|
|
|
48241
49481
|
...brief.codeFence ? { codeFence: brief.codeFence.map(under) } : {},
|
|
48242
49482
|
...brief.sharedPaths ? { sharedPaths: brief.sharedPaths.map(under) } : {},
|
|
48243
49483
|
...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
|
|
48244
|
-
root: brief.root ? under(brief.root) :
|
|
49484
|
+
root: brief.root ? under(brief.root) : relative27
|
|
48245
49485
|
};
|
|
48246
49486
|
}
|
|
48247
49487
|
function loadRegistry() {
|
|
@@ -48402,7 +49642,7 @@ function externalsUsedBy(record) {
|
|
|
48402
49642
|
}
|
|
48403
49643
|
return out;
|
|
48404
49644
|
}
|
|
48405
|
-
function
|
|
49645
|
+
function bindingModulesOf2(record) {
|
|
48406
49646
|
const ids = new Set(implementedComponents(record).map((c) => c.id));
|
|
48407
49647
|
const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
|
|
48408
49648
|
return [...new Set(loadImplementationSpecs().filter((impl) => contracts.has(impl.contract)).flatMap((impl) => impl.bindings ?? []))];
|
|
@@ -48685,19 +49925,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
|
|
|
48685
49925
|
const backupDir = path36.join(root, BACKUP_DIR, timestampSlug());
|
|
48686
49926
|
let backedUp = false;
|
|
48687
49927
|
for (const rootRel of roots) {
|
|
48688
|
-
const
|
|
48689
|
-
const stagedSpecDir = path36.join(stagingDir,
|
|
49928
|
+
const relative27 = rootRel === "." ? "" : rootRel;
|
|
49929
|
+
const stagedSpecDir = path36.join(stagingDir, relative27, ".wai");
|
|
48690
49930
|
if (!fs23.existsSync(stagedSpecDir)) continue;
|
|
48691
|
-
const liveSpecDir = liveSpecDirFor(path36.join(root,
|
|
49931
|
+
const liveSpecDir = liveSpecDirFor(path36.join(root, relative27));
|
|
48692
49932
|
if (backupExisting && liveSpecDir && fs23.existsSync(liveSpecDir)) {
|
|
48693
|
-
const backupTarget = path36.join(backupDir,
|
|
49933
|
+
const backupTarget = path36.join(backupDir, relative27, path36.basename(liveSpecDir));
|
|
48694
49934
|
fs23.mkdirSync(path36.dirname(backupTarget), { recursive: true });
|
|
48695
49935
|
fs23.renameSync(liveSpecDir, backupTarget);
|
|
48696
49936
|
backedUp = true;
|
|
48697
49937
|
} else if (liveSpecDir && fs23.existsSync(liveSpecDir)) {
|
|
48698
49938
|
fs23.rmSync(liveSpecDir, { recursive: true, force: true });
|
|
48699
49939
|
}
|
|
48700
|
-
const target = path36.join(root,
|
|
49940
|
+
const target = path36.join(root, relative27, ".wai");
|
|
48701
49941
|
fs23.mkdirSync(path36.dirname(target), { recursive: true });
|
|
48702
49942
|
if (fs23.existsSync(target)) fs23.rmSync(target, { recursive: true, force: true });
|
|
48703
49943
|
fs23.renameSync(stagedSpecDir, target);
|
|
@@ -49260,7 +50500,7 @@ function retireSpecialists(apply3) {
|
|
|
49260
50500
|
if (dependency2 && DATA_FACADES.has(dependency2.componentType)) return "facade";
|
|
49261
50501
|
return "neither";
|
|
49262
50502
|
};
|
|
49263
|
-
const
|
|
50503
|
+
const described2 = (id) => {
|
|
49264
50504
|
const dependency2 = byId.get(id);
|
|
49265
50505
|
const fit = fitOf(id);
|
|
49266
50506
|
let what;
|
|
@@ -49273,14 +50513,14 @@ function retireSpecialists(apply3) {
|
|
|
49273
50513
|
const reasonFor = (specialist, decision, settled2) => {
|
|
49274
50514
|
const dependencies = specialist.dependsOn;
|
|
49275
50515
|
if (decision === "pure") {
|
|
49276
|
-
return dependencies.length === 0 ? "no dependencies" : `depends only on pure logic: ${dependencies.map(
|
|
50516
|
+
return dependencies.length === 0 ? "no dependencies" : `depends only on pure logic: ${dependencies.map(described2).join(", ")}`;
|
|
49277
50517
|
}
|
|
49278
50518
|
if (decision === "read") {
|
|
49279
50519
|
const deciding2 = dependencies.filter((id) => fitOf(id) !== "pure");
|
|
49280
|
-
return `${settled2 ? "in or behind a dependency cycle; " : ""}depends on read logic or a data facade: ${deciding2.map(
|
|
50520
|
+
return `${settled2 ? "in or behind a dependency cycle; " : ""}depends on read logic or a data facade: ${deciding2.map(described2).join(", ")}`;
|
|
49281
50521
|
}
|
|
49282
50522
|
const deciding = dependencies.filter((id) => fitOf(id) === "neither");
|
|
49283
|
-
return `depends on ${deciding.map(
|
|
50523
|
+
return `depends on ${deciding.map(described2).join(", ")}, which neither pure nor read logic may depend on`;
|
|
49284
50524
|
};
|
|
49285
50525
|
let classifiedAny;
|
|
49286
50526
|
do {
|
|
@@ -49976,12 +51216,12 @@ Approved: ${stamp} \u2014 this lock predates per-spec approval, so drift is only
|
|
|
49976
51216
|
const carry = isContentReadingRecord(approval) && own2?.state === "approved" ? `
|
|
49977
51217
|
${REEXPRESS_HINT}` : "";
|
|
49978
51218
|
if (diffSize(diff3) === 0) {
|
|
49979
|
-
const owes = own2?.state === "drifted" && !own2.upgraded ? own2.owed !== void 0 ? ` But the approval does not cover this project: ${own2.owed}. \`wairon lock-check\` fails until it is cleared${/never pinned|beyond its pin/.test(own2.owed) ? " \u2014 pin first (`wairon externals pin`), then `wairon lock`" : " \u2014 `wairon lock` re-approves the unchanged design"}.` : own2.release && !own2.release.carried ? own2.release.reason ? ` But ${own2.release.reason}` : ` But it was approved under wairon ${own2.release.from}, and the new release (${own2.release.to}) finds ${own2.release.count ?? 0} issue(s) in the approved design: ${(own2.release.findings ?? []).join("; ")}. Resolve them, then \`wairon lock\`.` : pinDrift ? "" : own2.inputsMoved && own2.inputsMoved.length > 0 ? ` But what it was approved under did change: ${describeGateParts(own2.inputsMoved)}. \`wairon lock\` re-approves the unchanged design under it.` : " But what it was approved under did change: an input the gate identity covers \u2014 the doctrine, the network declaration, a consumed contract's pin, `composition` or a member's approval. `wairon lock` re-approves the unchanged design." : "";
|
|
51219
|
+
const owes = own2?.state === "drifted" && !own2.upgraded ? own2.owed !== void 0 ? ` But the approval does not cover this project: ${own2.owed}. \`wairon lock-check\` fails until it is cleared${/never pinned|beyond its pin/.test(own2.owed) ? " \u2014 pin first (`wairon externals pin`), then `wairon lock`" : /extension pack\(s\) selected/.test(own2.owed) ? " \u2014 make the pack load (install or vendor it, or drop the selection), then `wairon lock`" : " \u2014 `wairon lock` re-approves the unchanged design"}.` : own2.release && !own2.release.carried ? own2.release.reason ? ` But ${own2.release.reason}` : ` But it was approved under wairon ${own2.release.from}, and the new release (${own2.release.to}) finds ${own2.release.count ?? 0} issue(s) in the approved design: ${(own2.release.findings ?? []).join("; ")}. Resolve them, then \`wairon lock\`.` : pinDrift ? "" : own2.inputsMoved && own2.inputsMoved.length > 0 ? ` But what it was approved under did change: ${describeGateParts(own2.inputsMoved)}. \`wairon lock\` re-approves the unchanged design under it.` : " But what it was approved under did change: an input the gate identity covers \u2014 the doctrine, the network declaration, a consumed contract's pin, `composition` or a member's approval. `wairon lock` re-approves the unchanged design." : "";
|
|
49980
51220
|
const released = own2?.release?.carried ? ` Approved under wairon ${own2.release.from}, re-validated under ${own2.release.to}: still approved (\`wairon lock\` refreshes the record's release stamp without a re-approval).` : "";
|
|
49981
51221
|
const ignored = own2?.partsIgnored !== void 0 ? ` Notice: the record's gate parts are ignored \u2014 ${own2.partsIgnored}; \`wairon lock\` rewrites them.` : "";
|
|
49982
51222
|
return {
|
|
49983
51223
|
text: `
|
|
49984
|
-
Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}${childNote}${carry}
|
|
51224
|
+
Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}${stampNotice(own2)}${childNote}${carry}
|
|
49985
51225
|
`,
|
|
49986
51226
|
drifted: pinDrift || own2?.state === "drifted"
|
|
49987
51227
|
};
|
|
@@ -49990,7 +51230,7 @@ Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}
|
|
|
49990
51230
|
if (diff3.changed.length) parts.push(`${diff3.changed.length} changed`);
|
|
49991
51231
|
if (diff3.added.length) parts.push(`${diff3.added.length} added`);
|
|
49992
51232
|
if (diff3.removed.length) parts.push(`${diff3.removed.length} removed`);
|
|
49993
|
-
const named2 = [...diff3.changed, ...diff3.added, ...diff3.removed].slice(0,
|
|
51233
|
+
const named2 = [...diff3.changed, ...diff3.added, ...diff3.removed].slice(0, VERDICT_LIST_LIMIT);
|
|
49994
51234
|
const rest = diffSize(diff3) - named2.length;
|
|
49995
51235
|
const n = diffSize(diff3);
|
|
49996
51236
|
const noun = `${n} spec${n === 1 ? "" : "s"}`;
|
|
@@ -49999,8 +51239,9 @@ Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}
|
|
|
49999
51239
|
text: `
|
|
50000
51240
|
${headline} \u2014 approved ${stamp}:
|
|
50001
51241
|
` + named2.map((p) => ` ${p}`).join("\n") + (rest > 0 ? `
|
|
50002
|
-
\u2026 and ${rest} more` : "") + (own2?.owed !== void 0 ? `
|
|
50003
|
-
The approval also owes: ${own2.owed}.` : "") +
|
|
51242
|
+
\u2026 and ${rest} more \u2014 \`wairon status --all\` lists every one` : "") + (own2?.owed !== void 0 ? `
|
|
51243
|
+
The approval also owes: ${own2.owed}.` : "") + (own2?.stampProblem !== void 0 ? `
|
|
51244
|
+
${stampNotice(own2).trim()}` : "") + childNote + carry + "\n",
|
|
50004
51245
|
drifted: true,
|
|
50005
51246
|
moved: [...diff3.changed.map((p) => `~ ${p}`), ...diff3.added.map((p) => `+ ${p}`), ...diff3.removed.map((p) => `- ${p}`)]
|
|
50006
51247
|
};
|
|
@@ -50008,6 +51249,10 @@ The approval also owes: ${own2.owed}.` : "") + childNote + carry + "\n",
|
|
|
50008
51249
|
return quiet;
|
|
50009
51250
|
}
|
|
50010
51251
|
}
|
|
51252
|
+
var VERDICT_LIST_LIMIT = 40;
|
|
51253
|
+
function stampNotice(own2) {
|
|
51254
|
+
return own2?.stampProblem !== void 0 ? ` Notice: ${own2.stampProblem} \u2014 the identity alone decided this, and \`wairon lock\` replaces the stamp with this release.` : "";
|
|
51255
|
+
}
|
|
50011
51256
|
function isContentReadingRecord(record) {
|
|
50012
51257
|
return record.specsReading !== "design";
|
|
50013
51258
|
}
|
|
@@ -50119,13 +51364,13 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
|
|
|
50119
51364
|
|
|
50120
51365
|
### How you operate
|
|
50121
51366
|
- **To design/modify specs**: Use **\`sdd-architect\`** skill (in \`.claude/skills/\` or \`.gemini/skills/\`).
|
|
50122
|
-
- **Manage specs via MCP tools only** (namespaced if needed; do not edit specs manually): author with \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_set_endpoints\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_set_public_interfaces\`, \`sdd_update_spec\` (a granular delta, with \`dryRun\`) and \`sdd_delete_spec\`; rename and move with traces with \`sdd_rename_component\`, \`sdd_rename_method\` (with \`dryRun\`: it names the consumers a renamed verb breaks), \`sdd_rename_param\`, \`sdd_rename_type\`, \`sdd_rename_field\` and \`sdd_move_methods\`; read with \`sdd_get_status\`, \`sdd_get_spec\`, \`sdd_validate_tree\`, \`sdd_get_agent_brief\`, \`sdd_get_network_flows\` and \`sdd_explain_flow\` (the network), \`sdd_list_consumers\` and \`sdd_surface_diff\` (who uses this project's exports, method by method, and what its public surface changed since the last approval), \`sdd_get_externals_status\` and \`sdd_pack_impact\`; the network with \`sdd_set_network\`; externals with \`sdd_add_external\`, \`sdd_update_external\`, \`sdd_pin_externals\` and \`sdd_remove_external\`; the family's shape with the member tools below. The legacy topology tools \`getAgent\`, \`getProjectConfig\`, \`listAgents\`, \`listDomains\` and \`validateTopology\` read the generated agent registry only.
|
|
51367
|
+
- **Manage specs via MCP tools only** (namespaced if needed; do not edit specs manually): author with \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_set_endpoints\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_set_public_interfaces\`, \`sdd_update_spec\` (a granular delta, with \`dryRun\`) and \`sdd_delete_spec\`; rename and move with traces with \`sdd_rename_component\`, \`sdd_rename_method\` (with \`dryRun\`: it names the consumers a renamed verb breaks), \`sdd_rename_param\`, \`sdd_rename_type\`, \`sdd_rename_field\`, \`sdd_rename_spec\` (a contract or an implementation on its own) and \`sdd_move_methods\`, and move a component or a type to another subsystem with \`sdd_move_spec\` (every one with \`dryRun\`; \`sdd_delete_spec\` and a contract method removed through \`sdd_update_spec\` name the consumers they break, as the renames do); read with \`sdd_get_status\`, \`sdd_get_spec\`, \`sdd_validate_tree\`, \`sdd_get_agent_brief\`, \`sdd_get_network_flows\` and \`sdd_explain_flow\` (the network), \`sdd_list_consumers\` and \`sdd_surface_diff\` (who uses this project's exports, method by method, and what its public surface changed since the last approval), \`sdd_get_externals_status\` and \`sdd_pack_impact\`; the network with \`sdd_set_network\`; externals with \`sdd_add_external\`, \`sdd_update_external\`, \`sdd_pin_externals\` and \`sdd_remove_external\`; the family's shape with the member tools below. The legacy topology tools \`getAgent\`, \`getProjectConfig\`, \`listAgents\`, \`listDomains\` and \`validateTopology\` read the generated agent registry only.
|
|
50123
51368
|
- **Growing a system \u2014 subsystems \u2192 parts \u2192 projects**: boundaries are earned. Start with subsystems in one folder. A piece that needs its own folder or repository but stays the same system is a **part**: declared in \`.wai/project.yaml\` \`members\` by one location key (\`scheduler: services/scheduler\`, \`admin: ../admin\`, \`payments: git@host:acme/payments.git#<commit>\`; long form \`{ source, as, description }\`), its subsystems this project's own \u2014 local ids, this project's lock. \`sdd_add_member\` creates a part by default (\`as: project\` for a project) and \`sdd_externalize_subsystem\` moves a subsystem into one. A **project** is an independent boundary \u2014 its own id, exports, lock \u2014 reached as \`alias::name\`; make one only when it needs its own team, release, approval or public surface: \`sdd_promote_member\` (\`sdd_demote_member\` back). What a member is follows from its content (an id, an L0 or a lock make a project).
|
|
50124
51369
|
- **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan (the human's CLI calls the same plan \`--report\`); applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root \u2014 its specs, its L0 export table and its \`project.yaml\` are written by a session opened in that member's folder (its own guide and \`.mcp.json\`), and the tools here refuse such a write naming that folder.
|
|
50125
51370
|
- **\`alias::name\`**: An id without \`::\` is local to the project that writes it. Anything another project provides is referenced as \`alias::name\` \u2014 the alias is one of your members or declared \`externals\`, the name a public name in that project's L0 export table. A reference to something it does not export is reported (\`EXTERNAL_NOT_EXPORTED\`).
|
|
50126
51371
|
- **Deprecated forms** (they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them): a leading \`::\` (\`::shared::error-type\`), \`super::\` (\`super::sibling_comp\`), member paths (\`billing::invoice::invoice_portal\`), and an L1 subsystem carrying \`projectPath\` (\`DEPRECATED_MOUNT_FORM\`).
|
|
50127
51372
|
- **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
|
|
50128
|
-
- **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
|
|
51373
|
+
- **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. When a change to an approved design stays inside one subsystem, the human may re-approve just that subsystem with \`wairon lock --subsystem <id>\` (refused as a first approval, when anything outside the subsystem moved, and for a member project's subsystem, which is locked at the member's own root). In a family, members lock first at their own roots, then the parent pins them. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
|
|
50129
51374
|
- **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`. It is left out of the approval, so promoting a status never reopens an approved design.
|
|
50130
51375
|
- **Prose is design; linkage is not**: an L4/L5 prose change \u2014 an implementation's or a method's description, intent or narrative step text \u2014 IS a design change: it re-opens the approval, so \`wairon lock\` is owed before code is implemented against it (an implement step asked for in the same turn as a prose edit is refused until the human re-locks; sequence spec turn \u2192 lock \u2192 code turn). Only code linkage is outside it.
|
|
50131
51376
|
- **Code linkage is not approval**: the lock approves the DESIGN. Where it is realized \u2014 \`sourcePath\`, \`symbol\`, \`exportedVia\`, \`simPath\`, \`injectedParams\`, conformance tiers, timestamps \u2014 is outside the approved digests, so setting or changing it never asks for a re-lock. Declare each implementation's planned \`sourcePath\` at design time: a named file not written yet is \`SOURCE_FILE_PLANNED\` (a notice), at method level as at implementation level; once the component's realization begins (any file it names exists), a contract method naming no file is \`METHOD_SOURCE_PATH_MISSING\` (warning) and one its existing file does not hold is \`UNREALIZED_METHOD\`. \`rules.conformance.requireCode: true\` makes the planned and unlinked notices errors. Briefs fence planned files, marked \`(planned \u2014 create it)\`.
|
|
@@ -50732,7 +51977,9 @@ function getStatusReport(options = {}, decor) {
|
|
|
50732
51977
|
members.forEach((member, j) => {
|
|
50733
51978
|
const isLast = ownSubs.length + j === total - 1;
|
|
50734
51979
|
const form = member.mountForm === "mount" ? mark.draft(" [mount form]") : "";
|
|
50735
|
-
const
|
|
51980
|
+
const top = family.nodes.find((n) => n.namespace === "")?.directory;
|
|
51981
|
+
const sibling = top !== void 0 && path39.relative(top, member.directory).startsWith("..") ? ` \xB7 ${storageOf("path", member.directory, void 0)}` : "";
|
|
51982
|
+
const label7 = `[Project] ${member.mountAlias ?? member.namespace} (${member.id ?? "no id"})${sibling}`;
|
|
50736
51983
|
const approval = approvalTag(options.approvals?.find((a) => a.key === member.namespace));
|
|
50737
51984
|
output += `${mark.structure(indent + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "))}${mark.layer("project", label7)}${form}${approval}
|
|
50738
51985
|
`;
|
|
@@ -50764,7 +52011,8 @@ function getStatusReport(options = {}, decor) {
|
|
|
50764
52011
|
} else if (own2) {
|
|
50765
52012
|
const release = own2.release ? own2.release.carried ? ` (approved under wairon ${own2.release.from}, re-validated under ${own2.release.to}: still approved)` : own2.release.reason ? ` (${own2.release.reason})` : ` (approved under wairon ${own2.release.from}; the new release finds ${own2.release.count ?? 0} issue(s) in the approved design)` : "";
|
|
50766
52013
|
const ignored = own2.partsIgnored !== void 0 ? ` (notice: the record's gate parts are ignored \u2014 ${own2.partsIgnored}; \`wairon lock\` rewrites them)` : "";
|
|
50767
|
-
|
|
52014
|
+
const stamp = own2.stampProblem !== void 0 ? ` (notice: ${own2.stampProblem}; \`wairon lock\` replaces the stamp)` : "";
|
|
52015
|
+
output += `${mark.layer("system", "Approval:")} this project is ${OWN_APPROVAL_WORDS[own2.state]}${release}${own2.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}${ignored}${stamp}
|
|
50768
52016
|
`;
|
|
50769
52017
|
}
|
|
50770
52018
|
return { text: output, failed: false };
|
|
@@ -51585,9 +52833,14 @@ function canonical(value) {
|
|
|
51585
52833
|
}
|
|
51586
52834
|
function writeSpec(restatement) {
|
|
51587
52835
|
const bound2 = candidateOptions();
|
|
52836
|
+
const id = restatement.kind === "system" ? "system" : restatement.spec.id;
|
|
51588
52837
|
const parentRef = restatementParent(restatement);
|
|
52838
|
+
const foreign = crossProjectWriteRefusal(
|
|
52839
|
+
[{ kind: restatement.kind, id }, ...parentRef && typeof parentRef.id === "string" ? [{ kind: parentRef.kind, id: parentRef.id }] : []],
|
|
52840
|
+
CREATE_TOOL[restatement.kind]
|
|
52841
|
+
);
|
|
52842
|
+
if (foreign !== void 0) throw new Error(foreign);
|
|
51589
52843
|
const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
|
|
51590
|
-
const id = restatement.kind === "system" ? "system" : restatement.spec.id;
|
|
51591
52844
|
const existing = loadSpec(restatement.kind, id);
|
|
51592
52845
|
if (!existing && TRACED_KINDS.has(restatement.kind)) {
|
|
51593
52846
|
const spec = restatement.spec;
|
|
@@ -51603,21 +52856,83 @@ function writeSpec(restatement) {
|
|
|
51603
52856
|
if (application.refusal !== void 0) throw new Error(application.refusal);
|
|
51604
52857
|
const names = introducedNameProblems(restatement.kind, application.spec, existing);
|
|
51605
52858
|
if (names.length) throw new Error(nameRefusal(restatement.kind, id, names));
|
|
52859
|
+
refuseMountForm(restatement.kind, application.spec);
|
|
51606
52860
|
const gateNotices = restatement.kind === "component" ? judgeComponent(application.spec, bound2) : [];
|
|
51607
52861
|
gateNotices.push(...judgeInTree(restatement.kind, application.spec, bound2));
|
|
51608
|
-
|
|
52862
|
+
let cascade = [];
|
|
52863
|
+
if (restatement.kind === "interface" && existing) {
|
|
52864
|
+
const dropped = removedByName(existing.methods, application.spec.methods);
|
|
52865
|
+
if (dropped.length > 0) {
|
|
52866
|
+
const removal = methodRemoval(scanAllSpecs({ memberDepth: 0 }), id, application.spec.component, dropped);
|
|
52867
|
+
if (removal.references.length > 0) throw new Error(methodReferencedRefusal(id, removal.referenced, removal.references));
|
|
52868
|
+
cascade = removal.cascade;
|
|
52869
|
+
}
|
|
52870
|
+
}
|
|
51609
52871
|
const testsToRevisit = testsInvalidatedBy(restatement.kind, id, application.changedMethods, [existing, application.spec], bound2);
|
|
52872
|
+
const cascaded = cascade.map((c) => `${c.implementation}.${c.method}`);
|
|
52873
|
+
const name = application.spec.name;
|
|
52874
|
+
const status = application.status ? { status: application.status } : {};
|
|
52875
|
+
if (restatement.dryRun) {
|
|
52876
|
+
return {
|
|
52877
|
+
kind: restatement.kind,
|
|
52878
|
+
id,
|
|
52879
|
+
name,
|
|
52880
|
+
replacedExisting: application.replacedExisting,
|
|
52881
|
+
...status,
|
|
52882
|
+
notices: [...gateNotices, ...application.notices, ...cascadeNotices(cascade, true)],
|
|
52883
|
+
respellings: application.respellings,
|
|
52884
|
+
testsToRevisit,
|
|
52885
|
+
...cascaded.length > 0 ? { cascaded } : {},
|
|
52886
|
+
dryRun: true
|
|
52887
|
+
};
|
|
52888
|
+
}
|
|
52889
|
+
const persisted = persistCandidate(restatement.kind, application.spec);
|
|
52890
|
+
for (const [implementation, methods] of groupCascade(cascade)) {
|
|
52891
|
+
updateSpec("implementation", implementation, { methods: methods.map((m) => ({ name: m, action: "delete" })) });
|
|
52892
|
+
}
|
|
51610
52893
|
return {
|
|
51611
52894
|
kind: restatement.kind,
|
|
51612
52895
|
id,
|
|
51613
|
-
name
|
|
52896
|
+
name,
|
|
51614
52897
|
replacedExisting: application.replacedExisting,
|
|
51615
|
-
...
|
|
51616
|
-
notices: [...gateNotices, ...persisted.notices, ...application.notices],
|
|
52898
|
+
...status,
|
|
52899
|
+
notices: [...gateNotices, ...persisted.notices, ...application.notices, ...cascadeNotices(cascade, false)],
|
|
51617
52900
|
respellings: application.respellings,
|
|
51618
|
-
testsToRevisit
|
|
52901
|
+
testsToRevisit,
|
|
52902
|
+
...cascaded.length > 0 ? { cascaded } : {}
|
|
51619
52903
|
};
|
|
51620
52904
|
}
|
|
52905
|
+
var CREATE_TOOL = {
|
|
52906
|
+
system: "sdd_initialize_system",
|
|
52907
|
+
subsystem: "sdd_add_subsystem",
|
|
52908
|
+
component: "sdd_add_component",
|
|
52909
|
+
interface: "sdd_define_interface",
|
|
52910
|
+
implementation: "sdd_write_narrative",
|
|
52911
|
+
type: "sdd_add_type"
|
|
52912
|
+
};
|
|
52913
|
+
function methodRemoval(index, contract, component, removed) {
|
|
52914
|
+
const realizing = index.implementations.filter((impl) => impl.contract === contract);
|
|
52915
|
+
const stillDeclared = (m) => index.interfaces.some((i) => i.component === component && i.id !== contract && i.methods.some((x) => x.name === m));
|
|
52916
|
+
const referenced = removed.filter((m) => !stillDeclared(m));
|
|
52917
|
+
const inside = (r) => r.kind === "implementation" && realizing.some((impl) => impl.id === r.id) && removed.some((m) => r.position === `methods.${m}` || r.position.startsWith(`methods.${m}.`));
|
|
52918
|
+
const references = referenced.length === 0 ? [] : specIndexMethodReferences(index, component, referenced).filter((r) => !inside(r));
|
|
52919
|
+
const cascade = realizing.flatMap((impl) => removed.filter((m) => impl.methods.some((x) => x.name === m)).map((m) => ({ implementation: impl.id, method: m })));
|
|
52920
|
+
return { referenced, references, cascade };
|
|
52921
|
+
}
|
|
52922
|
+
function methodReferencedRefusal(contract, removed, references) {
|
|
52923
|
+
return `Refusing to remove ${removed.length === 1 ? "method" : "methods"} ${removed.map((m) => `"${m}"`).join(", ")} from contract "${contract}": specs that stay still hold ${references.length === 1 ? "a reference" : `${references.length} references`} to ${removed.length === 1 ? "it" : "them"} \u2014 the invalid method references validate would report:
|
|
52924
|
+
${references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`).join("\n")}
|
|
52925
|
+
Edit those specs first \u2014 retarget or remove each call. Nothing was written.`;
|
|
52926
|
+
}
|
|
52927
|
+
function groupCascade(cascade) {
|
|
52928
|
+
const out = /* @__PURE__ */ new Map();
|
|
52929
|
+
for (const c of cascade) out.set(c.implementation, [...out.get(c.implementation) ?? [], c.method]);
|
|
52930
|
+
return out;
|
|
52931
|
+
}
|
|
52932
|
+
function cascadeNotices(cascade, dryRun) {
|
|
52933
|
+
if (cascade.length === 0) return [];
|
|
52934
|
+
return [`Cascaded: ${dryRun ? "would remove" : "removed"} the implementation ${cascade.length === 1 ? "entry" : "entries"} ${cascade.map((c) => `"${c.implementation}.${c.method}"`).join(", ")} with ${cascade.length === 1 ? "its" : "their"} contract method \u2014 a narrative realizes its contract method and means nothing without it.`];
|
|
52935
|
+
}
|
|
51621
52936
|
function judgeComponent(candidate, options) {
|
|
51622
52937
|
const verdict = validateComponentCandidate(candidate, options);
|
|
51623
52938
|
if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
|
|
@@ -51636,6 +52951,10 @@ Nothing was written. Route the dependency the way the message names, or bind the
|
|
|
51636
52951
|
return noticesFrom(verdict);
|
|
51637
52952
|
}
|
|
51638
52953
|
function persistCandidate(kind, spec) {
|
|
52954
|
+
refuseMountForm(kind, spec);
|
|
52955
|
+
return { notices: saveSpec(kind, spec) };
|
|
52956
|
+
}
|
|
52957
|
+
function refuseMountForm(kind, spec) {
|
|
51639
52958
|
const projectPath = kind === "subsystem" ? spec.projectPath : void 0;
|
|
51640
52959
|
if (projectPath !== void 0 && projectPath.trim() !== "") {
|
|
51641
52960
|
const id = spec.id;
|
|
@@ -51643,7 +52962,6 @@ function persistCandidate(kind, spec) {
|
|
|
51643
52962
|
`mount form refused: subsystem "${id}" names a projectPath ("${projectPath}"), which is the legacy L1 mount form \u2014 wairon never writes it. A member project is declared in project.yaml \`members\`: use sdd_add_member (or \`wairon member add ${id} ${projectPath}\`), which scaffolds the member project and declares it.`
|
|
51644
52963
|
);
|
|
51645
52964
|
}
|
|
51646
|
-
return { notices: saveSpec(kind, spec) };
|
|
51647
52965
|
}
|
|
51648
52966
|
function testsInvalidatedBy(kind, id, methods, specs, options, written = []) {
|
|
51649
52967
|
const testRoots = options.rules?.conformance?.testRoots ?? [];
|
|
@@ -51671,25 +52989,51 @@ function searchedMethods(kind, id, names, specs, written) {
|
|
|
51671
52989
|
});
|
|
51672
52990
|
}
|
|
51673
52991
|
function deleteSpec2(kind, id, dryRun, force) {
|
|
51674
|
-
|
|
52992
|
+
const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_delete_spec");
|
|
52993
|
+
if (foreign !== void 0) throw new Error(foreign);
|
|
52994
|
+
if (kind === "system") {
|
|
52995
|
+
throw new Error("Refusing to delete the L0 System spec: it is the root every other spec hangs from, and is never deleted. Nothing was deleted.");
|
|
52996
|
+
}
|
|
52997
|
+
if (kind !== "type" && id.includes("::")) {
|
|
51675
52998
|
const deleted = dryRun ? loadSpec(kind, id) !== null : deleteSpec(kind, id);
|
|
51676
|
-
|
|
52999
|
+
if (!deleted && loadSpec(kind, id) === null) throw new Error(`spec-missing: no ${kind} has the id "${id}" \u2014 nothing was deleted.`);
|
|
53000
|
+
return { kind, id, deleted: deleted && !dryRun, dryRun: dryRun === true, removed: [{ kind, id }], references: [], testsToRevisit: [], published: [] };
|
|
51677
53001
|
}
|
|
51678
53002
|
const bound2 = candidateOptions();
|
|
51679
53003
|
const index = scanAllSpecs({ memberDepth: 0 });
|
|
51680
53004
|
const system = loadSpec("system", "system");
|
|
51681
53005
|
const removed = specIndexSubtreeOf(index, kind, id);
|
|
51682
|
-
if (removed.length === 0)
|
|
53006
|
+
if (removed.length === 0) throw new Error(missingRefusal(kind, id, index));
|
|
51683
53007
|
const references = specIndexReferencesTo(index, removed, system);
|
|
53008
|
+
const published = publishedUsesOf(removed, []);
|
|
51684
53009
|
const testsToRevisit = removed.flatMap((ref) => {
|
|
51685
53010
|
const spec = specOf(index, ref);
|
|
51686
53011
|
const methods = (spec?.methods ?? []).map((m) => m.name);
|
|
51687
53012
|
return ref.kind === "interface" || ref.kind === "implementation" || ref.kind === "type" ? testsInvalidatedBy(ref.kind, ref.id, methods, [spec], bound2) : [];
|
|
51688
53013
|
});
|
|
51689
|
-
if (dryRun) return { kind, id, deleted: false, dryRun: true, removed, references, testsToRevisit };
|
|
53014
|
+
if (dryRun) return { kind, id, deleted: false, dryRun: true, removed, references, testsToRevisit, published };
|
|
51690
53015
|
if (references.length > 0 && !force) throw new Error(referencedRefusal(kind, id, references));
|
|
51691
53016
|
for (const ref of removed) deleteSpec(ref.kind, ref.id);
|
|
51692
|
-
return { kind, id, deleted: true, dryRun: false, removed, references, testsToRevisit };
|
|
53017
|
+
return { kind, id, deleted: true, dryRun: false, removed, references, testsToRevisit, published };
|
|
53018
|
+
}
|
|
53019
|
+
function missingRefusal(kind, id, index) {
|
|
53020
|
+
const pools = {
|
|
53021
|
+
subsystem: index.subsystems,
|
|
53022
|
+
component: index.components,
|
|
53023
|
+
interface: index.interfaces,
|
|
53024
|
+
implementation: index.implementations,
|
|
53025
|
+
type: index.types
|
|
53026
|
+
};
|
|
53027
|
+
const hints = [];
|
|
53028
|
+
const elsewhere = Object.keys(pools).filter((k) => k !== kind && pools[k].some((s) => s.id === id));
|
|
53029
|
+
if (elsewhere.length > 0) hints.push(`"${id}" is ${elsewhere.map((k) => `a ${k}`).join(" and ")} \u2014 pass kind "${elsewhere[0]}"`);
|
|
53030
|
+
const dot = id.lastIndexOf(".");
|
|
53031
|
+
if (kind === "type" && dot > 0 && index.types.some((t) => t.id === id.slice(dot + 1))) {
|
|
53032
|
+
hints.push(`a type is addressed by its bare id, or as <subsystem>::<id> \u2014 "${id.slice(dot + 1)}" or "${id.slice(0, dot)}::${id.slice(dot + 1)}"`);
|
|
53033
|
+
}
|
|
53034
|
+
const known = pools[kind].map((s) => kind === "type" ? qualifiedTypeId(s) : s.id).sort();
|
|
53035
|
+
const listed = known.length === 0 ? `This project has no ${kind} at all.` : `Its ${kind}s: ${known.slice(0, 12).join(", ")}${known.length > 12 ? `, \u2026 (${known.length} in all)` : ""}.`;
|
|
53036
|
+
return `spec-missing: no ${kind} has the id "${id}" \u2014 nothing was deleted.${hints.length > 0 ? ` ${hints.join("; ")}.` : ""} ${listed}`;
|
|
51693
53037
|
}
|
|
51694
53038
|
function specOf(index, ref) {
|
|
51695
53039
|
const pool = ref.kind === "subsystem" ? index.subsystems : ref.kind === "component" ? index.components : ref.kind === "interface" ? index.interfaces : ref.kind === "implementation" ? index.implementations : ref.kind === "type" ? index.types : [];
|
|
@@ -51702,10 +53046,29 @@ Edit those specs first, or pass force: true to delete anyway and leave them dang
|
|
|
51702
53046
|
}
|
|
51703
53047
|
function updateSpecGated(kind, id, delta, dryRun) {
|
|
51704
53048
|
const bound2 = candidateOptions();
|
|
53049
|
+
const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_update_spec");
|
|
53050
|
+
if (foreign !== void 0) throw new Error(foreign);
|
|
51705
53051
|
const testRoots = bound2.rules?.conformance?.testRoots ?? [];
|
|
51706
53052
|
const stored = loadSpec(kind, id);
|
|
53053
|
+
const removedMethods = kind === "interface" ? methodsDeltaRemoves(stored, delta) : [];
|
|
53054
|
+
let cascade = [];
|
|
53055
|
+
let published = [];
|
|
53056
|
+
if (removedMethods.length > 0) {
|
|
53057
|
+
const removal = methodRemoval(scanAllSpecs({ memberDepth: 0 }), id, stored.component, removedMethods);
|
|
53058
|
+
if (removal.references.length > 0) throw new Error(methodReferencedRefusal(id, removal.referenced, removal.references));
|
|
53059
|
+
cascade = removal.cascade;
|
|
53060
|
+
published = publishedUsesOf([], removal.referenced.map((m) => `${id}.${m}`));
|
|
53061
|
+
}
|
|
51707
53062
|
const gate = componentCandidateGate(bound2, stored?.subsystem, stored);
|
|
51708
53063
|
const report3 = updateSpec(kind, id, delta, gate, dryRun);
|
|
53064
|
+
if (cascade.length > 0 && report3.changes.length > 0) {
|
|
53065
|
+
for (const [implementation, methods] of groupCascade(cascade)) {
|
|
53066
|
+
updateSpec("implementation", implementation, { methods: methods.map((m) => ({ name: m, action: "delete" })) }, void 0, dryRun);
|
|
53067
|
+
}
|
|
53068
|
+
report3.cascaded = cascade.map((c) => `${c.implementation}.${c.method}`);
|
|
53069
|
+
report3.notices.push(...cascadeNotices(cascade, dryRun === true));
|
|
53070
|
+
}
|
|
53071
|
+
if (published.length > 0 && report3.changes.length > 0) report3.published = published;
|
|
51709
53072
|
if (kind === "component" && typeof delta?.transport === "string" && report3.changes.some((c) => c.path === "transport")) {
|
|
51710
53073
|
report3.notices.push(...adapterTransportImpact(id, delta.transport));
|
|
51711
53074
|
}
|
|
@@ -51720,6 +53083,14 @@ function updateSpecGated(kind, id, delta, dryRun) {
|
|
|
51720
53083
|
}
|
|
51721
53084
|
return report3;
|
|
51722
53085
|
}
|
|
53086
|
+
function methodsDeltaRemoves(stored, delta) {
|
|
53087
|
+
const held = (stored?.methods ?? []).map((m) => m.name);
|
|
53088
|
+
if (held.length === 0 || !delta || typeof delta !== "object") return [];
|
|
53089
|
+
const methods = delta.methods;
|
|
53090
|
+
if (Array.isArray(methods) && methods.length === 0 || Array.isArray(delta.unset) && delta.unset.includes("methods")) return held;
|
|
53091
|
+
if (!Array.isArray(methods)) return [];
|
|
53092
|
+
return methods.filter((m) => m && typeof m === "object" && (m.action === "delete" || m.remove === true) && held.includes(m.name)).map((m) => String(m.name));
|
|
53093
|
+
}
|
|
51723
53094
|
function adapterTransportImpact(portal, transport) {
|
|
51724
53095
|
const components = loadComponentSpecs();
|
|
51725
53096
|
if (components.find((c) => c.id === portal)?.componentType !== "Portal") return [];
|
|
@@ -51765,8 +53136,34 @@ function judgeMoveHome(component, source, options) {
|
|
|
51765
53136
|
return { codes: [...new Set(codes)], verdict, ceiling };
|
|
51766
53137
|
}
|
|
51767
53138
|
function moveMethods2(from, to, methods, dryRun) {
|
|
53139
|
+
const foreign = crossProjectWriteRefusal([{ kind: "component", id: from }, { kind: "component", id: to }], "sdd_move_methods");
|
|
53140
|
+
if (foreign !== void 0) throw new Error(foreign);
|
|
51768
53141
|
return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
|
|
51769
53142
|
}
|
|
53143
|
+
function moveSpec2(kind, id, subsystem, dryRun) {
|
|
53144
|
+
const foreign = crossProjectWriteRefusal(
|
|
53145
|
+
[{ kind, id }, ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
|
|
53146
|
+
"sdd_move_spec"
|
|
53147
|
+
);
|
|
53148
|
+
if (foreign !== void 0) throw new Error(foreign);
|
|
53149
|
+
if (kind !== "component") return moveSpec(kind, id, subsystem, dryRun);
|
|
53150
|
+
const bound2 = candidateOptions();
|
|
53151
|
+
const plan6 = moveSpec(kind, id, subsystem, true);
|
|
53152
|
+
const moving = plan6.moved.filter((ref) => ref.kind === "component").map((ref) => loadSpec("component", ref.id)).filter((spec) => spec !== null).map((spec) => ({ ...spec, subsystem: plan6.to }));
|
|
53153
|
+
const [first, ...rest] = moving;
|
|
53154
|
+
const verdict = first ? introducedFindings("component", first, bound2, rest) : { errors: [], warnings: [], notices: [] };
|
|
53155
|
+
if (verdict.errors.length > 0) {
|
|
53156
|
+
throw new Error(
|
|
53157
|
+
`Refused: moving component "${id}" to subsystem "${plan6.to}" would introduce ${verdict.errors.length === 1 ? "an error" : `${verdict.errors.length} errors`} validate reports \u2014 a forbidden edge between two blocks, or an edge into another subsystem whose target is not a Portal:
|
|
53158
|
+
${verdict.errors.map((e) => `- ${e.code}: ${e.message}`).join("\n")}
|
|
53159
|
+
|
|
53160
|
+
Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it.`
|
|
53161
|
+
);
|
|
53162
|
+
}
|
|
53163
|
+
const notices = noticesFrom(verdict);
|
|
53164
|
+
const report3 = dryRun ? plan6 : moveSpec(kind, id, subsystem, false);
|
|
53165
|
+
return { ...report3, notices: [...report3.notices, ...notices] };
|
|
53166
|
+
}
|
|
51770
53167
|
|
|
51771
53168
|
// src/mcp/build.ts
|
|
51772
53169
|
var fs26 = __toESM(require("fs"));
|
|
@@ -53659,17 +55056,24 @@ function boundNode(family, bound2) {
|
|
|
53659
55056
|
function nodeAt(family, dir) {
|
|
53660
55057
|
return family.nodes.find((n) => path45.resolve(n.directory) === path45.resolve(dir));
|
|
53661
55058
|
}
|
|
55059
|
+
function memberLocation(boundDir, written) {
|
|
55060
|
+
const location = parseMemberSource(written);
|
|
55061
|
+
if (location.storage === "path" && location.problem === void 0) {
|
|
55062
|
+
const sibling = path45.resolve(boundDir, written);
|
|
55063
|
+
if (path45.relative(sibling, boundDir).startsWith("..")) return { dir: sibling };
|
|
55064
|
+
return { refused: `the path "${written}" names a directory holding {bound} itself (${boundDir}), never a member of it` };
|
|
55065
|
+
}
|
|
55066
|
+
const dir = resolve36(boundDir, written);
|
|
55067
|
+
if (dir !== null) return { dir };
|
|
55068
|
+
return { refused: `the path "${written}" does not resolve strictly within {bound} (${boundDir}), and is not a sibling written with a leading \`../\`${location.problem ? ` (${location.problem})` : ""}` };
|
|
55069
|
+
}
|
|
53662
55070
|
function planAttach(family, bound2, request) {
|
|
53663
55071
|
const plan6 = emptyPlan(family, request);
|
|
53664
55072
|
const node = boundNode(family, bound2);
|
|
53665
55073
|
const alias = request.alias ?? "";
|
|
53666
|
-
const
|
|
53667
|
-
const
|
|
53668
|
-
|
|
53669
|
-
const dir = sibling !== null && !holdsBound ? sibling : resolve36(node.directory, request.path ?? "");
|
|
53670
|
-
if (dir === null) {
|
|
53671
|
-
refuse(plan6, "not-contained", bound2, holdsBound ? `the path "${request.path ?? ""}" names a directory holding ${label4(bound2)} itself (${node.directory}), never a member of it` : `the path "${request.path ?? ""}" does not resolve strictly within ${label4(bound2)} (${node.directory}), and is not a sibling written with a leading \`../\`${location.problem ? ` (${location.problem})` : ""}`);
|
|
53672
|
-
}
|
|
55074
|
+
const located2 = memberLocation(node.directory, request.path ?? "");
|
|
55075
|
+
const dir = "dir" in located2 ? located2.dir : null;
|
|
55076
|
+
if ("refused" in located2) refuse(plan6, "not-contained", bound2, located2.refused.replace("{bound}", label4(bound2)));
|
|
53673
55077
|
const config = configAt3(node.directory) ?? {};
|
|
53674
55078
|
const held = ownGet(config.members, alias);
|
|
53675
55079
|
const heldPath = held !== void 0 ? memberLocationOf(held) : void 0;
|
|
@@ -53830,9 +55234,12 @@ function planAdopt(family, bound2, request) {
|
|
|
53830
55234
|
if (!external) refuse(plan6, "not-an-external", bound2, `${label4(bound2)} declares no external "${alias}"`);
|
|
53831
55235
|
else if (request.relocation) return planRelocatedAdopt(plan6, family, bound2, alias, external, request.relocation);
|
|
53832
55236
|
else {
|
|
53833
|
-
|
|
53834
|
-
|
|
53835
|
-
|
|
55237
|
+
const written = externalSourceOf2(external)?.path;
|
|
55238
|
+
const located2 = written !== void 0 ? memberLocation(boundDir, written) : null;
|
|
55239
|
+
dir = located2 !== null && "dir" in located2 ? located2.dir : null;
|
|
55240
|
+
if (located2 === null) refuse(plan6, "not-contained", bound2, `the external "${alias}" names no source.path`);
|
|
55241
|
+
else if ("refused" in located2) refuse(plan6, "not-contained", bound2, `the external "${alias}" is found at "${written}": ${located2.refused.replace("{bound}", label4(bound2))}`);
|
|
55242
|
+
else if (nodeAt(family, located2.dir)) refuse(plan6, "already-member", nodeAt(family, located2.dir).namespace, `${located2.dir} is already the family project ${label4(nodeAt(family, located2.dir).namespace)}`);
|
|
53836
55243
|
}
|
|
53837
55244
|
if (plan6.refusals.length > 0) return plan6;
|
|
53838
55245
|
return adoptEdits(plan6, family, bound2, alias, external, dir);
|
|
@@ -54259,8 +55666,7 @@ function planInternalize(family, bound2, request) {
|
|
|
54259
55666
|
const alias = request.alias ?? "";
|
|
54260
55667
|
const part = familyNode(family, bound2)?.parts.find((p) => p.alias === alias);
|
|
54261
55668
|
if (part) return planPartInternalize(plan6, family, bound2, alias, part);
|
|
54262
|
-
|
|
54263
|
-
if (sibling) return planSiblingInternalize(plan6, family, bound2, request);
|
|
55669
|
+
if (storedAsSibling2(family, bound2, alias)) return planSiblingInternalize(plan6, family, bound2, request);
|
|
54264
55670
|
const member = memberUnder2(family, bound2, alias, plan6);
|
|
54265
55671
|
if (!member) return plan6;
|
|
54266
55672
|
const consumers = consumersOf(family, bound2, member);
|
|
@@ -54388,12 +55794,19 @@ function internalizeEdits(plan6, family, bound2, member, alias, consumers, desti
|
|
|
54388
55794
|
);
|
|
54389
55795
|
for (const c of repinned) {
|
|
54390
55796
|
edit3(plan6, c.node.namespace, "pin", `pin ${c.alias} taken again against ${boundNode2.id ?? "the top project"}`, { root: c.node.directory, call: "pinExternals", args: [[c.alias]] });
|
|
55797
|
+
loopClosed(plan6, family, bound2, member, c);
|
|
54391
55798
|
}
|
|
54392
55799
|
if (carried.length > 0) edit3(plan6, bound2, "pin", `pin ${carried.join(", ")} (externals carried from ${alias})`, { root: boundNode2.directory, call: "pinExternals", args: [carried] });
|
|
54393
55800
|
plan6.edits.push({ project: member.namespace, kind: "delete", detail: `${alias}'s lock, pins and derived outputs have no home: deleted, each listed` });
|
|
54394
55801
|
plan6.notes.push(`Consumers outside the family that name "${member.id ?? alias}" by path are not found: each must repoint its own external at ${boundNode2.id ?? "this project"}.`);
|
|
54395
55802
|
if (member.id !== void 0 && approvalRecord(member.directory)) plan6.notes.push(`${label6(member.namespace)}'s approval ends with it: its lock is deleted, and ${label6(bound2)} is re-locked instead.`);
|
|
54396
55803
|
}
|
|
55804
|
+
function loopClosed(plan6, family, bound2, member, c) {
|
|
55805
|
+
const uses = family.references.filter((r) => (r.consumer === bound2 || r.consumer === member.namespace) && r.producer === c.node.namespace);
|
|
55806
|
+
if (uses.length === 0) return;
|
|
55807
|
+
const named2 = [...new Set(uses.map((r) => `"${r.authored}"`))].slice(0, 3).join(", ");
|
|
55808
|
+
plan6.notes.push(`${label6(c.node.namespace)} is re-pointed at ${label6(bound2)}, which uses it (${named2}${uses.length > 3 ? ", \u2026" : ""}): the two projects depend on each other in a loop after the internalize \u2014 PROJECT_DEPENDENCY_CYCLE. To keep them apart, move what ${label6(c.node.namespace)} uses of "${member.mountAlias ?? member.namespace}" into ${label6(c.node.namespace)} itself, or demote ${label6(c.node.namespace)} into a part of ${label6(bound2)} first.`);
|
|
55809
|
+
}
|
|
54397
55810
|
function respellConsumer(plan6, family, c, own2) {
|
|
54398
55811
|
const seen = /* @__PURE__ */ new Set();
|
|
54399
55812
|
for (const ref of family.authoredReferences) {
|
|
@@ -54531,6 +55944,11 @@ function plan3(family, bound2, request) {
|
|
|
54531
55944
|
throw new Error(`boundary_migration plans no "${request.verb}": internalize | externalize | promote | demote`);
|
|
54532
55945
|
}
|
|
54533
55946
|
}
|
|
55947
|
+
function storedAsSibling2(family, bound2, alias) {
|
|
55948
|
+
const held = ownGet(configAt5(familyNode(family, bound2).directory)?.members, alias);
|
|
55949
|
+
const location = held === void 0 ? void 0 : memberLocationOf(held);
|
|
55950
|
+
return location !== void 0 && parseMemberSource(location).storage === "path";
|
|
55951
|
+
}
|
|
54534
55952
|
function insideFamily(plan6, dir) {
|
|
54535
55953
|
const rel2 = path47.relative(plan6.familyRoot, dir);
|
|
54536
55954
|
return rel2 !== "" && !rel2.startsWith("..") && !path47.isAbsolute(rel2);
|
|
@@ -54595,7 +56013,6 @@ function planPromote(family, bound2, request) {
|
|
|
54595
56013
|
{ root: node.directory, call: "promoteMember", args: [alias, newId] }
|
|
54596
56014
|
);
|
|
54597
56015
|
edit3(plan6, newId, "pin", `pin the new project's external for ${node.id ?? "this project"} (when its specs reference it)`, { root: part.directory, call: "pinExternals", args: [[]] });
|
|
54598
|
-
if (part.storage !== "contained") edit3(plan6, bound2, "pin", `pin ${alias} (a project stored outside ${label6(bound2)} is judged against its pin)`, { root: node.directory, call: "pinExternals", args: [[alias]] });
|
|
54599
56016
|
repinIntactConsumers2(plan6, family, bound2, [part.directory]);
|
|
54600
56017
|
plan6.notes.push(`"${newId}" has no lock yet: lock it at its own root (\`wairon lock\`) once it validates.`);
|
|
54601
56018
|
return plan6;
|
|
@@ -54617,7 +56034,7 @@ function planDemote(family, bound2, request) {
|
|
|
54617
56034
|
if (referenced?.sourceKind === "git") refuse3(plan6, "read-only", bound2, `the member "${alias}" is fetched from git: demote it in a checkout of its repository`);
|
|
54618
56035
|
if (referenced?.sourceKind === "hosted") refuse3(plan6, "read-only", bound2, `the member "${alias}" is a hosted record: a part lives in its parent's tree`);
|
|
54619
56036
|
const dir = member?.directory ?? referenced?.directory;
|
|
54620
|
-
if (
|
|
56037
|
+
if (dir !== void 0 && (storedAsSibling2(family, bound2, alias) || referenced?.sourceKind === "path")) siblingRefused(plan6, bound2, alias, dir);
|
|
54621
56038
|
if (member) {
|
|
54622
56039
|
const users = /* @__PURE__ */ new Map();
|
|
54623
56040
|
for (const ref of family.references) {
|
|
@@ -54656,7 +56073,7 @@ function write5(plan6, rehearsal) {
|
|
|
54656
56073
|
const [kind, id] = spec.split("\n");
|
|
54657
56074
|
runWithProjectRoot(rehearsalRoot(rehearsal, plan6.familyRoot), () => rewriteReferences2(kind, id, edits));
|
|
54658
56075
|
}
|
|
54659
|
-
for (const e of planned.filter((x) => x.write.call === "internalizeMember")) writeOne3(rehearsal, e.write);
|
|
56076
|
+
for (const e of planned.filter((x) => x.write.call === "internalizeMember")) recordFolded(plan6, e.project, writeOne3(rehearsal, e.write));
|
|
54660
56077
|
for (const e of planned.filter((x) => ["importNames", "unpin", "removeExternal"].includes(x.write.call))) writeOne3(rehearsal, e.write);
|
|
54661
56078
|
for (const e of planned.filter((x) => x.write.call === "repointExternal")) writeOne3(rehearsal, e.write);
|
|
54662
56079
|
for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
|
|
@@ -54675,7 +56092,7 @@ function write5(plan6, rehearsal) {
|
|
|
54675
56092
|
for (const e of planned.filter((x) => x.write.call === "demoteMember")) {
|
|
54676
56093
|
const demoted = writeOne3(rehearsal, e.write);
|
|
54677
56094
|
for (const line of demoted?.consumersRestored ?? []) plan6.edits.push({ project: e.project, kind: "reference", detail: `consumers across the old boundary restored: ${line}` });
|
|
54678
|
-
|
|
56095
|
+
recordFolded(plan6, e.project, demoted);
|
|
54679
56096
|
}
|
|
54680
56097
|
for (const e of planned.filter((x) => x.write.call === "unpin")) writeOne3(rehearsal, e.write);
|
|
54681
56098
|
}
|
|
@@ -54724,6 +56141,7 @@ function recordCrossing(plan6, bound2, member, result) {
|
|
|
54724
56141
|
}
|
|
54725
56142
|
for (const line of crossed.exported) plan6.edits.push({ project: bound2, kind: "export", detail: `export ${line}` });
|
|
54726
56143
|
for (const line of crossed.imported) plan6.edits.push({ project: bound2, kind: "external", detail: `import: ${line}` });
|
|
56144
|
+
for (const line of crossed.externalsDeclared ?? []) plan6.edits.push({ project: member, kind: "external", detail: `externals: ${line} \u2014 a project its moved specs name` });
|
|
54727
56145
|
for (const line of crossed.consumersDropped) plan6.edits.push({ project: bound2, kind: "reference", detail: `consumers across the boundary to ${member} dropped: ${line}` });
|
|
54728
56146
|
for (const line of crossed.pathsRebased ?? []) plan6.edits.push({ project: member, kind: "reference", detail: `path re-expressed under the new root \u2014 ${line}` });
|
|
54729
56147
|
for (const line of crossed.signaturesRederived ?? []) plan6.edits.push({ project: bound2, kind: "reference", detail: `signature text re-derived \u2014 ${line}` });
|
|
@@ -54734,6 +56152,12 @@ function recordCrossing(plan6, bound2, member, result) {
|
|
|
54734
56152
|
for (const line of topics) plan6.notes.push(` ${line}`);
|
|
54735
56153
|
}
|
|
54736
56154
|
}
|
|
56155
|
+
function recordFolded(plan6, project3, result) {
|
|
56156
|
+
const folded = result;
|
|
56157
|
+
if (!folded) return;
|
|
56158
|
+
for (const line of folded.signaturesRederived ?? []) plan6.edits.push({ project: project3, kind: "reference", detail: `signature text re-derived \u2014 ${line}` });
|
|
56159
|
+
for (const line of folded.notCarried) if (!plan6.notes.includes(`not carried: ${line}`)) plan6.notes.push(`not carried: ${line}`);
|
|
56160
|
+
}
|
|
54737
56161
|
function writeOne3(rehearsal, w) {
|
|
54738
56162
|
const args = w.args;
|
|
54739
56163
|
return runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
|
|
@@ -55407,10 +56831,14 @@ function newFindings(plan6, rehearsal, scope, changes) {
|
|
|
55407
56831
|
if (owners.length === 0) return [];
|
|
55408
56832
|
const run3 = (copy) => {
|
|
55409
56833
|
try {
|
|
55410
|
-
|
|
55411
|
-
|
|
55412
|
-
|
|
55413
|
-
|
|
56834
|
+
const shape = familyShape(plan6, copy);
|
|
56835
|
+
return [
|
|
56836
|
+
...owners.filter((owner) => shape.roots.has(path49.resolve(rehearsalRoot(copy, owner)))).flatMap((owner) => runWithProjectRoot(rehearsalRoot(copy, owner), () => {
|
|
56837
|
+
const codeCodes = new Set(listRules2().filter((r) => r.judges === "code").flatMap((r) => r.codes.map((c) => c.code)));
|
|
56838
|
+
return validateFamily({ memberDepth: 0 }).issues.filter((i) => (i.severity === "error" || i.severity === "warning") && !codeCodes.has(i.code)).map((issue2) => ({ owner, issue: issue2 }));
|
|
56839
|
+
})),
|
|
56840
|
+
...shape.loops.map((loop) => ({ owner: `loop ${loop.where}`, issue: loop.issue }))
|
|
56841
|
+
];
|
|
55414
56842
|
} catch {
|
|
55415
56843
|
return null;
|
|
55416
56844
|
}
|
|
@@ -55448,6 +56876,28 @@ function newFindings(plan6, rehearsal, scope, changes) {
|
|
|
55448
56876
|
}
|
|
55449
56877
|
return notes;
|
|
55450
56878
|
}
|
|
56879
|
+
function familyShape(plan6, copy) {
|
|
56880
|
+
const top = rehearsalRoot(copy, plan6.familyRoot);
|
|
56881
|
+
const family = runWithProjectRoot(top, () => graph());
|
|
56882
|
+
const roots = new Set(family.nodes.map((n) => path49.resolve(n.directory)));
|
|
56883
|
+
const label7 = (key2) => {
|
|
56884
|
+
const node = family.nodes.find((n) => n.namespace === key2);
|
|
56885
|
+
return `"${node?.id ?? node?.name ?? key2}"${key2 === "" ? "" : ` (member "${node?.mountAlias ?? key2}")`}`;
|
|
56886
|
+
};
|
|
56887
|
+
const loops = dependencyCycles(family).map((loop) => {
|
|
56888
|
+
const where = [...new Set(loop)].map((k) => posixPath(path49.relative(top, family.nodes.find((n) => n.namespace === k)?.directory ?? top))).sort().join(",");
|
|
56889
|
+
const issue2 = {
|
|
56890
|
+
severity: "warning",
|
|
56891
|
+
code: "PROJECT_DEPENDENCY_CYCLE",
|
|
56892
|
+
message: `Projects ${loop.map(label7).join(" -> ")} depend on each other in a loop, so neither can be approved or released before the other`
|
|
56893
|
+
};
|
|
56894
|
+
return { where, issue: issue2 };
|
|
56895
|
+
});
|
|
56896
|
+
return { roots, loops };
|
|
56897
|
+
}
|
|
56898
|
+
function posixPath(p) {
|
|
56899
|
+
return p.split(path49.sep).join("/");
|
|
56900
|
+
}
|
|
55451
56901
|
function movedOf(plan6) {
|
|
55452
56902
|
const relocation = plan6.request.relocation;
|
|
55453
56903
|
const move2 = plan6.edits.find((e) => e.write?.call === "move");
|
|
@@ -55511,9 +56961,7 @@ function partDirectories(plan6, family) {
|
|
|
55511
56961
|
return rel2 !== "" && !rel2.startsWith("..") && !path49.isAbsolute(rel2);
|
|
55512
56962
|
};
|
|
55513
56963
|
const subject = plan6.request.alias;
|
|
55514
|
-
|
|
55515
|
-
const siblings = family.nodes.flatMap((n) => n.externals.filter((e) => e.role === "member" && e.sourceKind === "path" && e.directory !== void 0 && e.alias === subject).map((e) => e.directory));
|
|
55516
|
-
return [...parts, ...siblings];
|
|
56964
|
+
return family.nodes.flatMap((n) => n.parts.filter((p) => p.storage !== "git" && p.directory !== void 0 && (inside(p.directory) || p.alias === subject)).map((p) => p.directory));
|
|
55517
56965
|
}
|
|
55518
56966
|
function broughtIn(plan6, family) {
|
|
55519
56967
|
const known = new Set(family.nodes.map((n) => path49.resolve(n.directory)));
|
|
@@ -55673,11 +57121,11 @@ function statusFamilyContext() {
|
|
|
55673
57121
|
const root = getProjectRoot();
|
|
55674
57122
|
const members = listDirectChainedSubprojects(root);
|
|
55675
57123
|
if (members.length > 0) {
|
|
55676
|
-
const
|
|
57124
|
+
const described2 = members.map((m) => {
|
|
55677
57125
|
const rel2 = path51.relative(root, m.dir).split(path51.sep).join("/") || ".";
|
|
55678
57126
|
return `${m.alias} (${rel2}${m.form === "mount" ? ", legacy L1 mount" : ""})`;
|
|
55679
57127
|
});
|
|
55680
|
-
lines.push(`Family: member projects declared here \u2014 ${
|
|
57128
|
+
lines.push(`Family: member projects declared here \u2014 ${described2.join(", ")}.`);
|
|
55681
57129
|
}
|
|
55682
57130
|
} catch {
|
|
55683
57131
|
}
|
|
@@ -55959,9 +57407,24 @@ function renderChangeReport(report3) {
|
|
|
55959
57407
|
lines.push("", `DROPPED FROM THE STORED FILE (${report3.dryRun ? "would be " : ""}removed \u2014 the schema does not know them):`, ...report3.strippedKeys.map((k) => `- ${k}`));
|
|
55960
57408
|
}
|
|
55961
57409
|
lines.push(...renderRespellings(report3.respellings));
|
|
57410
|
+
lines.push(...renderBreaks(report3.published ?? [], report3.breaks ?? [], report3.dryRun));
|
|
55962
57411
|
lines.push(...renderTestsToRevisit(report3.testsToRevisit));
|
|
55963
57412
|
return lines.join("\n");
|
|
55964
57413
|
}
|
|
57414
|
+
function renderBreaks(published, breaks, dryRun) {
|
|
57415
|
+
if (published.length === 0) return [];
|
|
57416
|
+
const lines = ["", `PUBLISHED as: ${published.map((u) => `${u.publicName}${u.members.length ? ` (${u.members.join(", ")})` : ""}`).join("; ")} \u2014 a consumer of these ${dryRun ? "would lose" : "lost"} what goes.`];
|
|
57417
|
+
if (breaks.length === 0) {
|
|
57418
|
+
lines.push("No consumer in reach uses it (the family, and the searched folders when `search` names any).");
|
|
57419
|
+
return lines;
|
|
57420
|
+
}
|
|
57421
|
+
lines.push(`BREAKS ${breaks.length} consumer(s), whose specs still reach it:`);
|
|
57422
|
+
for (const c of breaks) {
|
|
57423
|
+
const uses = (c.uses ?? []).map((u) => `${u.publicName}${u.members.length ? `.${u.members.join("/")}` : ""}${u.specs?.length ? ` in ${u.specs.join(", ")}` : ""}`);
|
|
57424
|
+
lines.push(`- ${c.project} (${c.section}.${c.alias})${c.found === "search" ? ` [${c.directory}]` : ""}${uses.length ? `: ${uses.join("; ")}` : ""}`);
|
|
57425
|
+
}
|
|
57426
|
+
return lines;
|
|
57427
|
+
}
|
|
55965
57428
|
function renderDeletion(deletion) {
|
|
55966
57429
|
const others = deletion.removed.filter((r) => !(r.kind === deletion.kind && r.id === deletion.id));
|
|
55967
57430
|
const lines = [deletion.dryRun ? `DRY RUN \u2014 nothing was deleted. Deleting ${deletion.kind} "${deletion.id}" would remove ${deletion.removed.length} spec${deletion.removed.length === 1 ? "" : "s"}.` : `Successfully deleted ${deletion.kind} spec "${deletion.id}"${others.length ? ` and ${others.length} spec${others.length === 1 ? "" : "s"} it contained` : ""}.`];
|
|
@@ -55970,6 +57433,7 @@ function renderDeletion(deletion) {
|
|
|
55970
57433
|
lines.push("", deletion.dryRun ? "STILL REFERENCED \u2014 the deletion would be refused unless forced (force: true leaves these dangling):" : "LEFT DANGLING (forced) \u2014 validate reports each:");
|
|
55971
57434
|
lines.push(...deletion.references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`));
|
|
55972
57435
|
}
|
|
57436
|
+
lines.push(...renderBreaks(deletion.published, deletion.breaks ?? [], deletion.dryRun));
|
|
55973
57437
|
lines.push(...renderTestsToRevisit(deletion.testsToRevisit));
|
|
55974
57438
|
return lines.join("\n");
|
|
55975
57439
|
}
|
|
@@ -56020,6 +57484,25 @@ function renderRespellings(entries) {
|
|
|
56020
57484
|
if (!entries.length) return [];
|
|
56021
57485
|
return ["", "RESPELLED (stored canonical):", ...entries.map((r) => `- ${r.path}: "${r.written}" \u2192 "${r.stored}"`)];
|
|
56022
57486
|
}
|
|
57487
|
+
var exportUseOutput = import_zod13.z.object({
|
|
57488
|
+
publicName: import_zod13.z.string(),
|
|
57489
|
+
kind: import_zod13.z.string(),
|
|
57490
|
+
members: import_zod13.z.array(import_zod13.z.string()).describe("The contract methods (or `type`) a consumer reaches on it that the removal takes away."),
|
|
57491
|
+
specs: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
57492
|
+
formerNames: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
57493
|
+
formerMembers: import_zod13.z.record(import_zod13.z.array(import_zod13.z.string())).optional()
|
|
57494
|
+
});
|
|
57495
|
+
var breakingConsumerOutput = import_zod13.z.object({
|
|
57496
|
+
project: import_zod13.z.string(),
|
|
57497
|
+
key: import_zod13.z.string(),
|
|
57498
|
+
directory: import_zod13.z.string(),
|
|
57499
|
+
alias: import_zod13.z.string(),
|
|
57500
|
+
section: import_zod13.z.enum(["externals", "members"]),
|
|
57501
|
+
names: import_zod13.z.array(import_zod13.z.string()),
|
|
57502
|
+
broken: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
57503
|
+
found: import_zod13.z.enum(["search"]).optional(),
|
|
57504
|
+
uses: import_zod13.z.array(exportUseOutput).optional()
|
|
57505
|
+
});
|
|
56023
57506
|
var specWriteReceiptOutput = {
|
|
56024
57507
|
kind: import_zod13.z.enum(SPEC_KINDS).describe("The spec kind written."),
|
|
56025
57508
|
id: import_zod13.z.string().describe('The id the spec is stored under; "system" for the L0 singleton.'),
|
|
@@ -56037,6 +57520,10 @@ var specWriteReceiptOutput = {
|
|
|
56037
57520
|
"Each type position the write normalised: what the input wrote against the canonical spelling stored. Aliases are accepted, never refused, and this list is how an author learns the canonical form. Empty when every position was already canonical."
|
|
56038
57521
|
),
|
|
56039
57522
|
testsToRevisit: testsToRevisitOutput,
|
|
57523
|
+
dryRun: import_zod13.z.boolean().optional().describe("Present, true, on a dry run: every judgement ran and nothing was written."),
|
|
57524
|
+
cascaded: import_zod13.z.array(import_zod13.z.string()).optional().describe(
|
|
57525
|
+
"Each implementation method entry a contract restatement removed with a method it dropped, as `<implementation>.<method>`."
|
|
57526
|
+
),
|
|
56040
57527
|
...staleServerOutput
|
|
56041
57528
|
};
|
|
56042
57529
|
function writeReceipt(sentence, receipt) {
|
|
@@ -56058,6 +57545,12 @@ var specDeletionOutput = {
|
|
|
56058
57545
|
removed: import_zod13.z.array(import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string() })).describe("Every spec the deletion removes (or would), innermost first and the addressed spec last."),
|
|
56059
57546
|
references: import_zod13.z.array(import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string(), position: import_zod13.z.string(), target: import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string() }) })).describe("Every reference a spec outside the removed set holds to it: why a deletion is refused, or what a forced one left dangling."),
|
|
56060
57547
|
testsToRevisit: testsToRevisitOutput,
|
|
57548
|
+
published: import_zod13.z.array(exportUseOutput).describe(
|
|
57549
|
+
"Every public name of this project a consumer reaches what the deletion removes through, read before anything goes. Empty when nothing removed is published."
|
|
57550
|
+
),
|
|
57551
|
+
breaks: import_zod13.z.array(breakingConsumerOutput).optional().describe(
|
|
57552
|
+
"The consumers in reach (family members included, and checkouts in the searched folders) whose specs reach a published name on a member the deletion takes \u2014 the projects it breaks. Absent when none breaks."
|
|
57553
|
+
),
|
|
56061
57554
|
...staleServerOutput
|
|
56062
57555
|
};
|
|
56063
57556
|
var specChangeOutput = {
|
|
@@ -56090,6 +57583,15 @@ var specChangeReportOutput = {
|
|
|
56090
57583
|
strippedKeys: import_zod13.z.array(import_zod13.z.string()).optional().describe(
|
|
56091
57584
|
"Keys the stored file carries that the level's schema does not know, each as `path: value`, which this write drops (or, on a dry run, would drop). Absent when there are none."
|
|
56092
57585
|
),
|
|
57586
|
+
cascaded: import_zod13.z.array(import_zod13.z.string()).optional().describe(
|
|
57587
|
+
"Each implementation method entry removed with a contract method the delta removed, as `<implementation>.<method>` (on a dry run, would be)."
|
|
57588
|
+
),
|
|
57589
|
+
published: import_zod13.z.array(exportUseOutput).optional().describe(
|
|
57590
|
+
"Every public name a consumer reaches a removed contract method through. Absent when no removed method is published."
|
|
57591
|
+
),
|
|
57592
|
+
breaks: import_zod13.z.array(breakingConsumerOutput).optional().describe(
|
|
57593
|
+
"The consumers in reach whose specs call a removed method on a published name \u2014 the projects the removal breaks."
|
|
57594
|
+
),
|
|
56093
57595
|
...staleServerOutput
|
|
56094
57596
|
};
|
|
56095
57597
|
function statusNetworkLine() {
|
|
@@ -56454,6 +57956,8 @@ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
56454
57956
|
"sdd_rename_type",
|
|
56455
57957
|
"sdd_rename_field",
|
|
56456
57958
|
"sdd_rename_param",
|
|
57959
|
+
"sdd_rename_spec",
|
|
57960
|
+
"sdd_move_spec",
|
|
56457
57961
|
"sdd_add_component",
|
|
56458
57962
|
"sdd_define_interface",
|
|
56459
57963
|
"sdd_set_endpoints",
|
|
@@ -56464,6 +57968,11 @@ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
56464
57968
|
"sdd_move_methods"
|
|
56465
57969
|
]);
|
|
56466
57970
|
var listChangedEmitters = /* @__PURE__ */ new WeakMap();
|
|
57971
|
+
function withArgumentNames(description, inputSchema) {
|
|
57972
|
+
const names = Object.entries(inputSchema).map(([name, schema]) => schema.isOptional() ? `${name}?` : name);
|
|
57973
|
+
if (names.length === 0) return description;
|
|
57974
|
+
return `${description.trimEnd()} Arguments: ${names.join(", ")}.`;
|
|
57975
|
+
}
|
|
56467
57976
|
function reg(server, name, config, cb) {
|
|
56468
57977
|
const guarded2 = (args) => {
|
|
56469
57978
|
const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
|
|
@@ -56475,7 +57984,7 @@ function reg(server, name, config, cb) {
|
|
|
56475
57984
|
if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
|
|
56476
57985
|
return result;
|
|
56477
57986
|
};
|
|
56478
|
-
const strictConfig = config.inputSchema ? { ...config, inputSchema: import_zod13.z.object(config.inputSchema).strict() } : config;
|
|
57987
|
+
const strictConfig = config.inputSchema ? { ...config, description: withArgumentNames(config.description, config.inputSchema), inputSchema: import_zod13.z.object(config.inputSchema).strict() } : config;
|
|
56479
57988
|
server.registerTool(name, strictConfig, guarded2);
|
|
56480
57989
|
}
|
|
56481
57990
|
var statusInput = import_zod13.z.enum(STATUS_VALUES).optional().describe(
|
|
@@ -56933,7 +58442,7 @@ ${renderChangeReport(report3)}`,
|
|
|
56933
58442
|
server,
|
|
56934
58443
|
"sdd_adopt_member",
|
|
56935
58444
|
{
|
|
56936
|
-
description: "Make the bound project's external a member \u2014 sdd_detach_member's inverse. Locally the external is found by a path inside the bound project and adopted where it is; on a hosted instance it names a project by source.hosted and `path` says where to adopt it (the hosting server relocates it there). All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
|
|
58445
|
+
description: "Make the bound project's external a member \u2014 sdd_detach_member's inverse. Locally the external is found by a path inside the bound project or a `../` sibling checkout, and adopted where it is; on a hosted instance it names a project by source.hosted and `path` says where to adopt it (the hosting server relocates it there). All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
|
|
56937
58446
|
inputSchema: {
|
|
56938
58447
|
alias: import_zod13.z.string().describe("The external's alias"),
|
|
56939
58448
|
path: import_zod13.z.string().optional().describe("Hosted: the member path to adopt a source.hosted external at, relative to the bound project; a source.path external is adopted where it is"),
|
|
@@ -56946,7 +58455,7 @@ ${renderChangeReport(report3)}`,
|
|
|
56946
58455
|
server,
|
|
56947
58456
|
"sdd_rename_component",
|
|
56948
58457
|
{
|
|
56949
|
-
description: "Rename a component and rewrite every reference to it in the bound tree: dependsOn, owns and dispatch entries, lifecycle entrypoints, published interfaces at L1 and L0, narrative call/dispatch/register targets, an interface's component, an implementation's contract and an entity's componentClass. The interface named i<id> and the implementation named <id>_impl move to the new ids and files; interfaces and implementations named otherwise keep their ids. Findings keyed on the component follow it too: every lint allow naming it on an edge site or in a covered unit, and every entry of the debt register (.wai/project.yaml rules.conformance.carried) anchored on a renamed spec or naming the component in its site or units \u2014 rewritten in place, every comment and all other formatting of the register kept, so unchanged debt never reads as paid and new at once. A register that cannot be rewritten that precisely is refused before the first write. Display names are left as they are. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename it from that project's own root), a new id that is not a lowercase identifier without a namespace separator (invalid-id), and a new id \u2014 or i<newId> or <newId>_impl \u2014 already in use (id-taken). With dryRun it answers what it would move, rewrite, keep and rekey, and writes nothing. Returns the specs moved, each with its old and new id, the ids of the other specs rewritten (lint allows included), and `carried`: each register edit \u2014 the entry by code, spec and site as it stood, the key changed (spec, at or covers), from and to.",
|
|
58458
|
+
description: "Rename a component and rewrite every reference to it in the bound tree: dependsOn, owns and dispatch entries, lifecycle entrypoints, published interfaces at L1 and L0, narrative call/dispatch/register targets, an interface's component, an implementation's contract and an entity's componentClass. The interface named i<id> and the implementation named <id>_impl move to the new ids and files; interfaces and implementations named otherwise keep their ids, and their files follow the component's folder (rename one of those on its own with sdd_rename_spec). Findings keyed on the component follow it too: every lint allow naming it on an edge site or in a covered unit, and every entry of the debt register (.wai/project.yaml rules.conformance.carried) anchored on a renamed spec or naming the component in its site or units \u2014 rewritten in place, every comment and all other formatting of the register kept, so unchanged debt never reads as paid and new at once. A register that cannot be rewritten that precisely is refused before the first write. Display names are left as they are. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename it from that project's own root), a new id that is not a lowercase identifier without a namespace separator (invalid-id), and a new id \u2014 or i<newId> or <newId>_impl \u2014 already in use (id-taken). With dryRun it answers what it would move, rewrite, keep and rekey, and writes nothing. Returns the specs moved, each with its old and new id, the ids of the other specs rewritten (lint allows included), and `carried`: each register edit \u2014 the entry by code, spec and site as it stood, the key changed (spec, at or covers), from and to.",
|
|
56950
58459
|
inputSchema: {
|
|
56951
58460
|
id: import_zod13.z.string().describe("The component to rename (namespaced if needed)"),
|
|
56952
58461
|
newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator"),
|
|
@@ -56980,7 +58489,8 @@ ${renderChangeReport(report3)}`,
|
|
|
56980
58489
|
const root = getProjectRoot();
|
|
56981
58490
|
const consumers = listConsumers3(search?.map((d) => path51.resolve(root, d)));
|
|
56982
58491
|
const report3 = renameMethod(qualifiedComponentId(id), method2, newName, pinSymbol, dryRun);
|
|
56983
|
-
const
|
|
58492
|
+
const published = report3.publishedIn.map((publicName) => ({ publicName, kind: "component", members: [method2] }));
|
|
58493
|
+
const breaks = consumers.map((c) => narrowedToUses(c, published)).filter((c) => c !== null);
|
|
56984
58494
|
return json({ ...report3, ...breaks.length > 0 ? { breaks } : {} });
|
|
56985
58495
|
} catch (e) {
|
|
56986
58496
|
return errText(String(e));
|
|
@@ -56991,18 +58501,22 @@ ${renderChangeReport(report3)}`,
|
|
|
56991
58501
|
server,
|
|
56992
58502
|
"sdd_rename_type",
|
|
56993
58503
|
{
|
|
56994
|
-
description: "Rename a type and rewrite every reference to it in the bound tree: every type position naming it (contract and type-method params and returns, fields, signature-type params and returns), a signatureFrom naming it as a signature type, and an L1 or L0 export entry's typeDef. Only the named members of a type expression that name this type are respelled, in the style they were written (`Invoice` \u2192 `Bill`, `invoice` \u2192 `bill`); the rest of the text stays as written. The type moves to the new id and file under the same owner, and its old id joins its rename trace (previousIds) so a consumer of the design export reads the rename as a rename; an export entry that derived its public name from the old id gets `as: <that name>` first, so the published name and every consumer's pin stay. Rename traces are never rewritten. Findings keyed on the type follow it: lint allows naming it, and every debt-register entry anchored on it, rewritten in place (a register that cannot be rewritten that precisely is refused before the first write). The display name is left as it is. Refuses, writing nothing: a type that does not exist (type-missing), one in another project (chained-type), a new id that is not a lowercase identifier (invalid-id), and a new id another type of the same owner holds (id-taken) or retired (id-retired \u2014 unsetting the holder's previousIds releases it). With dryRun it answers what it would rewrite, keep and rekey, and writes nothing. Returns from and to, the specs rewritten, keptPublicNames
|
|
58504
|
+
description: "Rename a type and rewrite every reference to it in the bound tree: every type position naming it (contract and type-method params and returns, fields, signature-type params and returns), a signatureFrom naming it as a signature type, and an L1 or L0 export entry's typeDef. Only the named members of a type expression that name this type are respelled, in the style they were written (`Invoice` \u2192 `Bill`, `invoice` \u2192 `bill`); the rest of the text stays as written. The type moves to the new id and file under the same owner, and its old id joins its rename trace (previousIds) so a consumer of the design export reads the rename as a rename; an export entry that derived its public name from the old id gets `as: <that name>` first, so the published name and every consumer's pin stay. Rename traces are never rewritten. Findings keyed on the type follow it: lint allows naming it, and every debt-register entry anchored on it, rewritten in place (a register that cannot be rewritten that precisely is refused before the first write). The display name is left as it is. Refuses, writing nothing: a type that does not exist (type-missing), one in another project (chained-type), a new id that is not a lowercase identifier (invalid-id), and a new id another type of the same owner holds (id-taken) or retired (id-retired \u2014 unsetting the holder's previousIds releases it). With dryRun it answers what it would rewrite, keep and rekey, and writes nothing. Returns from and to, the specs rewritten, keptPublicNames, `carried`: each register edit, `publishedIn` \u2014 every public name a consumer reaches the type through (an exported type that is or embeds it, member `type`; an exported contract whose methods name one in their signature closure, those methods) \u2014 and `breaks`: the consumers in reach (family members included, and checkouts in the `search` folders) whose specs reach one of those, a contract they depend on as a whole included, narrowed to what carries the type \u2014 as sdd_rename_field and sdd_rename_param name theirs.",
|
|
56995
58505
|
inputSchema: {
|
|
56996
58506
|
id: import_zod13.z.string().describe("The type to rename (qualified by its owning subsystem if needed, `subsystem::id`)"),
|
|
56997
58507
|
newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator"),
|
|
56998
|
-
dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing")
|
|
58508
|
+
dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing"),
|
|
58509
|
+
search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family (e.g. the folder holding sibling checkouts), relative to the bound project root or absolute")
|
|
56999
58510
|
}
|
|
57000
58511
|
},
|
|
57001
|
-
({ id, newId, dryRun }) => {
|
|
58512
|
+
({ id, newId, dryRun, search }) => {
|
|
57002
58513
|
try {
|
|
57003
58514
|
const bare2 = id.startsWith("::") ? id.slice(2) : id;
|
|
57004
58515
|
const resolved = loadTypeSpec(bare2) ? bare2 : id;
|
|
57005
|
-
|
|
58516
|
+
const consumers = listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d)));
|
|
58517
|
+
const report3 = renameType(resolved, newId, dryRun);
|
|
58518
|
+
const breaks = consumers.map((c) => narrowedToUses(c, report3.publishedIn)).filter((c) => c !== null);
|
|
58519
|
+
return json({ ...report3, ...breaks.length > 0 ? { breaks } : {} });
|
|
57006
58520
|
} catch (e) {
|
|
57007
58521
|
return errText(String(e));
|
|
57008
58522
|
}
|
|
@@ -57083,6 +58597,46 @@ ${renderChangeReport(report3)}`,
|
|
|
57083
58597
|
}
|
|
57084
58598
|
}
|
|
57085
58599
|
);
|
|
58600
|
+
reg(
|
|
58601
|
+
server,
|
|
58602
|
+
"sdd_rename_spec",
|
|
58603
|
+
{
|
|
58604
|
+
description: "Rename ONE contract (kind interface) or ONE implementation on its own \u2014 the ids sdd_rename_component keeps when they are not derived from the component's (i<id>, <id>_impl) \u2014 and rewrite every reference to it in the bound tree: every implementation's contract and every L1 and L0 export entry's interface (an entry whose public name derived from the old id gets `as: <that name>` first, so the published surface and every consumer's pin stay), lint allows and debt-register entries keyed on it. The spec moves to its new id and file beside its owner, its old id in its rename trace (previousIds). Refuses, writing nothing: another kind (invalid-kind, naming the tool that renames it), a spec that does not exist (spec-missing) or lives in another project (chained-spec), a new id outside the grammar \u2014 a contract id begins with i \u2014 (invalid-id), and one taken or retired (id-taken, id-retired). With dryRun it answers what it would move, rewrite and keep, and writes nothing.",
|
|
58605
|
+
inputSchema: {
|
|
58606
|
+
kind: import_zod13.z.enum(["interface", "implementation"]).describe("interface (a contract) or implementation"),
|
|
58607
|
+
id: import_zod13.z.string().describe("The spec to rename"),
|
|
58608
|
+
newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator (a contract's begins with i)"),
|
|
58609
|
+
dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing")
|
|
58610
|
+
}
|
|
58611
|
+
},
|
|
58612
|
+
({ kind, id, newId, dryRun }) => {
|
|
58613
|
+
try {
|
|
58614
|
+
return json(renameSpecId(kind, id, newId, dryRun));
|
|
58615
|
+
} catch (e) {
|
|
58616
|
+
return errText(String(e));
|
|
58617
|
+
}
|
|
58618
|
+
}
|
|
58619
|
+
);
|
|
58620
|
+
reg(
|
|
58621
|
+
server,
|
|
58622
|
+
"sdd_move_spec",
|
|
58623
|
+
{
|
|
58624
|
+
description: "Move a component or a type to another subsystem of the bound project with everything that follows it. A component takes the members it owns, its contracts and their implementations (their files move to the new subsystem's folder in the nested layout); ids never change, so references by id stay, and what names the OLD subsystem follows: its export entries and lifecycle entrypoints for the moved components, and an L0 re-export from it. A type takes its file, its L1 export entries and every reference qualified by the old subsystem (`old::type` becomes `new::type`); omit subsystem to make a type system-level. Gated like every write: a move that would create a forbidden doctrine edge, or a cross-subsystem edge into a non-Portal, is refused with the rule's own words, and a curable boundary finding (a direct cross-subsystem edge into a Portal, which a client Adapter or a trustedLink would license) is named among the notices. Refuses, writing nothing: a kind other than component or type, a spec that does not exist or lives in another project, a subsystem the bound tree does not have or the one it already lives in, a component a pattern owns (move its owner), a type id the target already holds. With dryRun it answers the plan and the notices, and writes nothing.",
|
|
58625
|
+
inputSchema: {
|
|
58626
|
+
kind: import_zod13.z.enum(["component", "type"]).describe("component or type"),
|
|
58627
|
+
id: import_zod13.z.string().describe("The spec to move (a type by its bare id, or as <subsystem>::<id>)"),
|
|
58628
|
+
subsystem: import_zod13.z.string().optional().describe("The subsystem it moves to; omitted for a type made system-level"),
|
|
58629
|
+
dryRun: import_zod13.z.boolean().optional().describe("Answer what the move would do \u2014 refused exactly as the move would be \u2014 and write nothing")
|
|
58630
|
+
}
|
|
58631
|
+
},
|
|
58632
|
+
({ kind, id, subsystem, dryRun }) => {
|
|
58633
|
+
try {
|
|
58634
|
+
return json(moveSpec2(kind, id, subsystem, dryRun));
|
|
58635
|
+
} catch (e) {
|
|
58636
|
+
return errText(String(e));
|
|
58637
|
+
}
|
|
58638
|
+
}
|
|
58639
|
+
);
|
|
57086
58640
|
const componentInput = {
|
|
57087
58641
|
id: import_zod13.z.string().describe("Lowercase identifier for the component"),
|
|
57088
58642
|
name: import_zod13.z.string().describe("Human-readable display name"),
|
|
@@ -57254,7 +58808,7 @@ ${renderChangeReport(report3)}`,
|
|
|
57254
58808
|
server,
|
|
57255
58809
|
"sdd_set_endpoints",
|
|
57256
58810
|
{
|
|
57257
|
-
description: "Bind concrete wire endpoints to existing L3 interface methods, for a Portal whose transport requires them (every transport but InProcess and Custom). Pick the Portal's own `transport` and fill that transport's address fields. A binding for an InProcess Portal is refused: its verbs are its contract methods. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
|
|
58811
|
+
description: "Bind concrete wire endpoints to existing L3 interface methods, for a Portal whose transport requires them (every transport but InProcess and Custom). Pick the Portal's own `transport` and fill that transport's address fields \u2014 for HTTP the verb, the path and, optionally, the success `status` (a 2xx or 3xx code: 202 Accepted, a redirect, a 201 for a workflow verb) the operation answers with, which the OpenAPI export uses in place of its convention. A binding for an InProcess Portal is refused: its verbs are its contract methods. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
|
|
57258
58812
|
inputSchema: {
|
|
57259
58813
|
interface: import_zod13.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
|
|
57260
58814
|
endpoints: import_zod13.z.array(import_zod13.z.object({
|
|
@@ -57262,6 +58816,7 @@ ${renderChangeReport(report3)}`,
|
|
|
57262
58816
|
transport: import_zod13.z.enum(["HTTP", "HTTP_API", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "JSONRPC", "Custom"]).describe("Wire protocol; must be the Portal's own transport. The retired spelling HTTP_API is still read as HTTP for one release; any other transport is refused, naming the Portal's"),
|
|
57263
58817
|
httpMethod: import_zod13.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
|
|
57264
58818
|
path: import_zod13.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
|
|
58819
|
+
status: import_zod13.z.number().int().min(200).max(399).optional().describe("HTTP: the success status the operation answers with when it is not the verb's convention \u2014 202 Accepted, a 3xx redirect, a 201 for a workflow verb that files something. The OpenAPI export uses it; omitted, the export answers 204 for a method returning nothing, 201 for a POST that creates, else 200"),
|
|
57265
58820
|
service: import_zod13.z.string().optional().describe("gRPC: service name"),
|
|
57266
58821
|
rpcMethod: import_zod13.z.string().optional().describe("gRPC: rpc method name"),
|
|
57267
58822
|
operation: import_zod13.z.enum(["query", "mutation", "subscription"]).optional().describe("GraphQL: operation kind"),
|
|
@@ -57275,11 +58830,12 @@ ${renderChangeReport(report3)}`,
|
|
|
57275
58830
|
command: import_zod13.z.string().optional().describe("CLI: command/subcommand"),
|
|
57276
58831
|
jsonRpcMethod: import_zod13.z.string().optional().describe("JSONRPC: the JSON-RPC method name"),
|
|
57277
58832
|
address: import_zod13.z.string().optional().describe("Custom: free-form address")
|
|
57278
|
-
}).strict()).describe("One binding per method")
|
|
58833
|
+
}).strict()).describe("One binding per method"),
|
|
58834
|
+
dryRun: import_zod13.z.boolean().optional().describe("Answer the change report the binding would give \u2014 the gate (a route another method binds is refused) and the writer's parse included \u2014 and write nothing")
|
|
57279
58835
|
},
|
|
57280
58836
|
outputSchema: specChangeReportOutput
|
|
57281
58837
|
},
|
|
57282
|
-
({ interface: interfaceId, endpoints }) => {
|
|
58838
|
+
({ interface: interfaceId, endpoints, dryRun }) => {
|
|
57283
58839
|
try {
|
|
57284
58840
|
const intf = loadInterfaceSpec(interfaceId);
|
|
57285
58841
|
if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
|
|
@@ -57291,7 +58847,7 @@ ${renderChangeReport(report3)}`,
|
|
|
57291
58847
|
const buildRaw = (e, transport) => {
|
|
57292
58848
|
switch (transport) {
|
|
57293
58849
|
case "HTTP":
|
|
57294
|
-
return { transport: "HTTP", method: e.httpMethod, path: e.path };
|
|
58850
|
+
return { transport: "HTTP", method: e.httpMethod, path: e.path, ...e.status !== void 0 ? { status: e.status } : {} };
|
|
57295
58851
|
case "gRPC":
|
|
57296
58852
|
return { transport: "gRPC", service: e.service, method: e.rpcMethod };
|
|
57297
58853
|
case "GraphQL":
|
|
@@ -57329,9 +58885,9 @@ ${renderChangeReport(report3)}`,
|
|
|
57329
58885
|
methods.push({ name: e.method, endpoint: parsed.data });
|
|
57330
58886
|
bound2.push(`${e.method}\u2192${transport}`);
|
|
57331
58887
|
}
|
|
57332
|
-
const report3 = updateSpecGated("interface", interfaceId, { methods });
|
|
58888
|
+
const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun);
|
|
57333
58889
|
return structured(
|
|
57334
|
-
|
|
58890
|
+
`${dryRun ? "Would bind" : "Bound"} ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
|
|
57335
58891
|
${renderChangeReport(report3)}`,
|
|
57336
58892
|
report3
|
|
57337
58893
|
);
|
|
@@ -57401,7 +58957,7 @@ ${renderChangeReport(report3)}`,
|
|
|
57401
58957
|
bindings: import_zod13.z.array(import_zod13.z.string()).optional().describe("Optional code linkage: the hand-written binding modules (project-relative; N:1 sharing allowed) through which this realization reaches another project's code \u2014 a typed binding to a native library, a client stub. Outside the approval, like sourcePath. Validate compares each with the pinned snapshots of the externals this component reaches (function and method names, parameter names and arity, type field names) and reports a name the pin renamed or no longer exports (BINDING_DRIFT)"),
|
|
57402
58958
|
router: import_zod13.z.string().min(1).optional().describe("Portal-only code linkage: the router entry this Portal's own file exports, through which whatever process serves the Portal hands it its requests (replaces the retired listener mount's `via`). Route coverage reads the routes out of it and export conformance holds the file to it. Outside the approval, like sourcePath"),
|
|
57403
58959
|
technologies: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.object({ name: import_zod13.z.string(), matches: import_zod13.z.array(import_zod13.z.string()).min(1).describe("The tokens the leakage and contract checks match INSTEAD of the name \u2014 for a technology whose name is also an ordinary word of the tree (the yaml package, the YAML format)") }).strict()])).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"], or [{ "name": "yaml", "matches": ["yaml package", "parseDocument"] }] when the bare name would match a common word) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components, and an Observer bound to a messaging technology.`),
|
|
57404
|
-
injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM).
|
|
58960
|
+
injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). A name matches with or without a leading `_` (`ctx` names `_ctx`). A LEADING run of these names is dropped, and so is a TRAILING run of names the contract does not declare (a handler's `res` after the contract's own parameters); one appearing in the middle of the contract's parameters is an argument of the caller's list, not wiring"),
|
|
57405
58961
|
detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
|
|
57406
58962
|
conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
|
|
57407
58963
|
methods: import_zod13.z.array(import_zod13.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
|
|
@@ -57412,7 +58968,7 @@ ${renderChangeReport(report3)}`,
|
|
|
57412
58968
|
server,
|
|
57413
58969
|
"sdd_write_narrative",
|
|
57414
58970
|
{
|
|
57415
|
-
description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 are wiring, and are declared once for the spec in injectedParams; a leading parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
|
|
58971
|
+
description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 or after them (a handler's response handle), are wiring, and are declared once for the spec in injectedParams (matched with or without a leading `_`); a leading or trailing parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
|
|
57416
58972
|
inputSchema: implInput,
|
|
57417
58973
|
outputSchema: specWriteReceiptOutput
|
|
57418
58974
|
},
|
|
@@ -57500,10 +59056,10 @@ ${renderChangeReport(report3)}`,
|
|
|
57500
59056
|
"sdd_add_type",
|
|
57501
59057
|
{
|
|
57502
59058
|
description: "Define a type: an entity or value-object (the data components operate on), a signature \u2014 a named function type \u2014 or an enum: a closed, ordered set of named `values` ({name, description?}, unique by name ignoring case and separators; the name is also the value as data carries it), with optional pure methods and nothing else (no fields, params or returns). A value-object may instead be a NAMED SCALAR: it `holds` one primitive in place of fields (a newtype or type alias in every language). Every type position (a field's type, a method's params and returns, a signature's params and returns, a named scalar's holds) is written in the neutral type grammar; aliases are respelled and listed in the receipt. Entities are owned by a subsystem; shared value objects and signatures omit subsystem (system-level). Fields are data; methods are PURE intrinsic behaviour only \u2014 anything needing a collaborator belongs on a component, taking the entity as an argument \u2014 and a type method may carry structured params, which then derive its signature text as a contract method's do. A signature carries top-level params and returns and nothing else (no fields, methods or invariants \u2014 SIGNATURE_TYPE_MEMBERS); a contract method takes it by naming it as its signatureFrom, and a param may be typed by one (a callback). A type may also CLAIM code: sourcePath names the file holding its declaration (and each method may name its own), symbol binds the code-level name when it differs \u2014 the file must then resolve and the declaration must be anchored in it. An owning subsystem the tree does not have is refused, writing nothing, exactly as a component under an unknown subsystem is. Re-defining an existing id REPLACES fields/methods/invariants and restates sourcePath/symbol (an omitted list or path is CLEARED, and a dropped member is reported); lint/ext are carried forward. The answer carries a write receipt as structured content beside the sentence \u2014 whether a spec already held the id, and the notices a restatement raised, each as its own entry. A type carries no lifecycle status, so the receipt states none.",
|
|
57503
|
-
inputSchema: typeInput,
|
|
59059
|
+
inputSchema: { ...typeInput, dryRun: import_zod13.z.boolean().optional().describe("Answer the receipt the write would give \u2014 every judgement run, a refusal exactly as the write would give it \u2014 and write nothing") },
|
|
57504
59060
|
outputSchema: specWriteReceiptOutput
|
|
57505
59061
|
},
|
|
57506
|
-
({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol, params, returns, values, holds }) => {
|
|
59062
|
+
({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol, params, returns, values, holds, dryRun }) => {
|
|
57507
59063
|
try {
|
|
57508
59064
|
const spec = {
|
|
57509
59065
|
kind,
|
|
@@ -57533,9 +59089,9 @@ ${renderChangeReport(report3)}`,
|
|
|
57533
59089
|
...values ? { values } : {},
|
|
57534
59090
|
...holds ? { holds } : {}
|
|
57535
59091
|
};
|
|
57536
|
-
const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields });
|
|
59092
|
+
const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields, ...dryRun ? { dryRun: true } : {} });
|
|
57537
59093
|
return writeReceipt(
|
|
57538
|
-
|
|
59094
|
+
`${dryRun ? "DRY RUN \u2014 nothing was written. Would have " : "Successfully "}${receipt.replacedExisting ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
|
|
57539
59095
|
receipt
|
|
57540
59096
|
);
|
|
57541
59097
|
} catch (e) {
|
|
@@ -57693,20 +59249,23 @@ ${renderChangeReport(report3)}`,
|
|
|
57693
59249
|
server,
|
|
57694
59250
|
"sdd_delete_spec",
|
|
57695
59251
|
{
|
|
57696
|
-
description: "Delete a spec and everything it contains \u2014 a contract with its implementations, a component with its contracts and owned members, a subsystem with its components and types \u2014 pruning the folders left empty. Refused, naming every reference, while a spec that stays still references what would go (the invalid references the deletion would introduce); force: true deletes anyway and leaves them for validate to report. dryRun answers the same plan and removes nothing. The structured answer lists every spec removed, every reference, and the tests that encode a removed method (when the project declares test roots).",
|
|
59252
|
+
description: "Delete a spec and everything it contains \u2014 a contract with its implementations, a component with its contracts and owned members, a subsystem with its components and types \u2014 pruning the folders left empty. Refused, naming every reference, while a spec that stays still references what would go (the invalid references the deletion would introduce); force: true deletes anyway and leaves them for validate to report. Refused outright, the dry run included: a spec of another project (a member's spec read through this tree, `alias::name`, is deleted from that member's own root \u2014 the refusal names its folder), an id no spec of the kind holds (the refusal names what it might have meant: another kind holding it, the `<subsystem>::<id>` form of a type, the ids the kind holds), and the L0. dryRun answers the same plan and removes nothing. Like a rename, it reports `published` \u2014 every public name of this project a consumer reaches what goes through \u2014 and `breaks`: the consumers in reach (family members included, and checkouts in the `search` folders) whose specs reach one of those. The structured answer lists every spec removed, every reference, the published names, the consumers it breaks, and the tests that encode a removed method (when the project declares test roots).",
|
|
57697
59253
|
inputSchema: {
|
|
57698
59254
|
kind: import_zod13.z.enum(["subsystem", "component", "interface", "implementation", "type"]).describe("The kind of spec to delete"),
|
|
57699
59255
|
id: import_zod13.z.string().describe("The ID of the spec to delete"),
|
|
57700
59256
|
dryRun: import_zod13.z.boolean().optional().describe("Answer with the plan \u2014 every spec that would be removed, every reference that would be left \u2014 and remove nothing"),
|
|
57701
|
-
force: import_zod13.z.boolean().optional().describe("Delete even though specs that stay still reference what goes, leaving those references for validate to report. Without it such a deletion is refused")
|
|
59257
|
+
force: import_zod13.z.boolean().optional().describe("Delete even though specs that stay still reference what goes, leaving those references for validate to report. Without it such a deletion is refused"),
|
|
59258
|
+
search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family (e.g. the folder holding sibling checkouts), relative to the bound project root or absolute")
|
|
57702
59259
|
},
|
|
57703
59260
|
outputSchema: specDeletionOutput
|
|
57704
59261
|
},
|
|
57705
|
-
({ kind, id, dryRun, force }) => {
|
|
59262
|
+
({ kind, id, dryRun, force, search }) => {
|
|
57706
59263
|
try {
|
|
59264
|
+
const consumers = listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d)));
|
|
57707
59265
|
const deletion = deleteSpec2(kind, id, dryRun, force);
|
|
57708
|
-
|
|
57709
|
-
|
|
59266
|
+
const breaks = consumers.map((c) => narrowedToUses(c, deletion.published)).filter((c) => c !== null);
|
|
59267
|
+
const answer = breaks.length > 0 ? { ...deletion, breaks } : deletion;
|
|
59268
|
+
return structured(renderDeletion(answer), answer);
|
|
57710
59269
|
} catch (e) {
|
|
57711
59270
|
return errText(String(e));
|
|
57712
59271
|
}
|
|
@@ -57721,13 +59280,18 @@ ${renderChangeReport(report3)}`,
|
|
|
57721
59280
|
kind: import_zod13.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project export table, each entry a re-export {from, component, interface, typeDef, as, name, type, details, audience: project|department|instance|partner|external} \u2014 from alone re-exports everything a subsystem exports, and a legacy {id, subsystem} reads as {as, from})"),
|
|
57722
59281
|
id: import_zod13.z.string().describe("The ID of the spec to update (namespaced if needed)"),
|
|
57723
59282
|
delta: import_zod13.z.record(import_zod13.z.any()).describe(`The fields to merge. LISTS UPSERT by identity at every depth: naming one element leaves the others, and a named element merges field by field \u2014 a field left out, or null, keeps its stored value (a method named without "narrative" keeps its steps). Identity is name or id by default; dispatch by capability, lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by subsystem, publicInterfaces by component+interface (a re-export by from+item+interface+as), lint.allow by code+at, findings by code, boundaries by name, globalRequirements by description, cases by value, catches by error. Plain-value lists (owns, dependsOn, guarantees, technologies, requirements written as text) append what they lack and keep what they hold. Restating a list with exactly its stored elements in another order REORDERS it. Remove an element with "action": "delete" (or "remove": true) beside its identity, a plain value as {"value": \u2026, "action": "delete"}; [] clears a list; {"unset": ["basePath"]} removes an optional field (null means no change). NARRATIVE steps are addressed by stepNumber, applied in ascending order against the numbering earlier entries of the same delta left: "action": "insert" shifts later steps up, "delete" shifts them down \u2014 refused when a jump still targets the step, when a restated label or description does not match it, or for a loop/try/parallel header whose body stands. Stored jumps relocate; a jump the delta writes names the final numbering; "captureJumps": true on an insert lands entry jumps on the new step. Every jump field has a *Label twin (toLabel, onFalseLabel, endLabel, a case or catch "label") resolved after the merge. Retyping a step rebuilds it for its new type and names the fields dropped. lint: {"allow": [{code, at, covers, reason}]} silences a warning or notice on this spec: "at" is the site the finding reports and is required when it reports one, "covers" lists an aggregating finding's units; errors never silence. Local ids are qualified as the loader qualifies them. A key that is not a field at its depth is named back under NO EFFECT: read that list.`),
|
|
57724
|
-
dryRun: import_zod13.z.boolean().optional().describe("Ask what this delta WOULD do: the whole write runs \u2014 the gate and the writer's parse included \u2014 and answers the report it would produce, marked DRY RUN, with not one byte written.")
|
|
59283
|
+
dryRun: import_zod13.z.boolean().optional().describe("Ask what this delta WOULD do: the whole write runs \u2014 the gate and the writer's parse included \u2014 and answers the report it would produce, marked DRY RUN, with not one byte written."),
|
|
59284
|
+
search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family, read when the delta removes contract methods: the removal names the consumers it breaks, as a rename does")
|
|
57725
59285
|
},
|
|
57726
59286
|
outputSchema: specChangeReportOutput
|
|
57727
59287
|
},
|
|
57728
|
-
({ kind, id, delta, dryRun }) => {
|
|
59288
|
+
({ kind, id, delta, dryRun, search }) => {
|
|
57729
59289
|
try {
|
|
59290
|
+
const removes = kind === "interface" && delta && typeof delta === "object" && (Array.isArray(delta.methods) && (delta.methods.length === 0 || delta.methods.some((m) => m?.action === "delete" || m?.remove === true)) || Array.isArray(delta.unset) && delta.unset.includes("methods"));
|
|
59291
|
+
const consumers = removes ? listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d))) : [];
|
|
57730
59292
|
const report3 = updateSpecGated(kind, id, delta, dryRun);
|
|
59293
|
+
const breaks = report3.published ? consumers.map((c) => narrowedToUses(c, report3.published)).filter((c) => c !== null) : [];
|
|
59294
|
+
if (breaks.length > 0) report3.breaks = breaks;
|
|
57731
59295
|
return structured(renderChangeReport(report3), report3);
|
|
57732
59296
|
} catch (e) {
|
|
57733
59297
|
return errText(String(e));
|
|
@@ -58289,6 +59853,7 @@ function loadProjectConfig3() {
|
|
|
58289
59853
|
createEmptyTopologyConfig,
|
|
58290
59854
|
createMember,
|
|
58291
59855
|
createProjectConfig,
|
|
59856
|
+
crossProjectWriteRefusal,
|
|
58292
59857
|
currentRootBinding,
|
|
58293
59858
|
declareExternal,
|
|
58294
59859
|
declareMember,
|
|
@@ -58458,6 +60023,7 @@ function loadProjectConfig3() {
|
|
|
58458
60023
|
moveMember,
|
|
58459
60024
|
moveMethods,
|
|
58460
60025
|
moveMountToMembers,
|
|
60026
|
+
moveSpec,
|
|
58461
60027
|
nameKey,
|
|
58462
60028
|
namesAnInteger,
|
|
58463
60029
|
narrativeStepForeignFields,
|
|
@@ -58487,6 +60053,7 @@ function loadProjectConfig3() {
|
|
|
58487
60053
|
projectIdentity,
|
|
58488
60054
|
promoteMember,
|
|
58489
60055
|
provisionProject,
|
|
60056
|
+
publishedUsesOf,
|
|
58490
60057
|
qualifiedTypeId,
|
|
58491
60058
|
rangeProblem,
|
|
58492
60059
|
readCodeAnnotation,
|
|
@@ -58516,6 +60083,7 @@ function loadProjectConfig3() {
|
|
|
58516
60083
|
renameId,
|
|
58517
60084
|
renameMethod,
|
|
58518
60085
|
renameParam,
|
|
60086
|
+
renameSpecId,
|
|
58519
60087
|
renameType,
|
|
58520
60088
|
renderDiagram,
|
|
58521
60089
|
repairForeignStepFields,
|
|
@@ -58545,6 +60113,7 @@ function loadProjectConfig3() {
|
|
|
58545
60113
|
retiredReachFacts,
|
|
58546
60114
|
returnTypeOf,
|
|
58547
60115
|
rewriteReferences,
|
|
60116
|
+
routerLinkage,
|
|
58548
60117
|
runWithHostedLookup,
|
|
58549
60118
|
runWithProjectBinding,
|
|
58550
60119
|
runWithProjectRoot,
|
|
@@ -58565,6 +60134,7 @@ function loadProjectConfig3() {
|
|
|
58565
60134
|
settledSpecPaths,
|
|
58566
60135
|
signatureFacts,
|
|
58567
60136
|
signatureTypeRefs,
|
|
60137
|
+
specIndexMethodReferences,
|
|
58568
60138
|
specIndexReferencesTo,
|
|
58569
60139
|
specIndexRetiredBy,
|
|
58570
60140
|
specIndexSubtreeOf,
|