@wairon/cli 5.1.1-dev.113 → 5.1.1-dev.114
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 +1502 -212
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +79 -6
- package/dist/index.js +1463 -202
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +4 -1
- package/dist/templates/skills/sdd-delegate.md +6 -8
- package/dist/templates/skills/sdd-implement.md +1 -0
- package/docs/cli.md +41 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3059,7 +3059,7 @@ var require_dist = __commonJS({
|
|
|
3059
3059
|
function slug(name) {
|
|
3060
3060
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3061
3061
|
}
|
|
3062
|
-
var
|
|
3062
|
+
var fs34 = __toESM2(require("fs"));
|
|
3063
3063
|
var path52 = __toESM2(require("path"));
|
|
3064
3064
|
var import_fflate = require_node();
|
|
3065
3065
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
|
|
@@ -3099,8 +3099,8 @@ var require_dist = __commonJS({
|
|
|
3099
3099
|
function writeTree(destDir, files) {
|
|
3100
3100
|
for (const file of files) {
|
|
3101
3101
|
const absolute = path52.join(destDir, file.path);
|
|
3102
|
-
|
|
3103
|
-
|
|
3102
|
+
fs34.mkdirSync(path52.dirname(absolute), { recursive: true });
|
|
3103
|
+
fs34.writeFileSync(absolute, file.contents);
|
|
3104
3104
|
}
|
|
3105
3105
|
}
|
|
3106
3106
|
function classifyKind(name, symlinks) {
|
|
@@ -3109,14 +3109,14 @@ var require_dist = __commonJS({
|
|
|
3109
3109
|
return "file";
|
|
3110
3110
|
}
|
|
3111
3111
|
function walkPackDir(root, current2, out) {
|
|
3112
|
-
for (const entry of
|
|
3112
|
+
for (const entry of fs34.readdirSync(current2, { withFileTypes: true })) {
|
|
3113
3113
|
if (entry.isDirectory()) {
|
|
3114
3114
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
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
3118
|
const relative27 = path52.relative(root, absolute).split(path52.sep).join("/");
|
|
3119
|
-
out.push({ path: relative27, contents:
|
|
3119
|
+
out.push({ path: relative27, contents: fs34.readFileSync(absolute) });
|
|
3120
3120
|
}
|
|
3121
3121
|
}
|
|
3122
3122
|
}
|
|
@@ -3792,6 +3792,7 @@ __export(src_exports, {
|
|
|
3792
3792
|
localApprover: () => localApprover,
|
|
3793
3793
|
localGuideFilePath: () => localGuideFilePath,
|
|
3794
3794
|
localTypesOf: () => localTypesOf,
|
|
3795
|
+
lockTree: () => lockTree2,
|
|
3795
3796
|
logger: () => logger,
|
|
3796
3797
|
makeScopeFilter: () => makeScopeFilter,
|
|
3797
3798
|
markSelectionsBundled: () => markSelectionsBundled,
|
|
@@ -3801,6 +3802,7 @@ __export(src_exports, {
|
|
|
3801
3802
|
memberDeclarationOf: () => memberDeclarationOf,
|
|
3802
3803
|
memberDigest: () => memberDigest,
|
|
3803
3804
|
memberLocationOf: () => memberLocationOf,
|
|
3805
|
+
memberRevisions: () => memberRevisions,
|
|
3804
3806
|
memberTypesOf: () => memberTypesOf,
|
|
3805
3807
|
methodCasingFor: () => methodCasingFor,
|
|
3806
3808
|
methodGenericParameters: () => methodGenericParameters,
|
|
@@ -3849,6 +3851,7 @@ __export(src_exports, {
|
|
|
3849
3851
|
readJsonFile: () => readJsonFile,
|
|
3850
3852
|
readLockRecord: () => readLockRecord,
|
|
3851
3853
|
readLockState: () => readLockState,
|
|
3854
|
+
readPinnedSpec: () => readPinnedSpec,
|
|
3852
3855
|
readRetiredReachForms: () => readRetiredReachForms,
|
|
3853
3856
|
readStampVersion: () => readStampVersion,
|
|
3854
3857
|
readYamlFile: () => readYamlFile,
|
|
@@ -3957,6 +3960,7 @@ __export(src_exports, {
|
|
|
3957
3960
|
typeSourceFiles: () => typeSourceFiles,
|
|
3958
3961
|
typeSpellingFacts: () => typeSpellingFacts,
|
|
3959
3962
|
uninstallPack: () => uninstallPack,
|
|
3963
|
+
unlockTree: () => unlockTree2,
|
|
3960
3964
|
updateMember: () => updateMember,
|
|
3961
3965
|
updateSpec: () => updateSpec,
|
|
3962
3966
|
upsertPackSelection: () => upsertPackSelection,
|
|
@@ -4324,7 +4328,7 @@ function defaultTargetConfig(type) {
|
|
|
4324
4328
|
enabled: true
|
|
4325
4329
|
};
|
|
4326
4330
|
}
|
|
4327
|
-
var WAIRON_VERSION = "5.1.1-dev.
|
|
4331
|
+
var WAIRON_VERSION = "5.1.1-dev.114";
|
|
4328
4332
|
var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
4329
4333
|
var ARCHITECT_AGENT_ID = "agent-architect";
|
|
4330
4334
|
var ARCHITECT_TEMPLATE_ID = "architect";
|
|
@@ -4570,6 +4574,13 @@ function lastCommitOf(directory, pathspec) {
|
|
|
4570
4574
|
return null;
|
|
4571
4575
|
}
|
|
4572
4576
|
}
|
|
4577
|
+
function commitIntroducing(directory, pathspec, text3) {
|
|
4578
|
+
try {
|
|
4579
|
+
return git(["log", "--reverse", "--format=%H", `-S${text3}`, "--", pathspec], directory).split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? null;
|
|
4580
|
+
} catch {
|
|
4581
|
+
return null;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4573
4584
|
function commitOf(directory, ref) {
|
|
4574
4585
|
try {
|
|
4575
4586
|
return git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], directory).trim() || null;
|
|
@@ -10344,6 +10355,101 @@ function removeSpecFile(specPath, specsRoot) {
|
|
|
10344
10355
|
pruneEmptyDirs(path10.dirname(specPath), path10.resolve(specsRoot));
|
|
10345
10356
|
return true;
|
|
10346
10357
|
}
|
|
10358
|
+
var LOCK_FILE = ".spec-write.lock";
|
|
10359
|
+
var LOCK_STALE_MS = 6e4;
|
|
10360
|
+
var LOCK_WAIT_MS = 3e4;
|
|
10361
|
+
var lockHolds = /* @__PURE__ */ new Map();
|
|
10362
|
+
function sleepSync(ms) {
|
|
10363
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
10364
|
+
}
|
|
10365
|
+
function processAlive(pid) {
|
|
10366
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
10367
|
+
try {
|
|
10368
|
+
process.kill(pid, 0);
|
|
10369
|
+
return true;
|
|
10370
|
+
} catch (e) {
|
|
10371
|
+
return e.code === "EPERM";
|
|
10372
|
+
}
|
|
10373
|
+
}
|
|
10374
|
+
function lockHolder(file) {
|
|
10375
|
+
try {
|
|
10376
|
+
const [pid, at] = fs6.readFileSync(file, "utf8").split("\n");
|
|
10377
|
+
if (!/^\d+$/.test(pid ?? "") || !/^\d+$/.test(at ?? "")) return null;
|
|
10378
|
+
return { pid: Number(pid), at: Number(at) };
|
|
10379
|
+
} catch {
|
|
10380
|
+
return null;
|
|
10381
|
+
}
|
|
10382
|
+
}
|
|
10383
|
+
function lockTree(root) {
|
|
10384
|
+
const dir = path10.join(root, ".wai");
|
|
10385
|
+
if (!fs6.existsSync(dir)) return;
|
|
10386
|
+
const file = path10.join(dir, LOCK_FILE);
|
|
10387
|
+
const held = lockHolds.get(file) ?? 0;
|
|
10388
|
+
if (held > 0) {
|
|
10389
|
+
lockHolds.set(file, held + 1);
|
|
10390
|
+
return;
|
|
10391
|
+
}
|
|
10392
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
10393
|
+
while (!tryCreateLock(file)) {
|
|
10394
|
+
if (breakStaleLock(file)) continue;
|
|
10395
|
+
if (Date.now() > deadline) {
|
|
10396
|
+
throw new Error(
|
|
10397
|
+
`spec-tree-locked: another session (process ${lockHolder(file)?.pid ?? "unknown"}) has been writing this project's specs for over ${LOCK_WAIT_MS / 1e3}s (${file}). Nothing was written. Try again once it finishes; a lock left by a process that is gone is broken automatically.`
|
|
10398
|
+
);
|
|
10399
|
+
}
|
|
10400
|
+
sleepSync(15 + Math.floor(Math.random() * 20));
|
|
10401
|
+
}
|
|
10402
|
+
lockHolds.set(file, 1);
|
|
10403
|
+
}
|
|
10404
|
+
function lockAge(file) {
|
|
10405
|
+
try {
|
|
10406
|
+
return Date.now() - fs6.statSync(file).mtimeMs;
|
|
10407
|
+
} catch {
|
|
10408
|
+
return 0;
|
|
10409
|
+
}
|
|
10410
|
+
}
|
|
10411
|
+
function tryCreateLock(file) {
|
|
10412
|
+
let fd;
|
|
10413
|
+
try {
|
|
10414
|
+
fd = fs6.openSync(file, "wx");
|
|
10415
|
+
} catch (e) {
|
|
10416
|
+
if (e.code === "EEXIST") return false;
|
|
10417
|
+
throw e;
|
|
10418
|
+
}
|
|
10419
|
+
try {
|
|
10420
|
+
fs6.writeSync(fd, `${process.pid}
|
|
10421
|
+
${Date.now()}
|
|
10422
|
+
`);
|
|
10423
|
+
} finally {
|
|
10424
|
+
fs6.closeSync(fd);
|
|
10425
|
+
}
|
|
10426
|
+
return true;
|
|
10427
|
+
}
|
|
10428
|
+
function breakStaleLock(file) {
|
|
10429
|
+
const holder = lockHolder(file);
|
|
10430
|
+
const stale = holder === null ? lockAge(file) > LOCK_STALE_MS : holder.pid === process.pid || !processAlive(holder.pid) || Date.now() - holder.at > LOCK_STALE_MS;
|
|
10431
|
+
if (!stale) return false;
|
|
10432
|
+
try {
|
|
10433
|
+
fs6.unlinkSync(file);
|
|
10434
|
+
} catch {
|
|
10435
|
+
}
|
|
10436
|
+
return true;
|
|
10437
|
+
}
|
|
10438
|
+
function unlockTree(root) {
|
|
10439
|
+
const file = path10.join(root, ".wai", LOCK_FILE);
|
|
10440
|
+
const held = lockHolds.get(file) ?? 0;
|
|
10441
|
+
if (held <= 0) return;
|
|
10442
|
+
if (held > 1) {
|
|
10443
|
+
lockHolds.set(file, held - 1);
|
|
10444
|
+
return;
|
|
10445
|
+
}
|
|
10446
|
+
lockHolds.delete(file);
|
|
10447
|
+
if (lockHolder(file)?.pid !== process.pid) return;
|
|
10448
|
+
try {
|
|
10449
|
+
fs6.unlinkSync(file);
|
|
10450
|
+
} catch {
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10347
10453
|
function pruneEmptyDirs(dir, specsRoot) {
|
|
10348
10454
|
let at = dir;
|
|
10349
10455
|
while (at !== specsRoot && at.startsWith(specsRoot)) {
|
|
@@ -10627,10 +10733,10 @@ function stepGraph(method2) {
|
|
|
10627
10733
|
const s = byNum.get(n);
|
|
10628
10734
|
if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
|
|
10629
10735
|
const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
|
|
10630
|
-
const
|
|
10736
|
+
const join32 = fallNext(s.endStep);
|
|
10631
10737
|
for (let i = 0; i < entries.length; i++) {
|
|
10632
10738
|
const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
|
|
10633
|
-
if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd,
|
|
10739
|
+
if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join32);
|
|
10634
10740
|
}
|
|
10635
10741
|
}
|
|
10636
10742
|
const successorsOf = (n) => {
|
|
@@ -11007,7 +11113,13 @@ function closureTypeOf(snapshot, identifier) {
|
|
|
11007
11113
|
const ref = local.join("::");
|
|
11008
11114
|
const matches = snapshot.types.filter((def) => matchTypeRef(ref, def.id));
|
|
11009
11115
|
if (matches.length === 1) return matches[0];
|
|
11010
|
-
|
|
11116
|
+
const exact = matches.find((def) => nameKey(def.id) === nameKey(ref));
|
|
11117
|
+
if (exact || matches.length > 0) return exact;
|
|
11118
|
+
if (local.length === 2 && !identifier.includes("::")) {
|
|
11119
|
+
const unqualified = snapshot.types.filter((def) => !def.id.includes("::") && !def.id.includes(".") && nameKey(def.id) === nameKey(local[1]));
|
|
11120
|
+
if (unqualified.length === 1) return unqualified[0];
|
|
11121
|
+
}
|
|
11122
|
+
return void 0;
|
|
11011
11123
|
}
|
|
11012
11124
|
function renamedRefs(expr, rename) {
|
|
11013
11125
|
const args = expr.args.map((a) => renamedRefs(a, rename));
|
|
@@ -11108,6 +11220,9 @@ function carriedFactChanges(pinned, live) {
|
|
|
11108
11220
|
const nowMethod = liveMethods.get(method2.name);
|
|
11109
11221
|
if (!nowMethod) continue;
|
|
11110
11222
|
differs(`effect of ${entry.id}.${method2.name}`, method2.effect, nowMethod.effect);
|
|
11223
|
+
if (method2.endpoint && factValue(method2.endpoint) !== factValue(nowMethod.endpoint)) {
|
|
11224
|
+
changes.push(`endpoint of ${entry.id}.${method2.name} (${shownEndpoint(method2.endpoint)} \u2192 ${shownEndpoint(nowMethod.endpoint)})`);
|
|
11225
|
+
}
|
|
11111
11226
|
differs(`rename trace of ${entry.id}.${method2.name}`, method2.formerly, nowMethod.formerly);
|
|
11112
11227
|
for (const r of paramRenames(method2, nowMethod)) {
|
|
11113
11228
|
changes.push(`parameter "${r.from}" of ${entry.id}.${method2.name} (renamed to "${r.to}")`);
|
|
@@ -11152,7 +11267,8 @@ function memberDigest(snapshot, publicName, member) {
|
|
|
11152
11267
|
return sha256(canonicalize({ method: methodShape(snapshot, method2), closure: closureShapes(snapshot, methodTypeRefs(method2)) }));
|
|
11153
11268
|
}
|
|
11154
11269
|
function shownSignature(method2) {
|
|
11155
|
-
|
|
11270
|
+
const derived = `${method2.name}(${(method2.params ?? []).map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}): ${method2.returns}`;
|
|
11271
|
+
return (method2.params?.length ? derived : method2.signature) || derived;
|
|
11156
11272
|
}
|
|
11157
11273
|
function entryFacts(before, after) {
|
|
11158
11274
|
return [
|
|
@@ -11260,8 +11376,45 @@ function closureRenames(newer, older) {
|
|
|
11260
11376
|
function renamesIn(ids, notes) {
|
|
11261
11377
|
return ids.flatMap((id) => notes.get(id) ?? []);
|
|
11262
11378
|
}
|
|
11379
|
+
function carriedThroughOwnRecords(snapshot, method2, exported) {
|
|
11380
|
+
const byId = new Map(snapshot.types.map((def) => [def.id, def]));
|
|
11381
|
+
const carried = /* @__PURE__ */ new Set();
|
|
11382
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
11383
|
+
const queue = methodTypeRefs(method2).flatMap((expr) => extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))).filter((id) => byId.has(id) && !exported.has(id));
|
|
11384
|
+
while (queue.length) {
|
|
11385
|
+
const id = queue.shift();
|
|
11386
|
+
if (expanded.has(id)) continue;
|
|
11387
|
+
expanded.add(id);
|
|
11388
|
+
for (const expr of typeDefExprs(byId.get(id))) {
|
|
11389
|
+
for (const child of extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))) {
|
|
11390
|
+
if (!byId.has(child)) continue;
|
|
11391
|
+
carried.add(child);
|
|
11392
|
+
if (!exported.has(child)) queue.push(child);
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
return carried;
|
|
11397
|
+
}
|
|
11398
|
+
function tracedRenamesBehind(older, newer, was, now) {
|
|
11399
|
+
const written = new Set(methodTypeRefs(was).flatMap((expr) => expr.match(TYPE_IDENTIFIER) ?? []).map((token) => token.replace(/^::/, "")));
|
|
11400
|
+
const olderDefs = new Map(older.types.map((d) => [d.id, d]));
|
|
11401
|
+
const out = [];
|
|
11402
|
+
for (const id of closureIds(newer, now)) {
|
|
11403
|
+
const def = newer.types.find((d) => d.id === id);
|
|
11404
|
+
const from = (def.formerly ?? []).find((f) => written.has(f) && !written.has(def.id));
|
|
11405
|
+
if (from === void 0) continue;
|
|
11406
|
+
out.push(`type "${from}" renamed to "${def.id}"`);
|
|
11407
|
+
const olderDef = olderDefs.get(from);
|
|
11408
|
+
const renamed2 = olderDef ? fieldRenames(olderDef, def) : def.fields.flatMap((field) => (field.formerly ?? []).filter((f) => !def.fields.some((x) => x.name === f)).slice(-1).map((f) => ({ from: f, to: field.name })));
|
|
11409
|
+
for (const r of renamed2) out.push(`field "${def.id}.${r.from}" renamed to "${r.to}"`);
|
|
11410
|
+
}
|
|
11411
|
+
return out;
|
|
11412
|
+
}
|
|
11263
11413
|
function signatureChange(older, newer, was, now, undoneNow = now) {
|
|
11264
|
-
if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name)))
|
|
11414
|
+
if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name))) {
|
|
11415
|
+
const behind = tracedRenamesBehind(older, newer, was, now);
|
|
11416
|
+
return `signature ${shownSignature(was)} \u2192 ${shownSignature(now)}${behind.length ? ` (${behind.join("; ")})` : ""}`;
|
|
11417
|
+
}
|
|
11265
11418
|
const oldDefs = new Map(older.types.map((d) => [d.id, d]));
|
|
11266
11419
|
const newDefs = new Map(newer.types.map((d) => [d.id, d]));
|
|
11267
11420
|
const ids = [.../* @__PURE__ */ new Set([...closureIds(older, was), ...closureIds(newer, now)])].sort();
|
|
@@ -11271,7 +11424,8 @@ function signatureChange(older, newer, was, now, undoneNow = now) {
|
|
|
11271
11424
|
return !a || !b || canonicalize(typeShape(older, a)) !== canonicalize(typeShape(newer, b));
|
|
11272
11425
|
});
|
|
11273
11426
|
const exported = new Set([...newer.exportedTypes ?? [], ...older.exportedTypes ?? []].map((t) => t.type));
|
|
11274
|
-
const
|
|
11427
|
+
const carried = carriedThroughOwnRecords(newer, now, exported);
|
|
11428
|
+
const unlisted = moved.filter((id) => !exported.has(id) || carried.has(id));
|
|
11275
11429
|
if (moved.length > 0 && unlisted.length === 0) return null;
|
|
11276
11430
|
const named2 = (unlisted.length > 0 ? unlisted : moved).map((id) => `"${id}"`).join(", ");
|
|
11277
11431
|
return named2 ? `signature reads the same, but a type it names changed shape: ${named2}` : "signature reads the same, but a type it names changed shape";
|
|
@@ -11400,7 +11554,8 @@ function surfaceChanges(newer, older) {
|
|
|
11400
11554
|
if (changed !== null) out.push({ kind: "changed", name: entry.id, member: method2.name, detail: changed });
|
|
11401
11555
|
} else if (a !== b) {
|
|
11402
11556
|
const exported = new Set((newer.exportedTypes ?? []).map((t) => t.type));
|
|
11403
|
-
const
|
|
11557
|
+
const carried = carriedThroughOwnRecords(newer, method2, exported);
|
|
11558
|
+
const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id) || carried.has(id)), notes);
|
|
11404
11559
|
if (embedded.length > 0) {
|
|
11405
11560
|
const key2 = embedded.join("; ");
|
|
11406
11561
|
embeddedBy.set(key2, [...embeddedBy.get(key2) ?? [], method2.name]);
|
|
@@ -17398,7 +17553,8 @@ function schemaScope(ownDefs, externals, registry, alias) {
|
|
|
17398
17553
|
return hit !== void 0 ? own2(hit) : void 0;
|
|
17399
17554
|
},
|
|
17400
17555
|
isObject(typeRef2) {
|
|
17401
|
-
const
|
|
17556
|
+
const read2 = expressionOf(typeRef2);
|
|
17557
|
+
const expr = read2?.form === "optional" ? read2.args[0] : read2;
|
|
17402
17558
|
if (!expr || expr.form !== "named" && expr.form !== "applied") return false;
|
|
17403
17559
|
const name = expr.name ?? "";
|
|
17404
17560
|
const sep18 = name.indexOf("::");
|
|
@@ -17517,10 +17673,10 @@ function enumValuesFrom(schema) {
|
|
|
17517
17673
|
return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described2.has(name) ? { description: described2.get(name) } : {} }));
|
|
17518
17674
|
}
|
|
17519
17675
|
function openApiPath(basePath, endpointPath) {
|
|
17520
|
-
const
|
|
17676
|
+
const join32 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
|
|
17521
17677
|
const base = basePath && basePath.trim() && basePath.trim() !== "/" ? basePath.trim() : "";
|
|
17522
17678
|
const rootedBase = base.startsWith("/") || !base ? base : `/${base}`;
|
|
17523
|
-
const raw = base ? endpointPath.replace(/\/+$/, "") === "" ? rootedBase :
|
|
17679
|
+
const raw = base ? endpointPath.replace(/\/+$/, "") === "" ? rootedBase : join32(rootedBase, endpointPath) : endpointPath;
|
|
17524
17680
|
const rooted = raw.startsWith("/") ? raw : `/${raw}`;
|
|
17525
17681
|
return rooted.replace(/\/:([A-Za-z_][A-Za-z0-9_]*)/g, "/{$1}");
|
|
17526
17682
|
}
|
|
@@ -17580,7 +17736,7 @@ function operationFor(method2, scope, entry) {
|
|
|
17580
17736
|
const bodyParam = rest.length === 1 ? rest[0] : void 0;
|
|
17581
17737
|
if (bodyVerbs.has(httpVerb) && bodyParam && scope.isObject(bodyParam.type)) {
|
|
17582
17738
|
op.requestBody = {
|
|
17583
|
-
required: !bodyParam.optional
|
|
17739
|
+
required: !bodyParam.optional,
|
|
17584
17740
|
...bodyParam.description ? { description: bodyParam.description } : {},
|
|
17585
17741
|
content: { "application/json": { schema: schemaFor(bodyParam.type, scope) } }
|
|
17586
17742
|
};
|
|
@@ -18657,7 +18813,10 @@ function canonicalReferences(snapshot) {
|
|
|
18657
18813
|
...entry,
|
|
18658
18814
|
methods: entry.methods.map((m) => ({
|
|
18659
18815
|
...m,
|
|
18660
|
-
|
|
18816
|
+
// Rebuilt from the structured params when it has them: a parameter's
|
|
18817
|
+
// NAME is never a type position, so one spelled like a type
|
|
18818
|
+
// (`order_id: order_id`) keeps its name.
|
|
18819
|
+
signature: m.params?.length ? `${m.name}(${m.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${canon(p.type)}`).join(", ")}): ${canon(m.returns)}` : canon(m.signature),
|
|
18661
18820
|
returns: canon(m.returns),
|
|
18662
18821
|
...m.params ? { params: m.params.map((p) => ({ ...p, type: canon(p.type) })) } : {}
|
|
18663
18822
|
}))
|
|
@@ -18893,14 +19052,20 @@ function foreignSurfaces() {
|
|
|
18893
19052
|
}
|
|
18894
19053
|
const root = getProjectRoot();
|
|
18895
19054
|
for (const m of members) {
|
|
18896
|
-
|
|
19055
|
+
const location = m.path ?? m.source.path;
|
|
19056
|
+
if (m.problem || m.storage !== "contained" && m.storage !== "path" || location === void 0 || out.has(m.alias)) continue;
|
|
18897
19057
|
try {
|
|
18898
|
-
const surface = runWithProjectRoot(path15.resolve(root,
|
|
19058
|
+
const surface = runWithProjectRoot(path15.resolve(root, location), () => projectOwnSurface("project"));
|
|
18899
19059
|
out.set(m.alias, surface);
|
|
18900
19060
|
if (surface.projectId && !out.has(surface.projectId)) out.set(surface.projectId, surface);
|
|
18901
19061
|
} catch {
|
|
18902
19062
|
}
|
|
18903
19063
|
}
|
|
19064
|
+
try {
|
|
19065
|
+
const own2 = boundProjectId();
|
|
19066
|
+
if (own2 !== void 0 && !out.has(own2)) out.set(own2, projectOwnSurface("project"));
|
|
19067
|
+
} catch {
|
|
19068
|
+
}
|
|
18904
19069
|
return out;
|
|
18905
19070
|
}
|
|
18906
19071
|
function withForeignTypes(snapshot, foreign) {
|
|
@@ -19148,6 +19313,11 @@ function narrowerExports(external, missing, read2) {
|
|
|
19148
19313
|
}
|
|
19149
19314
|
return [...out].map(([name, audience]) => ({ name, audience }));
|
|
19150
19315
|
}
|
|
19316
|
+
function withMoved(detail, moved) {
|
|
19317
|
+
if (moved.length === 0) return detail;
|
|
19318
|
+
const said = `moved since the last pin: ${moved.join(", ")}`;
|
|
19319
|
+
return { detail: detail.detail ? `${said}; ${detail.detail}` : said };
|
|
19320
|
+
}
|
|
19151
19321
|
function pinBinding(binding, lock) {
|
|
19152
19322
|
const { external } = binding;
|
|
19153
19323
|
const unexported = binding.usage?.unexported ?? [];
|
|
@@ -19164,6 +19334,7 @@ function pinBinding(binding, lock) {
|
|
|
19164
19334
|
if (pinned) snapshot = withRetired(snapshot, pinned);
|
|
19165
19335
|
const snapshotChanged = !pinned || contentDigest(pinned) !== digest3 || pinnedContentKey(pinned) !== pinnedContentKey(snapshot);
|
|
19166
19336
|
if (snapshotChanged) externalsRepository.saveSnapshot(external.alias, snapshot);
|
|
19337
|
+
const moved = pinned && snapshotChanged ? carriedFactChanges(pinned, snapshot) : [];
|
|
19167
19338
|
const { used, missing, uncarried } = usedDigests(binding.usage, snapshot, external.alias);
|
|
19168
19339
|
const narrower = missing.length > 0 && external.audience !== "project" ? narrowerExports(external, missing, snapshot) : [];
|
|
19169
19340
|
const entry = {
|
|
@@ -19185,7 +19356,7 @@ function pinBinding(binding, lock) {
|
|
|
19185
19356
|
digest: digest3,
|
|
19186
19357
|
usedNames: Object.keys(used).length,
|
|
19187
19358
|
unexported: [...unexported, ...uncarried],
|
|
19188
|
-
...pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience)
|
|
19359
|
+
...withMoved(pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience), moved)
|
|
19189
19360
|
},
|
|
19190
19361
|
changed: entryChanged
|
|
19191
19362
|
};
|
|
@@ -19368,6 +19539,20 @@ function changedDetail(pinned, live, publicName, member, digest3) {
|
|
|
19368
19539
|
if (renames.length === 0) return {};
|
|
19369
19540
|
return { detail: `${renames.join("; ")}${renameOnly ? " (its shape is otherwise unchanged) \u2014 follow the rename" : " (and its shape changed beyond that)"}` };
|
|
19370
19541
|
}
|
|
19542
|
+
function routeOf(endpoint) {
|
|
19543
|
+
if (!endpoint) return "none";
|
|
19544
|
+
if (endpoint.transport === "HTTP") return `${endpoint.method} ${endpoint.path}${endpoint.status !== void 0 ? ` (answers ${endpoint.status})` : ""}`;
|
|
19545
|
+
const { transport, ...address } = endpoint;
|
|
19546
|
+
return `${String(transport)} ${Object.entries(address).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${String(v)}`).join(" ")}`;
|
|
19547
|
+
}
|
|
19548
|
+
function endpointMove(pinned, live, publicName, member) {
|
|
19549
|
+
if (!pinned || member === "type" || member.startsWith("capability:")) return null;
|
|
19550
|
+
const was = pinned.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
|
|
19551
|
+
if (!was) return null;
|
|
19552
|
+
const now = live.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
|
|
19553
|
+
if (canonicalize(was) === canonicalize(now ?? null)) return null;
|
|
19554
|
+
return now ? { removed: false, detail: `endpoint moved: ${routeOf(was)} \u2192 ${routeOf(now)} \u2014 its signature is unchanged, but a client that spells the route (a hand-written HTTP client, a binding) must follow` } : { removed: true, detail: `endpoint ${routeOf(was)} removed \u2014 the verb is bound to no route any more, so a client calling it over the wire breaks` };
|
|
19555
|
+
}
|
|
19371
19556
|
function compareUses(entry, usage, live, pinned = null) {
|
|
19372
19557
|
const uses = [];
|
|
19373
19558
|
const locked = entry?.used ?? {};
|
|
@@ -19377,11 +19562,21 @@ function compareUses(entry, usage, live, pinned = null) {
|
|
|
19377
19562
|
uses.push(liveHas(publicName) ? { publicName, state: "unchanged" } : goneUse(live, publicName, void 0, void 0));
|
|
19378
19563
|
continue;
|
|
19379
19564
|
}
|
|
19380
|
-
for (const [member,
|
|
19565
|
+
for (const [member, locked2] of Object.entries(members)) {
|
|
19566
|
+
const digest3 = (pinned ? memberDigest(pinned, publicName, member) : null) ?? locked2;
|
|
19381
19567
|
const now = memberDigest(live, publicName, member);
|
|
19382
|
-
if (now === null)
|
|
19383
|
-
|
|
19384
|
-
|
|
19568
|
+
if (now === null) {
|
|
19569
|
+
uses.push(goneUse(live, publicName, member, digest3, pinned));
|
|
19570
|
+
continue;
|
|
19571
|
+
}
|
|
19572
|
+
const route2 = endpointMove(pinned, live, publicName, member);
|
|
19573
|
+
if (now === digest3) {
|
|
19574
|
+
uses.push(route2 === null ? { publicName, member, state: "unchanged" } : { publicName, member, state: route2.removed ? "changed" : "unchanged", detail: route2.detail });
|
|
19575
|
+
continue;
|
|
19576
|
+
}
|
|
19577
|
+
const changed = changedDetail(pinned, live, publicName, member, digest3);
|
|
19578
|
+
const detail = [changed.detail, route2?.detail].filter((d) => !!d).join("; ");
|
|
19579
|
+
uses.push({ publicName, member, state: "changed", ...detail ? { detail } : {} });
|
|
19385
19580
|
}
|
|
19386
19581
|
}
|
|
19387
19582
|
const unlocked = (publicName, member) => {
|
|
@@ -19458,7 +19653,7 @@ function externalStatus(binding, lock) {
|
|
|
19458
19653
|
const snapshot = entry !== void 0 ? externalsRepository.readSnapshot(external.alias) : null;
|
|
19459
19654
|
const uses = compareUses(entry, binding.usage, live, snapshot);
|
|
19460
19655
|
const staleFacts = snapshot ? carriedFactChanges(snapshot, live) : [];
|
|
19461
|
-
const drifted = entry !== void 0 ? contentDigest(live) !== entry.digest || staleFacts.length > 0 : void 0;
|
|
19656
|
+
const drifted = entry !== void 0 ? contentDigest(live) !== (snapshot ? contentDigest(snapshot) : entry.digest) || staleFacts.length > 0 : void 0;
|
|
19462
19657
|
return {
|
|
19463
19658
|
...base,
|
|
19464
19659
|
reachable: true,
|
|
@@ -19468,6 +19663,48 @@ function externalStatus(binding, lock) {
|
|
|
19468
19663
|
uses
|
|
19469
19664
|
};
|
|
19470
19665
|
}
|
|
19666
|
+
function projectedAt(directory) {
|
|
19667
|
+
try {
|
|
19668
|
+
return runWithProjectRoot(directory, () => projectOwnSurface("project"));
|
|
19669
|
+
} catch {
|
|
19670
|
+
return null;
|
|
19671
|
+
}
|
|
19672
|
+
}
|
|
19673
|
+
function memberApprovedSurfaces() {
|
|
19674
|
+
const out = {};
|
|
19675
|
+
for (const member of memberRevisions()) {
|
|
19676
|
+
if (!member.revision) continue;
|
|
19677
|
+
const approved = projectedAt(member.revision.directory);
|
|
19678
|
+
if (approved) out[member.alias] = approved;
|
|
19679
|
+
}
|
|
19680
|
+
return out;
|
|
19681
|
+
}
|
|
19682
|
+
function memberStatuses() {
|
|
19683
|
+
const out = [];
|
|
19684
|
+
for (const member of memberRevisions()) {
|
|
19685
|
+
if (!member.revision) continue;
|
|
19686
|
+
const live = projectedAt(member.root);
|
|
19687
|
+
const approved = projectedAt(member.revision.directory);
|
|
19688
|
+
if (!live || !approved) continue;
|
|
19689
|
+
const { used } = usedDigests(member.usage, approved, member.alias);
|
|
19690
|
+
const entry = { project: member.project, snapshot: "", digest: contentDigest(approved), used };
|
|
19691
|
+
const uses = compareUses(entry, void 0, live, approved).filter((u) => u.state !== "unlocked" && u.state !== "unavailable");
|
|
19692
|
+
const staleFacts = carriedFactChanges(approved, live);
|
|
19693
|
+
out.push({
|
|
19694
|
+
alias: member.alias,
|
|
19695
|
+
project: member.project,
|
|
19696
|
+
sourceKind: "family",
|
|
19697
|
+
pinned: false,
|
|
19698
|
+
reachable: true,
|
|
19699
|
+
stale: uses.some((u) => u.state === "changed" || u.state === "removed" || u.state === "renamed"),
|
|
19700
|
+
drifted: contentDigest(live) !== contentDigest(approved) || staleFacts.length > 0,
|
|
19701
|
+
...staleFacts.length ? { staleFacts } : {},
|
|
19702
|
+
uses,
|
|
19703
|
+
detail: `a live member, compared with ${member.revision.label}`
|
|
19704
|
+
});
|
|
19705
|
+
}
|
|
19706
|
+
return out;
|
|
19707
|
+
}
|
|
19471
19708
|
function hostedOnly(external) {
|
|
19472
19709
|
return external.hosted !== void 0 && external.sourceKind === "unresolved" && getHostedLookup() === null;
|
|
19473
19710
|
}
|
|
@@ -19715,11 +19952,11 @@ function declare(request) {
|
|
|
19715
19952
|
} catch (e) {
|
|
19716
19953
|
bindProblem = e instanceof Error ? e.message : String(e);
|
|
19717
19954
|
}
|
|
19718
|
-
const
|
|
19719
|
-
const producer = "read" in
|
|
19955
|
+
const answered2 = binding ? readProducer(binding) : { why: bindProblem };
|
|
19956
|
+
const producer = "read" in answered2 ? answered2.read : null;
|
|
19720
19957
|
const contradicted = contradiction(request, binding, producer);
|
|
19721
19958
|
if (contradicted) return refused(alias, contradicted);
|
|
19722
|
-
const why2 = "why" in
|
|
19959
|
+
const why2 = "why" in answered2 ? answered2.why : "unknown";
|
|
19723
19960
|
if (request.dryRun) {
|
|
19724
19961
|
return {
|
|
19725
19962
|
alias,
|
|
@@ -19793,10 +20030,10 @@ function updateUse(request) {
|
|
|
19793
20030
|
const named2 = appended.filter((u) => u !== "*");
|
|
19794
20031
|
if (named2.length > 0) {
|
|
19795
20032
|
const binding = resolveDeclared(true).find((b) => b.external.alias === alias);
|
|
19796
|
-
const
|
|
19797
|
-
if (
|
|
19798
|
-
const missing = named2.filter((u) => !
|
|
19799
|
-
if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u,
|
|
20033
|
+
const answered2 = binding ? readProducer(binding) : null;
|
|
20034
|
+
if (answered2 && "read" in answered2) {
|
|
20035
|
+
const missing = named2.filter((u) => !answered2.read.exported.includes(u));
|
|
20036
|
+
if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered2.read)).join("; "));
|
|
19800
20037
|
}
|
|
19801
20038
|
}
|
|
19802
20039
|
const same = next.length === current2.length && next.every((u, i) => u === current2[i]);
|
|
@@ -19849,6 +20086,12 @@ function listExternals2() {
|
|
|
19849
20086
|
function listPinnedExternals2() {
|
|
19850
20087
|
return listPinnedExternals();
|
|
19851
20088
|
}
|
|
20089
|
+
function memberApprovedSurfaces2() {
|
|
20090
|
+
return memberApprovedSurfaces();
|
|
20091
|
+
}
|
|
20092
|
+
function memberStatuses2() {
|
|
20093
|
+
return memberStatuses();
|
|
20094
|
+
}
|
|
19852
20095
|
function unrecordedUses2() {
|
|
19853
20096
|
return unrecordedUses();
|
|
19854
20097
|
}
|
|
@@ -21191,12 +21434,11 @@ function adviseOn(status, family, config) {
|
|
|
21191
21434
|
`${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.`
|
|
21192
21435
|
);
|
|
21193
21436
|
}
|
|
21194
|
-
case "drifted":
|
|
21195
|
-
|
|
21196
|
-
|
|
21197
|
-
|
|
21198
|
-
|
|
21199
|
-
);
|
|
21437
|
+
case "drifted": {
|
|
21438
|
+
const routes = status.uses.filter((u) => u.state === "unchanged" && u.detail?.startsWith("endpoint moved: "));
|
|
21439
|
+
const lead = routes.length ? `The live producer of ${who} moved since it was pinned: the route of ${routes.map((u) => `${useName(u)} (${u.detail.slice("endpoint moved: ".length).split(" \u2014 ")[0]})`).join(", ")} moved. The signatures this project uses are unchanged, but a client that spells the route (a hand-written HTTP client, a binding) must follow.` : `The live producer of ${who} moved since it was pinned, but nothing this project uses changed.`;
|
|
21440
|
+
return advisory(config, "EXTERNAL_DRIFTED", `${lead}${staleTail(status)} Re-pin when convenient (${repin(status.alias)}).`);
|
|
21441
|
+
}
|
|
21200
21442
|
case "unavailable": {
|
|
21201
21443
|
const beyond = status.pinnedDigest !== void 0 && status.reachable && !status.outOfReach ? status.uses.filter((u) => u.state === "unlocked") : [];
|
|
21202
21444
|
if (beyond.length > 0 && status.uses.every((u) => u.state !== "unavailable")) {
|
|
@@ -21643,7 +21885,7 @@ function failureAt(ref, failures) {
|
|
|
21643
21885
|
const last = ref.resolved.split("::").pop() ?? "";
|
|
21644
21886
|
return failures.find((i) => i.resolution.callSite === site && (i.resolution.canonicalTarget === ref.authored || (i.resolution.canonicalTarget ?? "").endsWith(`::${last}`)));
|
|
21645
21887
|
}
|
|
21646
|
-
function containedStatus(child, refs, failures) {
|
|
21888
|
+
function containedStatus(child, refs, failures, approved) {
|
|
21647
21889
|
const base = { alias: child.mountAlias, project: child.id ?? child.namespace, sourceKind: "family", pinned: false };
|
|
21648
21890
|
const unreachable = outsideReach(child);
|
|
21649
21891
|
if (unreachable) {
|
|
@@ -21653,21 +21895,42 @@ function containedStatus(child, refs, failures) {
|
|
|
21653
21895
|
const uses = [];
|
|
21654
21896
|
for (const ref of refs) {
|
|
21655
21897
|
const failed = failureAt(ref, failures);
|
|
21656
|
-
|
|
21898
|
+
if (!failed) {
|
|
21899
|
+
const compared = (approved?.uses ?? []).filter((u) => u.publicName === referenceUse(ref));
|
|
21900
|
+
for (const use2 of compared.length ? compared : [{ publicName: referenceUse(ref), state: "unchanged" }]) {
|
|
21901
|
+
if (!uses.some((u) => u.publicName === use2.publicName && u.member === use2.member && u.state === use2.state)) uses.push(use2);
|
|
21902
|
+
}
|
|
21903
|
+
continue;
|
|
21904
|
+
}
|
|
21905
|
+
const use = { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason };
|
|
21657
21906
|
if (!uses.some((u) => u.publicName === use.publicName && u.state === use.state)) uses.push(use);
|
|
21658
21907
|
}
|
|
21659
|
-
return {
|
|
21908
|
+
return {
|
|
21909
|
+
...base,
|
|
21910
|
+
reachable: true,
|
|
21911
|
+
stale: uses.some((u) => u.state === "removed" || u.state === "changed" || u.state === "renamed"),
|
|
21912
|
+
...approved?.drifted ? { drifted: true } : {},
|
|
21913
|
+
...approved?.staleFacts?.length ? { staleFacts: approved.staleFacts } : {},
|
|
21914
|
+
uses,
|
|
21915
|
+
detail: approved ? `a live member, nothing pinned \u2014 ${approved.detail ?? "compared with its approval"}` : "a contained member, read live \u2014 nothing is pinned"
|
|
21916
|
+
};
|
|
21660
21917
|
}
|
|
21661
|
-
function containedRelations(
|
|
21918
|
+
function containedRelations(answered2) {
|
|
21662
21919
|
const own2 = graph();
|
|
21663
|
-
const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !
|
|
21920
|
+
const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered2.some((s) => s.alias === n.mountAlias)).map((child) => ({
|
|
21664
21921
|
child,
|
|
21665
21922
|
refs: own2.authoredReferences.filter((r) => (own2.owners.get(r.specId) ?? "") === "" && r.producer === child.namespace && r.binding !== "local")
|
|
21666
21923
|
})).filter((c) => c.refs.length > 0);
|
|
21667
21924
|
if (consumed.length === 0) return [];
|
|
21668
21925
|
const config = configOrNull();
|
|
21669
21926
|
const failures = validateProject({ rules: config?.rules, projectType: config?.projectType }).issues.filter((i) => i.resolution !== void 0 && i.resolution.outcome !== "resolved");
|
|
21670
|
-
|
|
21927
|
+
let approved = [];
|
|
21928
|
+
try {
|
|
21929
|
+
approved = memberStatuses2();
|
|
21930
|
+
} catch {
|
|
21931
|
+
approved = [];
|
|
21932
|
+
}
|
|
21933
|
+
return consumed.map(({ child, refs }) => containedStatus(child, refs, failures, approved.find((s) => s.alias === child.mountAlias)));
|
|
21671
21934
|
}
|
|
21672
21935
|
function relationsAt(key2, dir, reach2) {
|
|
21673
21936
|
const bound2 = (fn) => reach2 === null ? runWithProjectRoot(dir, fn) : runWithProjectBinding(dir, reach2, fn);
|
|
@@ -25877,12 +26140,13 @@ function placeholderProblems(path52, bindable, params) {
|
|
|
25877
26140
|
var portalsRule = {
|
|
25878
26141
|
name: "portal-endpoints",
|
|
25879
26142
|
judges: "design",
|
|
25880
|
-
description: "A Portal binds every interface method to a concrete endpoint of its own transport, when that transport requires one (transport.requiresEndpoint): every transport but InProcess, whose verbs are the contract methods themselves, and Custom, whose address is free-form. An HTTP endpoint's path placeholders (`{name}`, or a `:name` segment) bind the method's parameters, so each must be closed, appear once and name a parameter of the method or a field of a parameter's type (a method that states no structured params is not judged on names). One HTTP route binds one verb: two methods bound to the same HTTP method and path \u2014 the Portal's basePath joined in, placeholders compared by position whatever their name or spelling (`{id}` and `:code` are the same segment) \u2014 within one Portal, or across Portals that declare the same basePath, are a duplicate route: no router can dispatch both and an OpenAPI document holds one operation per path and verb.",
|
|
26143
|
+
description: "A Portal binds every interface method to a concrete endpoint of its own transport, when that transport requires one (transport.requiresEndpoint): every transport but InProcess, whose verbs are the contract methods themselves, and Custom, whose address is free-form. An HTTP endpoint's path placeholders (`{name}`, or a `:name` segment) bind the method's parameters, so each must be closed, appear once and name a parameter of the method or a field of a parameter's type (a method that states no structured params is not judged on names). An HTTP endpoint's stated `status` is the success code its callers and its OpenAPI document read, so it must be a standard success or redirect code (200-208, 226, 300-308) and must agree with what the method answers: a 204 or a 304 carries no body, so a method that answers a value cannot state it, and a redirect sends the caller on with a Location header and no body, so the value a redirecting method answers is that location \u2014 a string (or a named type holding one), never a record. One HTTP route binds one verb: two methods bound to the same HTTP method and path \u2014 the Portal's basePath joined in, placeholders compared by position whatever their name or spelling (`{id}` and `:code` are the same segment) \u2014 within one Portal, or across Portals that declare the same basePath, are a duplicate route: no router can dispatch both and an OpenAPI document holds one operation per path and verb.",
|
|
25881
26144
|
codes: [
|
|
25882
26145
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding, on a transport that requires one" },
|
|
25883
26146
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal's transport" },
|
|
25884
26147
|
{ code: "ENDPOINT_PATH_PLACEHOLDER", defaultSeverity: "error", summary: "An HTTP endpoint path holds an unclosed placeholder, one placeholder twice, or a placeholder that names no parameter of its method" },
|
|
25885
|
-
{ code: "ENDPOINT_ROUTE_DUPLICATE", defaultSeverity: "error", summary: "Two methods bound to the same HTTP method and path (placeholders compared by position) within one Portal, or across Portals declaring the same basePath" }
|
|
26148
|
+
{ code: "ENDPOINT_ROUTE_DUPLICATE", defaultSeverity: "error", summary: "Two methods bound to the same HTTP method and path (placeholders compared by position) within one Portal, or across Portals declaring the same basePath" },
|
|
26149
|
+
{ code: "ENDPOINT_STATUS_MISMATCH", defaultSeverity: "warning", summary: "An HTTP endpoint states a status that is no standard success or redirect code, or one that contradicts what its method answers: a 204 or 304 for a method answering a value, or a redirect for a method answering anything but the location (a string)" }
|
|
25886
26150
|
],
|
|
25887
26151
|
check(ctx) {
|
|
25888
26152
|
for (const comp of ctx.components) {
|
|
@@ -25926,6 +26190,7 @@ var portalsRule = {
|
|
|
25926
26190
|
}
|
|
25927
26191
|
}
|
|
25928
26192
|
}
|
|
26193
|
+
statusMismatches(ctx);
|
|
25929
26194
|
duplicateRoutes(ctx);
|
|
25930
26195
|
}
|
|
25931
26196
|
};
|
|
@@ -25971,6 +26236,47 @@ function duplicateRoutes(ctx) {
|
|
|
25971
26236
|
}
|
|
25972
26237
|
}
|
|
25973
26238
|
}
|
|
26239
|
+
var STANDARD_STATUSES = /* @__PURE__ */ new Set([200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308]);
|
|
26240
|
+
var REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
26241
|
+
function answered(returns) {
|
|
26242
|
+
if (!returns) return null;
|
|
26243
|
+
const expr = parseTypeExpression(returns.trim(), "returns").expression;
|
|
26244
|
+
const awaited = expr?.form === "async" ? expr.args[0] : expr;
|
|
26245
|
+
return (awaited?.form === "result" ? awaited.args[0] : awaited) ?? null;
|
|
26246
|
+
}
|
|
26247
|
+
function isLocation(ctx, expr) {
|
|
26248
|
+
const inner2 = expr.form === "optional" ? expr.args[0] : expr;
|
|
26249
|
+
if (inner2.form === "primitive") return inner2.name === "string";
|
|
26250
|
+
if (inner2.form !== "named" || !inner2.name) return false;
|
|
26251
|
+
const local = inner2.name.slice(inner2.name.lastIndexOf("::") + (inner2.name.includes("::") ? 2 : 0)).split(".").pop() ?? inner2.name;
|
|
26252
|
+
const def = ctx.types.find((t) => t.id === inner2.name || t.id === local || t.id.endsWith(`::${local}`));
|
|
26253
|
+
return def?.holds?.trim() === "string";
|
|
26254
|
+
}
|
|
26255
|
+
function statusMismatches(ctx) {
|
|
26256
|
+
for (const comp of ctx.components) {
|
|
26257
|
+
if (comp.componentType !== "Portal" || comp.transport !== "HTTP") continue;
|
|
26258
|
+
const isDraftCtx = ctx.isComponentDraft(comp.id);
|
|
26259
|
+
for (const intf of ctx.interfaces.filter((i) => i.component === comp.id)) {
|
|
26260
|
+
const isIntfDraft = intf.status === "draft" || intf.status === "design";
|
|
26261
|
+
for (const m of intf.methods) {
|
|
26262
|
+
const endpoint = m.endpoint;
|
|
26263
|
+
if (endpoint?.transport !== "HTTP" || endpoint.status === void 0) continue;
|
|
26264
|
+
const status = endpoint.status;
|
|
26265
|
+
const answer = answered(m.returns);
|
|
26266
|
+
const value = answer !== null && !(answer.form === "primitive" && answer.name === "void");
|
|
26267
|
+
const problem = !STANDARD_STATUSES.has(status) ? `${status} is no standard success or redirect code (200-208, 226, 300-308), so no client library names it` : (status === 204 || status === 304) && value ? `${status} carries no body, yet the method answers "${m.returns}" \u2014 the response would drop it` : REDIRECTS.has(status) && value && answer !== null && !isLocation(ctx, answer) ? `${status} is a redirect, answered with a Location header and no body, yet the method answers "${m.returns}" \u2014 a redirecting method answers the location itself (a string), so the response would drop it` : null;
|
|
26268
|
+
if (problem === null) continue;
|
|
26269
|
+
ctx.addIssue(
|
|
26270
|
+
"warning",
|
|
26271
|
+
"ENDPOINT_STATUS_MISMATCH",
|
|
26272
|
+
`Method "${m.name}" on interface "${intf.id}" binds ${endpoint.method ?? "HTTP"} ${endpoint.path ?? ""} with status ${status}: ${problem}. State the status the method's answer allows, or change what it answers.`,
|
|
26273
|
+
intf.id,
|
|
26274
|
+
isDraftCtx || isIntfDraft
|
|
26275
|
+
);
|
|
26276
|
+
}
|
|
26277
|
+
}
|
|
26278
|
+
}
|
|
26279
|
+
}
|
|
25974
26280
|
|
|
25975
26281
|
// src/core/rules/doctrine/non-portal-endpoints.ts
|
|
25976
26282
|
var nonPortalEndpointsRule = {
|
|
@@ -28696,10 +29002,11 @@ function bodyReachable(code, file, symbol) {
|
|
|
28696
29002
|
var methodRealizationRule = {
|
|
28697
29003
|
name: "method-realization",
|
|
28698
29004
|
judges: "code",
|
|
28699
|
-
description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. A method realized by a DECLARATION owes a function BODY as well: at exact grade one must be reachable under the symbol, here or through the imports and republications this file forwards it by, so a signature, an ambient or interface declaration or a plain value binding stops reading as an implementation (METHOD_BODY_NOT_FOUND). Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
|
|
29005
|
+
description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. A method realized by a DECLARATION owes a function BODY as well: at exact grade one must be reachable under the symbol, here or through the imports and republications this file forwards it by, so a signature, an ambient or interface declaration or a plain value binding stops reading as an implementation (METHOD_BODY_NOT_FOUND). Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. A dial turned off is said once per implementation as a notice (REALIZATION_UNCHECKED) naming the methods it covers: the dial is code linkage, outside the approval, so without it one line in an implementation spec would switch the realization checks off \u2014 method, parameters, async, whether narrated calls are found \u2014 with no surface saying so; the doctrine checks still run. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
|
|
28700
29006
|
codes: [
|
|
28701
29007
|
{ code: "UNREALIZED_METHOD", defaultSeverity: "warning", summary: "An L3 contract method has no anchor in its own source file (the method's sourcePath, else the implementation's) at the required conformance tier" },
|
|
28702
|
-
{ code: "METHOD_BODY_NOT_FOUND", defaultSeverity: "warning", summary: "The method symbol IS declared in its own source file, but the file holds no function-like body under it \u2014 a signature, an ambient or interface declaration, a value binding, an imported or re-exported name", carryable: true }
|
|
29008
|
+
{ code: "METHOD_BODY_NOT_FOUND", defaultSeverity: "warning", summary: "The method symbol IS declared in its own source file, but the file holds no function-like body under it \u2014 a signature, an ambient or interface declaration, a value binding, an imported or re-exported name", carryable: true },
|
|
29009
|
+
{ code: "REALIZATION_UNCHECKED", defaultSeverity: "notice", summary: "An implementation's conformance dial is off, on itself or on some of its methods, so whether those methods are realized in its code \u2014 present, taking the contract's parameters, async as declared, making the calls their narratives claim \u2014 is not checked" }
|
|
28703
29010
|
],
|
|
28704
29011
|
check(ctx) {
|
|
28705
29012
|
const code = ctx.codeIndex();
|
|
@@ -28711,6 +29018,19 @@ var methodRealizationRule = {
|
|
|
28711
29018
|
if (ctx.isInChainedSubproject(component.subsystem)) continue;
|
|
28712
29019
|
const draft = ctx.isImplementationDraft(impl);
|
|
28713
29020
|
const specTier = impl.conformance ?? defaultConformanceTier(component);
|
|
29021
|
+
const offMethods = contract.methods.filter((method2) => (impl.methods.find((m) => m.name === method2.name)?.conformance ?? specTier) === "off").map((method2) => method2.name);
|
|
29022
|
+
if (specTier === "off" || offMethods.length > 0) {
|
|
29023
|
+
const covers = specTier === "off" && offMethods.length === contract.methods.length ? `on the implementation itself \u2014 every method of contract "${impl.contract}"` : `on ${offMethods.length} method(s) of contract "${impl.contract}" \u2014 ${offMethods.map((name) => `"${name}"`).join(", ")}`;
|
|
29024
|
+
ctx.addIssue(
|
|
29025
|
+
"notice",
|
|
29026
|
+
"REALIZATION_UNCHECKED",
|
|
29027
|
+
`Realization not checked: conformance off on "${impl.id}", ${covers}. Whether those methods are in the code, take the contract's parameters, are async as declared and make the calls their narratives claim is not compared; the doctrine checks (an unnarrated write, a call the analysis cannot follow, a Portal write shortcut, route coverage) still run. The dial is code linkage, outside the approval \u2014 this notice is where it shows. It is meant for generated or vendored code; drop it (sdd_update_spec unset conformance) to have the code compared again.`,
|
|
29028
|
+
impl.id,
|
|
29029
|
+
draft,
|
|
29030
|
+
void 0,
|
|
29031
|
+
{ at: "conformance" }
|
|
29032
|
+
);
|
|
29033
|
+
}
|
|
28714
29034
|
for (const method2 of contract.methods) {
|
|
28715
29035
|
const methodImpl = impl.methods.find((m) => m.name === method2.name);
|
|
28716
29036
|
const tier = methodImpl?.conformance ?? specTier;
|
|
@@ -30642,7 +30962,7 @@ function bare(name) {
|
|
|
30642
30962
|
}
|
|
30643
30963
|
var HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "res", "response", "reply", "url", "ctx", "context", "next", "event", "socket"]);
|
|
30644
30964
|
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"]);
|
|
30965
|
+
var REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
|
|
30646
30966
|
var PRIMITIVE_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
|
|
30647
30967
|
var aKind = (kind) => kind === "object" ? "an object" : `a ${kind}`;
|
|
30648
30968
|
var fieldKey = (name) => name.toLowerCase().replace(/[_-]/g, "");
|
|
@@ -30691,7 +31011,7 @@ function pairUp(declared, realized, score) {
|
|
|
30691
31011
|
}
|
|
30692
31012
|
return align(best);
|
|
30693
31013
|
}
|
|
30694
|
-
function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
31014
|
+
function judge2(declared, realized, injected, typeAgrees, expected, typescript) {
|
|
30695
31015
|
const declaredNames = new Set(declared.map((param) => bare(param.name)));
|
|
30696
31016
|
const isInjected = (param) => param.name !== void 0 && injected.has(bare(param.name));
|
|
30697
31017
|
let lead = 0;
|
|
@@ -30700,6 +31020,25 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
|
30700
31020
|
while (end > lead && isInjected(realized[end - 1]) && !declaredNames.has(bare(realized[end - 1].name))) end--;
|
|
30701
31021
|
let taken = realized.map((param, at) => ({ param, at })).slice(lead, end);
|
|
30702
31022
|
taken = taken.filter(({ param }) => !(param.name !== void 0 && param.name.startsWith("_") && param.unused === true && !declaredNames.has(bare(param.name))));
|
|
31023
|
+
const writtenAt = new Map(taken.map(({ param, at }) => [unitOf2(param, at), at]));
|
|
31024
|
+
let bundle;
|
|
31025
|
+
for (const entry of taken) {
|
|
31026
|
+
const fields = entry.param.fields;
|
|
31027
|
+
if (entry.param.kind !== "object" || !fields || fields.length === 0) continue;
|
|
31028
|
+
if (declared.some((d) => typeAgrees(d, entry.param) || structural(expected(d)?.fields, entry.param) === "all")) continue;
|
|
31029
|
+
const keys = new Set(fields.map(fieldKey));
|
|
31030
|
+
const takenElsewhere = new Set(taken.filter((other) => other !== entry && other.param.name !== void 0).map((other) => fieldKey(bare(other.param.name))));
|
|
31031
|
+
const covers = new Set(declared.flatMap((d, index) => {
|
|
31032
|
+
const key2 = fieldKey(bare(d.name));
|
|
31033
|
+
return keys.has(key2) && !takenElsewhere.has(key2) ? [index] : [];
|
|
31034
|
+
}));
|
|
31035
|
+
if (covers.size >= 2) {
|
|
31036
|
+
bundle = { entry, covers };
|
|
31037
|
+
break;
|
|
31038
|
+
}
|
|
31039
|
+
}
|
|
31040
|
+
if (bundle) taken = taken.filter((entry) => entry !== bundle?.entry);
|
|
31041
|
+
const pairable = declared.flatMap((param, index) => bundle?.covers.has(index) ? [] : [{ param, index }]);
|
|
30703
31042
|
const score = (d, r) => {
|
|
30704
31043
|
let total = 0;
|
|
30705
31044
|
if (r.name !== void 0 && bare(r.name) === bare(d.name)) total += 4;
|
|
@@ -30711,7 +31050,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
|
30711
31050
|
else if (shape === "none") total -= 2;
|
|
30712
31051
|
return total;
|
|
30713
31052
|
};
|
|
30714
|
-
const pairs = pairUp(
|
|
31053
|
+
const pairs = pairUp(pairable.map((entry) => entry.param), taken.map((entry) => entry.param), score).map(([d, r]) => [pairable[d].index, r]);
|
|
30715
31054
|
const wiringParams = [...realized.slice(0, lead), ...realized.slice(end)];
|
|
30716
31055
|
const found = {
|
|
30717
31056
|
unrealized: /* @__PURE__ */ new Map(),
|
|
@@ -30721,38 +31060,73 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
|
30721
31060
|
transport: /* @__PURE__ */ new Map(),
|
|
30722
31061
|
wiring: realized.slice(0, lead).map((param) => param.name),
|
|
30723
31062
|
trailing: realized.slice(end).map((param) => param.name),
|
|
30724
|
-
carried: /* @__PURE__ */ new Map()
|
|
31063
|
+
carried: /* @__PURE__ */ new Map(),
|
|
31064
|
+
mismatched: /* @__PURE__ */ new Map()
|
|
30725
31065
|
};
|
|
31066
|
+
if (bundle) {
|
|
31067
|
+
const { param: object, at } = bundle.entry;
|
|
31068
|
+
const bundled = declared.filter((_, index) => bundle?.covers.has(index)).map((param) => param.name);
|
|
31069
|
+
const label7 = object.name ? `"${object.name}"` : `parameter #${at + 1}`;
|
|
31070
|
+
for (const name of bundled) {
|
|
31071
|
+
found.unrealized.set(name, { unit: name, told: `"${name}" (the code takes it inside the object ${label7}, where the contract declares it as a parameter of its own)` });
|
|
31072
|
+
}
|
|
31073
|
+
const unit = unitOf2(object, at);
|
|
31074
|
+
found.undeclared.set(unit, { unit, told: `"${unit}" (an object bundling the contract's own parameters ${bundled.join(", ")})` });
|
|
31075
|
+
}
|
|
30726
31076
|
const pairedDeclared = new Set(pairs.map(([d]) => d));
|
|
30727
31077
|
const pairedTaken = new Set(pairs.map(([, r]) => r));
|
|
30728
31078
|
const substituted = /* @__PURE__ */ new Set();
|
|
30729
31079
|
for (const [d, r] of pairs) {
|
|
30730
31080
|
const param = declared[d];
|
|
30731
|
-
const { param: at, at:
|
|
31081
|
+
const { param: at, at: position } = taken[r];
|
|
30732
31082
|
const sameName = at.name !== void 0 && bare(at.name) === bare(param.name);
|
|
30733
31083
|
const want = expected(param);
|
|
30734
31084
|
const agrees = typeAgrees(param, at);
|
|
30735
31085
|
const shape = structural(want?.fields, at);
|
|
30736
31086
|
const handleName = at.name !== void 0 && TRANSPORT_HANDLES.has(bare(at.name).toLowerCase()) && !TRANSPORT_HANDLES.has(bare(param.name).toLowerCase());
|
|
31087
|
+
const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
|
|
30737
31088
|
let instead;
|
|
30738
31089
|
if (!agrees) {
|
|
30739
31090
|
if (!sameName && want?.kinds && at.type && at.kind && !want.kinds.has(at.kind)) instead = aKind(at.kind);
|
|
30740
31091
|
else if (at.transport && want?.fields) instead = "a transport handle";
|
|
30741
31092
|
else if (shape === "none") instead = `an object sharing none of its fields (${(want?.fields ?? []).join(", ")})`;
|
|
30742
|
-
else if (handleName &&
|
|
31093
|
+
else if (handleName && shape !== "all" && !sameKind) instead = "a transport handle";
|
|
30743
31094
|
}
|
|
30744
31095
|
if (instead !== void 0) {
|
|
30745
31096
|
substituted.add(r);
|
|
30746
31097
|
const declares2 = want?.told ?? "an argument of its own";
|
|
30747
31098
|
found.unrealized.set(param.name, {
|
|
30748
31099
|
unit: param.name,
|
|
30749
|
-
told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${
|
|
31100
|
+
told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`}, ${instead}, where the contract declares ${declares2})`
|
|
30750
31101
|
});
|
|
30751
|
-
const unit = unitOf2(at,
|
|
31102
|
+
const unit = unitOf2(at, position);
|
|
30752
31103
|
found.undeclared.set(unit, { unit, told: `"${unit}" (${instead} in the place of the contract's "${param.name}", ${declares2})` });
|
|
30753
31104
|
continue;
|
|
30754
31105
|
}
|
|
30755
|
-
|
|
31106
|
+
if (!agrees && want?.fields) {
|
|
31107
|
+
const opaque = at.type !== void 0 && at.opaque === true || at.type === void 0 && typescript;
|
|
31108
|
+
let shapeTold;
|
|
31109
|
+
if (opaque) {
|
|
31110
|
+
shapeTold = `${at.type !== void 0 ? `typed "${at.type}"` : "with no annotation"}, a type that admits any value and names none of its fields (${want.fields.join(", ")})`;
|
|
31111
|
+
} else if (at.kind === "object" && at.fields && at.fields.length > 0) {
|
|
31112
|
+
const has = new Set(at.fields.map(fieldKey));
|
|
31113
|
+
const declaredKeys = new Set(want.fields.map(fieldKey));
|
|
31114
|
+
const missing = want.fields.filter((field) => !has.has(fieldKey(field)));
|
|
31115
|
+
const added = at.fields.filter((field) => !declaredKeys.has(fieldKey(field)));
|
|
31116
|
+
if (missing.length > 0 && missing.length < want.fields.length || missing.length === 0 && added.length > 0) {
|
|
31117
|
+
shapeTold = `an object ${[
|
|
31118
|
+
...missing.length > 0 ? [`missing ${missing.map((field) => `"${field}"`).join(", ")}`] : [],
|
|
31119
|
+
...added.length > 0 ? [`adding ${added.map((field) => `"${field}"`).join(", ")}`] : []
|
|
31120
|
+
].join(" and ")}`;
|
|
31121
|
+
}
|
|
31122
|
+
}
|
|
31123
|
+
if (shapeTold !== void 0) {
|
|
31124
|
+
found.mismatched.set(param.name, {
|
|
31125
|
+
unit: param.name,
|
|
31126
|
+
told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`} ${shapeTold}, where the contract declares ${want.told})`
|
|
31127
|
+
});
|
|
31128
|
+
}
|
|
31129
|
+
}
|
|
30756
31130
|
if (at.name !== void 0 && !sameName && (agrees || sameKind || shape === "all")) {
|
|
30757
31131
|
const former = (param.previousNames ?? []).some((name) => bare(name) === bare(at.name));
|
|
30758
31132
|
found.renamed.set(`${param.name}\u2192${at.name}`, {
|
|
@@ -30768,9 +31142,12 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
|
30768
31142
|
}
|
|
30769
31143
|
}
|
|
30770
31144
|
const requestReads = new Set(wiringParams.filter((param) => param.name !== void 0 && REQUEST_HANDLES.has(bare(param.name).toLowerCase())).flatMap((param) => (param.reads ?? []).map(bare)));
|
|
31145
|
+
const left = declared.filter((param, d) => !pairedDeclared.has(d) && !bundle?.covers.has(d) && !requestReads.has(bare(param.name)));
|
|
31146
|
+
const records = left.filter((param) => (expected(param)?.fields?.length ?? 0) > 0);
|
|
31147
|
+
const bodyRecord = requestReads.has("body") && records.length === 1 ? records[0].name : void 0;
|
|
30771
31148
|
declared.forEach((param, d) => {
|
|
30772
|
-
if (pairedDeclared.has(d)) return;
|
|
30773
|
-
if (requestReads.has(bare(param.name))) {
|
|
31149
|
+
if (pairedDeclared.has(d) || bundle?.covers.has(d)) return;
|
|
31150
|
+
if (requestReads.has(bare(param.name)) || param.name === bodyRecord) {
|
|
30774
31151
|
found.carried.set(param.name, { unit: param.name, told: param.name });
|
|
30775
31152
|
return;
|
|
30776
31153
|
}
|
|
@@ -30787,8 +31164,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
|
|
|
30787
31164
|
if (pairedTaken.has(r) && !substituted.has(r)) return;
|
|
30788
31165
|
if (HANDLE_NAMES.has(bare(param.name).toLowerCase())) found.transport.set(param.name, { unit: unitOf2(param, at), told: param.name });
|
|
30789
31166
|
});
|
|
30790
|
-
|
|
30791
|
-
found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (position.get(a) ?? 0) - (position.get(b) ?? 0)));
|
|
31167
|
+
found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (writtenAt.get(a) ?? 0) - (writtenAt.get(b) ?? 0)));
|
|
30792
31168
|
return found;
|
|
30793
31169
|
}
|
|
30794
31170
|
function agreed(judgements, reading) {
|
|
@@ -30835,7 +31211,7 @@ function typeKind(type) {
|
|
|
30835
31211
|
var paramConformanceRule = {
|
|
30836
31212
|
name: "param-conformance",
|
|
30837
31213
|
judges: "code",
|
|
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.
|
|
31214
|
+
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. An OBJECT whose properties name two or more of the contract's own parameters BUNDLES them and is set apart first: each parameter it bundles is unrealized (the code takes it inside the object) and the object undeclared, so `cancelOrder(input: { orderId, customerId })` realizing `cancelOrder(orderId, customerId)` names both, never one half by position. 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, a MEMBER's through the name its own type spec gives it, read live from the family with its kind and fields \u2014 so where a parameter's type lives never decides whether it is compared \u2014 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 data 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. 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 unless its type agrees with the declared one or holds every field of the declared record \u2014 however the handle is typed, a hand-written `type Req = { params: \u2026 }` included: `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is a handler's shape, not the contract's. Where the contract's type is a record, a paired parameter whose type cannot hold it fails closed, as an opaque receiver does (PARAM_TYPE_MISMATCH): an OPAQUE type \u2014 any, unknown, object, {}, an index signature alone (`Record<string, unknown>`), or no annotation at all in a TypeScript file \u2014 names no field the analysis could compare, and an object sharing some of the record's fields but not all of them, or adding fields the record does not declare, is another shape; the finding names the fields missing and the fields added, so `planRoute(request: any)` and `planRoute(request: { stops: unknown[]; secret: string })` realizing `planRoute(request: plan_request)` are never silent pairings. 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, url) by its own name is realized through that handle \u2014 `req.params.code`, `req.query.limit`, `req.body.code`, `url.searchParams.get('limit')`, a destructuring at any depth, a local the handle's value flows into (`const url = new URL(req.url, base)`), and what a helper the handle is passed to reads off it or its result yields, where the helper is written in the same file or in a file no implementation claims (`const { orderId } = pathParams(req)`) \u2014 and the one RECORD parameter left unpaired is realized by the request's body read whole (`req.body`). An injected name none of the implementation's realizing functions read in this run takes is said as a NOTICE (UNUSED_INJECTED_PARAM): an injection is code linkage outside the approval, and one nothing takes hides nothing, since no function takes a parameter it could set aside \u2014 so it never fails a gate. 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.",
|
|
30839
31215
|
codes: [
|
|
30840
31216
|
{
|
|
30841
31217
|
code: "UNREALIZED_PARAM",
|
|
@@ -30859,8 +31235,14 @@ var paramConformanceRule = {
|
|
|
30859
31235
|
},
|
|
30860
31236
|
{
|
|
30861
31237
|
code: "UNUSED_INJECTED_PARAM",
|
|
31238
|
+
defaultSeverity: "notice",
|
|
31239
|
+
summary: "An implementation declares an injected parameter that none of its realizing functions takes \u2014 stale code linkage, said so it can be dropped; nothing takes it, so it hides nothing and never fails a gate",
|
|
31240
|
+
carryable: true
|
|
31241
|
+
},
|
|
31242
|
+
{
|
|
31243
|
+
code: "PARAM_TYPE_MISMATCH",
|
|
30862
31244
|
defaultSeverity: "warning",
|
|
30863
|
-
summary: "
|
|
31245
|
+
summary: "A contract parameter declared as a record is realized by a parameter whose type cannot hold it \u2014 opaque (any, unknown, an index signature) or an object missing the record's fields or adding its own \u2014 so what a caller hands over is never compared with what the design declares",
|
|
30864
31246
|
carryable: true
|
|
30865
31247
|
},
|
|
30866
31248
|
{
|
|
@@ -30898,7 +31280,28 @@ var paramConformanceRule = {
|
|
|
30898
31280
|
typeNamed.set(`${pin2.alias}::${exported.id}`, type);
|
|
30899
31281
|
}
|
|
30900
31282
|
}
|
|
30901
|
-
const
|
|
31283
|
+
const family = ctx.projectFamily?.nodes ?? [];
|
|
31284
|
+
const root = family.find((node) => node.namespace === "");
|
|
31285
|
+
const memberTypes = /* @__PURE__ */ new Map();
|
|
31286
|
+
const lastOf = (id) => id.split(/::|\./).pop() ?? id;
|
|
31287
|
+
const memberType = (ref) => {
|
|
31288
|
+
const cut = ref.indexOf("::");
|
|
31289
|
+
if (cut <= 0) return void 0;
|
|
31290
|
+
if (memberTypes.has(ref)) return memberTypes.get(ref) ?? void 0;
|
|
31291
|
+
const segment = ref.slice(0, cut);
|
|
31292
|
+
const local = lastOf(ref.slice(cut + 2));
|
|
31293
|
+
const key2 = root?.aliases.get(segment) ?? family.find((node2) => node2.aliases.has(segment))?.aliases.get(segment) ?? segment;
|
|
31294
|
+
const node = family.find((n) => n.namespace !== "" && (n.namespace === key2 || n.mountAlias === segment || n.id === segment));
|
|
31295
|
+
const found = node ? ctx.types.find((t) => t.id === `${node.namespace}::${local}`) ?? ctx.types.find((t) => t.id.startsWith(`${node.namespace}::`) && lastOf(t.id) === local) : void 0;
|
|
31296
|
+
memberTypes.set(ref, found ?? null);
|
|
31297
|
+
return found;
|
|
31298
|
+
};
|
|
31299
|
+
const typeOf = (name) => typeNamed.get(name) ?? memberType(name);
|
|
31300
|
+
const externalCodeName = (ref) => {
|
|
31301
|
+
const member = memberType(ref);
|
|
31302
|
+
if (member) return member.symbol ?? member.name;
|
|
31303
|
+
return (ref.split("::").pop() ?? ref).split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
31304
|
+
};
|
|
30902
31305
|
const typeAgreesIn = (dialect) => (declared, realized) => {
|
|
30903
31306
|
if (!realized.type || !dialect) return false;
|
|
30904
31307
|
const stated = parseTypePosition(declared.type, "param", !!declared.optional);
|
|
@@ -30922,9 +31325,9 @@ var paramConformanceRule = {
|
|
|
30922
31325
|
};
|
|
30923
31326
|
if (expression.name && loose[expression.name]) return { kinds: new Set(loose[expression.name]), loose: true, told: `a ${expression.name}` };
|
|
30924
31327
|
}
|
|
30925
|
-
const kind = expressionKind(expression, (name) => typeKind(
|
|
31328
|
+
const kind = expressionKind(expression, (name) => typeKind(typeOf(name)));
|
|
30926
31329
|
if (expression.form === "named" && expression.name) {
|
|
30927
|
-
const type =
|
|
31330
|
+
const type = typeOf(expression.name);
|
|
30928
31331
|
const fields = (type?.fields ?? []).map((field) => field && typeof field === "object" ? field.name : void 0).filter((name) => typeof name === "string");
|
|
30929
31332
|
if (fields.length > 0 && kind === "object") return { kinds: /* @__PURE__ */ new Set(["object"]), fields, told: `the record "${expression.name}"` };
|
|
30930
31333
|
}
|
|
@@ -30951,7 +31354,8 @@ var paramConformanceRule = {
|
|
|
30951
31354
|
if (declared.length === 0) continue;
|
|
30952
31355
|
const injected = new Set((implementation.injectedParams ?? []).map(bare));
|
|
30953
31356
|
const typeAgrees = typeAgreesIn(dialectOf(facts));
|
|
30954
|
-
const
|
|
31357
|
+
const typescript = facts.language === "typescript";
|
|
31358
|
+
const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected, typescript));
|
|
30955
31359
|
const subject = candidates.length > 1 ? `every function called "${symbol}" in "${file}"` : `the function "${symbol}" in "${file}"`;
|
|
30956
31360
|
const opening = subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
30957
31361
|
const transport = agreed(judgements, "transport").map((found) => found.told);
|
|
@@ -30982,6 +31386,18 @@ var paramConformanceRule = {
|
|
|
30982
31386
|
{ at: method2.name, covers: undeclared.map((found) => found.unit) }
|
|
30983
31387
|
);
|
|
30984
31388
|
}
|
|
31389
|
+
const mismatched = agreed(judgements, "mismatched");
|
|
31390
|
+
if (mismatched.length > 0) {
|
|
31391
|
+
ctx.addIssue(
|
|
31392
|
+
"warning",
|
|
31393
|
+
"PARAM_TYPE_MISMATCH",
|
|
31394
|
+
`Method "${method2.name}" of contract "${implementation.contract}" declares ${mismatched.length} record parameter(s) ${subject} takes under a type that cannot hold the record \u2014 ${mismatched.map((found) => found.told).join("; ")}. Whatever a caller hands over there is never compared with what the design declares \u2014 an opaque type names no field to compare, and an object missing the record's fields or adding its own is another shape, the one a credential rides in unread \u2014 so the check fails closed on it, as it does on an opaque receiver. Type the parameter as the record (its code name), or declare on the contract the shape the code really takes.`,
|
|
31395
|
+
implementation.id,
|
|
31396
|
+
draftContext4,
|
|
31397
|
+
void 0,
|
|
31398
|
+
{ at: method2.name, covers: mismatched.map((found) => found.unit) }
|
|
31399
|
+
);
|
|
31400
|
+
}
|
|
30985
31401
|
const renamed2 = agreed(judgements, "renamed");
|
|
30986
31402
|
if (renamed2.length > 0) {
|
|
30987
31403
|
ctx.addIssue(
|
|
@@ -31014,9 +31430,9 @@ var paramConformanceRule = {
|
|
|
31014
31430
|
const stale = injected.filter((name) => !taken.has(bare(name)));
|
|
31015
31431
|
if (stale.length === 0) continue;
|
|
31016
31432
|
ctx.addIssue(
|
|
31017
|
-
"
|
|
31433
|
+
"notice",
|
|
31018
31434
|
"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
|
|
31435
|
+
`Implementation "${implementation.id}" declares ${stale.length} injected parameter(s) none of its realizing functions takes \u2014 ${stale.map((name) => `"${name}"`).join(", ")}. An injection is code linkage, outside the approval, and one no function takes hides nothing: there is no parameter of that name for it to set aside, so it never fails a gate. It is stale linkage all the same \u2014 drop it from injectedParams (sdd_update_spec, no re-lock), or take it in the handler that is handed it.`,
|
|
31020
31436
|
implementation.id,
|
|
31021
31437
|
ctx.isImplementationDraft(implementation),
|
|
31022
31438
|
void 0,
|
|
@@ -31118,7 +31534,14 @@ function memberSurface(ctx, node, alias) {
|
|
|
31118
31534
|
...m,
|
|
31119
31535
|
formerly: (m.previousNames ?? []).map(lastSegment4)
|
|
31120
31536
|
}));
|
|
31121
|
-
|
|
31537
|
+
const approved = ctx.memberApprovedSurfaces?.[alias]?.interfaces.find((x) => x.id === e.publicName);
|
|
31538
|
+
const kept = new Set(methods.flatMap((m) => [m.name, ...m.formerly ?? []]).map(nameKey));
|
|
31539
|
+
const retired = (approved?.methods ?? []).map((m) => m.name).filter((n) => !kept.has(nameKey(n)));
|
|
31540
|
+
entries.push({
|
|
31541
|
+
alias,
|
|
31542
|
+
entry: { id: e.publicName, name: e.publicName, component: e.component, methods, ...retired.length ? { retired } : {} },
|
|
31543
|
+
...retired.length ? { approved: true } : {}
|
|
31544
|
+
});
|
|
31122
31545
|
for (const m of methods) pending3.push(...methodTypeRefs(m));
|
|
31123
31546
|
} else if (e.kind === "type" && e.typeDef) {
|
|
31124
31547
|
const type = memberType(e.typeDef);
|
|
@@ -31213,7 +31636,7 @@ function methodDrift(where, params, entry, reportMissing, method2) {
|
|
|
31213
31636
|
if (current2) return paramDrift(where, params, current2);
|
|
31214
31637
|
const renamed2 = entry.entry.methods.find((m) => (m.formerly ?? []).some((f) => nameKey(f) === key2));
|
|
31215
31638
|
if (renamed2) return [`"${method2 ?? where}" was renamed to "${renamed2.name}" in ${entry.alias}::${entry.entry.id} \u2014 follow the rename`];
|
|
31216
|
-
if (retiredIn(entry, key2)) return [`"${method2 ?? where}" was removed from ${entry.alias}::${entry.entry.id} (an earlier pin held it) \u2014 drop it from the binding, and the calls that use it`];
|
|
31639
|
+
if (retiredIn(entry, key2)) return [`"${method2 ?? where}" was removed from ${entry.alias}::${entry.entry.id} (${entry.approved ? "the member's approved design held it" : "an earlier pin held it"}) \u2014 drop it from the binding, and the calls that use it`];
|
|
31217
31640
|
return reportMissing ? [`"${method2 ?? where}" is not exported by ${entry.alias}::${entry.entry.id} (it holds ${entry.entry.methods.map((m) => `"${m.name}"`).join(", ") || "no methods"})`] : [];
|
|
31218
31641
|
}
|
|
31219
31642
|
function formerType(name, types) {
|
|
@@ -31327,6 +31750,16 @@ function reachOf(ctx, segments) {
|
|
|
31327
31750
|
const members = [];
|
|
31328
31751
|
const unpinned2 = [];
|
|
31329
31752
|
const externals = new Set((boundRoot(ctx)?.imports ?? []).filter((i) => i.section === "externals").map((i) => i.alias));
|
|
31753
|
+
const ownId = boundRoot(ctx)?.id;
|
|
31754
|
+
if (ownId !== void 0 && segments.has(ownId)) {
|
|
31755
|
+
segments = new Set(segments);
|
|
31756
|
+
for (const table of (ctx.exportTables ?? []).filter((x) => x.level === "project" && !(ctx.projectFamily?.nodes ?? []).some((n) => n.namespace !== "" && n.namespace === x.owner))) {
|
|
31757
|
+
for (const e of table.entries) {
|
|
31758
|
+
const target = e.typeDef ?? e.component ?? "";
|
|
31759
|
+
if (target.includes("::")) segments.add(target.slice(0, target.indexOf("::")));
|
|
31760
|
+
}
|
|
31761
|
+
}
|
|
31762
|
+
}
|
|
31330
31763
|
for (const segment of segments) {
|
|
31331
31764
|
if (pins.some((p) => p.alias === segment)) continue;
|
|
31332
31765
|
const node = memberNode(ctx, segment);
|
|
@@ -31451,7 +31884,7 @@ function answers(route2, endpoint) {
|
|
|
31451
31884
|
var routeCoverageRule = {
|
|
31452
31885
|
name: "route-coverage",
|
|
31453
31886
|
judges: "code",
|
|
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.
|
|
31887
|
+
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 \u2014 the same entry or table in the same file, however each linkage spells it \u2014 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; an array may spread other tables and an object other objects, `[...ingestRoutes, ...statsRoutes]`, each followed through the type checker to the table it names in any module), 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. Route coverage is never silently off: a router that yields no route in either idiom, or a linkage naming a declaration that holds neither a function body nor a route table this analysis reads, is reported as unread; and a Portal binding HTTP endpoints whose implementation names no router while its code is read at exact grade is said as a notice (ROUTER_UNDECLARED) naming `router:` and the route-bearing names its files hold \u2014 a check that cannot see a router must say so rather than stay quiet.",
|
|
31455
31888
|
codes: [
|
|
31456
31889
|
{
|
|
31457
31890
|
code: "UNDECLARED_ROUTE",
|
|
@@ -31472,17 +31905,50 @@ var routeCoverageRule = {
|
|
|
31472
31905
|
defaultSeverity: "warning",
|
|
31473
31906
|
summary: "A Portal's router entry yields no route this analysis can read, so its routes were not checked against the contract at all",
|
|
31474
31907
|
carryable: true
|
|
31908
|
+
},
|
|
31909
|
+
{
|
|
31910
|
+
code: "ROUTER_UNDECLARED",
|
|
31911
|
+
defaultSeverity: "notice",
|
|
31912
|
+
summary: "A Portal binds HTTP endpoints but its implementation names no router (`router:`), so none of the routes its code serves were checked against its contract"
|
|
31475
31913
|
}
|
|
31476
31914
|
],
|
|
31477
31915
|
check(ctx) {
|
|
31478
31916
|
const code = ctx.codeIndex();
|
|
31479
31917
|
const realization = ctx.realizationIndex();
|
|
31480
31918
|
const groups = /* @__PURE__ */ new Map();
|
|
31919
|
+
const httpEndpoints = (portal) => ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
|
|
31920
|
+
const endpoint = method2.endpoint;
|
|
31921
|
+
if (endpoint?.transport !== "HTTP") return [];
|
|
31922
|
+
return [{
|
|
31923
|
+
verb: endpoint.method.toUpperCase(),
|
|
31924
|
+
segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
|
|
31925
|
+
key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
|
|
31926
|
+
}];
|
|
31927
|
+
});
|
|
31928
|
+
const exactAt = (file) => {
|
|
31929
|
+
const facts = code.factsAt(file);
|
|
31930
|
+
return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
|
|
31931
|
+
};
|
|
31481
31932
|
for (const portal of ctx.components) {
|
|
31482
31933
|
if (portal.componentType !== "Portal") continue;
|
|
31483
31934
|
for (const impl of realization.implementationsOf(portal.id)) {
|
|
31484
31935
|
const via = impl.router;
|
|
31485
|
-
if (!via)
|
|
31936
|
+
if (!via) {
|
|
31937
|
+
const endpoints2 = httpEndpoints(portal);
|
|
31938
|
+
const files2 = realization.filesOf(portal.id).map(pathKey).filter(exactAt);
|
|
31939
|
+
if (endpoints2.length === 0 || files2.length === 0) continue;
|
|
31940
|
+
const held = [...new Set(files2.flatMap((file) => Object.entries(code.factsAt(file)?.functionRoutes ?? {}).filter(([, routes]) => routes.length > 0).map(([name]) => `${name} (${file})`)))].sort();
|
|
31941
|
+
ctx.addIssue(
|
|
31942
|
+
"notice",
|
|
31943
|
+
"ROUTER_UNDECLARED",
|
|
31944
|
+
`Portal "${portal.id}" binds ${endpoints2.length} HTTP endpoint(s), but its implementation "${impl.id}" names no router (\`router:\`), so no route its code serves was checked against its contract \u2014 not a route the design never promised, not an endpoint no route answers. Name the router in the implementation's \`router:\` (code linkage, no re-lock): an entry its own files export, \`<module>#<name>\` for an entry or a route table another module holds, or a module alone. ` + (held.length > 0 ? `Its files hold what reads as a router or a route table: ${held.join(", ")}.` : "Its files hold no function or table that reads as a router \u2014 when another module routes this Portal, name that one."),
|
|
31945
|
+
impl.id,
|
|
31946
|
+
ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
|
|
31947
|
+
void 0,
|
|
31948
|
+
{ at: "router" }
|
|
31949
|
+
);
|
|
31950
|
+
continue;
|
|
31951
|
+
}
|
|
31486
31952
|
const linkage = routerLinkage(via);
|
|
31487
31953
|
const files = linkage.file ? [pathKey(linkage.file)] : realization.filesOf(portal.id).map(pathKey);
|
|
31488
31954
|
const exact = files.filter((file) => {
|
|
@@ -31494,7 +31960,22 @@ var routeCoverageRule = {
|
|
|
31494
31960
|
return !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, name) || !!facts.functionRoutes && Object.prototype.hasOwnProperty.call(facts.functionRoutes, name);
|
|
31495
31961
|
};
|
|
31496
31962
|
const holders = linkage.name !== void 0 ? exact.filter((file) => holds(file, linkage.name)) : exact;
|
|
31497
|
-
if (holders.length === 0)
|
|
31963
|
+
if (holders.length === 0) {
|
|
31964
|
+
const name = linkage.name;
|
|
31965
|
+
const declaring = exact.filter((file) => code.declarationsAt(file).has(name));
|
|
31966
|
+
if (declaring.length > 0) {
|
|
31967
|
+
ctx.addIssue(
|
|
31968
|
+
"warning",
|
|
31969
|
+
"UNREADABLE_ROUTER",
|
|
31970
|
+
`Portal "${portal.id}" names its router "${via}" (in ${declaring.map((file) => `"${file}"`).join(", ")}), but "${name}" there holds neither a function body nor a route table this analysis reads \u2014 so none of its routes were checked against the contract at all. A table must settle in every entry (a spread included: each spread must name a table that settles), and a router must be a function the file declares. Write it so, or carry this finding with the reason it cannot be.`,
|
|
31971
|
+
impl.id,
|
|
31972
|
+
ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
|
|
31973
|
+
void 0,
|
|
31974
|
+
{ at: via }
|
|
31975
|
+
);
|
|
31976
|
+
}
|
|
31977
|
+
continue;
|
|
31978
|
+
}
|
|
31498
31979
|
const prefixes = /* @__PURE__ */ new Set();
|
|
31499
31980
|
const read2 = holders.flatMap((file) => {
|
|
31500
31981
|
const facts = code.factsAt(file);
|
|
@@ -31513,15 +31994,7 @@ var routeCoverageRule = {
|
|
|
31513
31994
|
}
|
|
31514
31995
|
return [...names].flatMap((name) => Object.prototype.hasOwnProperty.call(routes, name) ? routes[name] : []);
|
|
31515
31996
|
});
|
|
31516
|
-
const endpoints =
|
|
31517
|
-
const endpoint = method2.endpoint;
|
|
31518
|
-
if (endpoint?.transport !== "HTTP") return [];
|
|
31519
|
-
return [{
|
|
31520
|
-
verb: endpoint.method.toUpperCase(),
|
|
31521
|
-
segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
|
|
31522
|
-
key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
|
|
31523
|
-
}];
|
|
31524
|
-
});
|
|
31997
|
+
const endpoints = httpEndpoints(portal);
|
|
31525
31998
|
const heads = [...new Set(endpoints.map((endpoint) => endpoint.segments[0]).filter((head2) => !!head2 && head2 !== "*"))].sort();
|
|
31526
31999
|
const router = {
|
|
31527
32000
|
portal,
|
|
@@ -31535,7 +32008,7 @@ var routeCoverageRule = {
|
|
|
31535
32008
|
draftContext: ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
|
|
31536
32009
|
...prefixes.size === 1 ? { prefix: [...prefixes][0] } : {}
|
|
31537
32010
|
};
|
|
31538
|
-
const key2 =
|
|
32011
|
+
const key2 = `${[...holders].sort().join("|")}#${linkage.name ?? "*"}`;
|
|
31539
32012
|
groups.set(key2, [...groups.get(key2) ?? [], router]);
|
|
31540
32013
|
}
|
|
31541
32014
|
}
|
|
@@ -32191,15 +32664,31 @@ var DATA_LAYER = /* @__PURE__ */ new Set(["Adapter", "Store", "Registry", "Index
|
|
|
32191
32664
|
var technologyBindingRule = {
|
|
32192
32665
|
name: "technology-binding",
|
|
32193
32666
|
judges: "design",
|
|
32194
|
-
description: "Only the stereotypes that sit at a technology seam (Adapter/Store/Registry/Index,
|
|
32667
|
+
description: "Only the stereotypes that sit at a technology seam (Adapter/Store/Registry/Index, Observer \u2014 the edge block that subscribes to a messaging technology \u2014 and a Portal, the transport seam, which binds the framework that serves its endpoints) should bind a technology directly: an L4 that declares `technologies` on logic has no swap seam. A Portal binding a technology an Adapter, Store, Registry, Index or Observer of the tree also binds is reaching past its own seam into theirs, and is reported. No hardcoded vendor lists \u2014 only declared tokens are policed, so the rule never fires on a tree that doesn't opt in.",
|
|
32195
32668
|
codes: [
|
|
32196
|
-
{ code: "TECH_ON_LOGIC_COMPONENT", defaultSeverity: "warning", summary: "Technology bound by a
|
|
32669
|
+
{ code: "TECH_ON_LOGIC_COMPONENT", defaultSeverity: "warning", summary: "Technology bound by a stereotype that sits at no technology seam, or a Portal binding a technology a data-layer stereotype binds" }
|
|
32197
32670
|
],
|
|
32198
32671
|
check(ctx) {
|
|
32672
|
+
const componentOf = (contract) => {
|
|
32673
|
+
const intf = ctx.interfaceMap.get(contract);
|
|
32674
|
+
return intf ? ctx.componentMap.get(intf.component) : void 0;
|
|
32675
|
+
};
|
|
32676
|
+
const dataLayer = new Set(ctx.implementations.filter((impl) => DATA_LAYER.has(componentOf(impl.contract)?.componentType ?? "")).flatMap((impl) => (impl.technologies ?? []).map((t) => technologyName(t).toLowerCase())));
|
|
32199
32677
|
for (const impl of ctx.implementations) {
|
|
32200
|
-
const
|
|
32201
|
-
const comp = intf ? ctx.componentMap.get(intf.component) : void 0;
|
|
32678
|
+
const comp = componentOf(impl.contract);
|
|
32202
32679
|
if (!impl.technologies?.length || !comp || DATA_LAYER.has(comp.componentType)) continue;
|
|
32680
|
+
if (comp.componentType === "Portal") {
|
|
32681
|
+
const reached = impl.technologies.map(technologyName).filter((name) => dataLayer.has(name.toLowerCase()));
|
|
32682
|
+
if (reached.length === 0) continue;
|
|
32683
|
+
ctx.addIssue(
|
|
32684
|
+
"warning",
|
|
32685
|
+
"TECH_ON_LOGIC_COMPONENT",
|
|
32686
|
+
`Implementation "${impl.id}" binds ${reached.join(", ")} on Portal "${comp.id}", a technology a data-layer component of this tree binds too. A Portal is the transport seam: it binds the framework that serves its endpoints, and reaches the data layer only through the components that bind it \u2014 drop ${reached.length === 1 ? "it" : "them"} from this implementation's technologies.`,
|
|
32687
|
+
impl.id,
|
|
32688
|
+
ctx.isImplementationDraft(impl)
|
|
32689
|
+
);
|
|
32690
|
+
continue;
|
|
32691
|
+
}
|
|
32203
32692
|
ctx.addIssue(
|
|
32204
32693
|
"warning",
|
|
32205
32694
|
"TECH_ON_LOGIC_COMPONENT",
|
|
@@ -33755,11 +34244,26 @@ function tableEntry(ts, entry, initializerOf, literalOf) {
|
|
|
33755
34244
|
const route2 = keyedPath ?? (paths.length === 1 ? paths[0] : void 0);
|
|
33756
34245
|
return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true, table: true } : void 0;
|
|
33757
34246
|
}
|
|
33758
|
-
function tableRoutes(ts, value, initializerOf, literalOf) {
|
|
34247
|
+
function tableRoutes(ts, value, initializerOf, literalOf, depth = 0) {
|
|
34248
|
+
if (depth > SETTLE_DEPTH) return [];
|
|
33759
34249
|
const at = bareExpression(ts, value);
|
|
33760
34250
|
const out = [];
|
|
34251
|
+
const spread = (expression) => {
|
|
34252
|
+
const named2 = bareExpression(ts, expression);
|
|
34253
|
+
if (!ts.isIdentifier(named2) && !ts.isPropertyAccessExpression(named2)) return void 0;
|
|
34254
|
+
const initializer = initializerOf(named2);
|
|
34255
|
+
if (!initializer) return void 0;
|
|
34256
|
+
const inner2 = tableRoutes(ts, initializer, initializerOf, literalOf, depth + 1);
|
|
34257
|
+
return inner2.length > 0 ? inner2 : void 0;
|
|
34258
|
+
};
|
|
33761
34259
|
if (ts.isArrayLiteralExpression(at)) {
|
|
33762
34260
|
for (const element of at.elements) {
|
|
34261
|
+
if (ts.isSpreadElement(element)) {
|
|
34262
|
+
const routes = spread(element.expression);
|
|
34263
|
+
if (!routes) return [];
|
|
34264
|
+
out.push(...routes);
|
|
34265
|
+
continue;
|
|
34266
|
+
}
|
|
33763
34267
|
const entry = bareExpression(ts, element);
|
|
33764
34268
|
if (!ts.isObjectLiteralExpression(entry)) return [];
|
|
33765
34269
|
const route2 = tableEntry(ts, entry, initializerOf, literalOf);
|
|
@@ -33770,6 +34274,12 @@ function tableRoutes(ts, value, initializerOf, literalOf) {
|
|
|
33770
34274
|
}
|
|
33771
34275
|
if (ts.isObjectLiteralExpression(at)) {
|
|
33772
34276
|
for (const property of at.properties) {
|
|
34277
|
+
if (ts.isSpreadAssignment(property)) {
|
|
34278
|
+
const routes = spread(property.expression);
|
|
34279
|
+
if (!routes) return [];
|
|
34280
|
+
out.push(...routes);
|
|
34281
|
+
continue;
|
|
34282
|
+
}
|
|
33773
34283
|
const key2 = !property.name ? void 0 : ts.isComputedPropertyName(property.name) ? settleString(ts, property.name.expression, initializerOf, literalOf) : propertyKeyText(ts, property.name);
|
|
33774
34284
|
const match2 = key2 ? /^([A-Za-z]+)\s+(\/\S*)$/.exec(key2) : null;
|
|
33775
34285
|
if (!match2 || !HTTP_VERBS.has(match2[1].toUpperCase())) return [];
|
|
@@ -33869,7 +34379,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
|
|
|
33869
34379
|
return void 0;
|
|
33870
34380
|
};
|
|
33871
34381
|
const prefix = strippedPrefix(ts, fn.body, initializerOf, literalOf);
|
|
33872
|
-
const
|
|
34382
|
+
const routeOf2 = (conjuncts) => {
|
|
33873
34383
|
let verb;
|
|
33874
34384
|
let length;
|
|
33875
34385
|
const fixed = /* @__PURE__ */ new Map();
|
|
@@ -33890,7 +34400,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
|
|
|
33890
34400
|
if (namedFunctionNameOf(ts, node) !== void 0) return;
|
|
33891
34401
|
if (ts.isIfStatement(node)) {
|
|
33892
34402
|
const guarded2 = [...enclosing2, ...conjunctsOf(node.expression)];
|
|
33893
|
-
const route2 =
|
|
34403
|
+
const route2 = routeOf2(guarded2);
|
|
33894
34404
|
if (route2) routes.set(routeKey2(route2), route2);
|
|
33895
34405
|
walk2(node.thenStatement, guarded2);
|
|
33896
34406
|
if (node.elseStatement) walk2(node.elseStatement, enclosing2);
|
|
@@ -33924,6 +34434,141 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
|
|
|
33924
34434
|
returns(fn.body);
|
|
33925
34435
|
return routes;
|
|
33926
34436
|
}
|
|
34437
|
+
var READS_CAP = 64;
|
|
34438
|
+
var READS_DEPTH = 3;
|
|
34439
|
+
var FLOW_CALLS = /* @__PURE__ */ new Set(["URL", "URLSearchParams", "parse"]);
|
|
34440
|
+
function bindingKeys(ts, pattern, out) {
|
|
34441
|
+
if (!ts.isObjectBindingPattern(pattern)) return;
|
|
34442
|
+
for (const element of pattern.elements) {
|
|
34443
|
+
const key2 = element.propertyName ?? element.name;
|
|
34444
|
+
if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
|
|
34445
|
+
if (!ts.isIdentifier(element.name)) bindingKeys(ts, element.name, out);
|
|
34446
|
+
}
|
|
34447
|
+
}
|
|
34448
|
+
function calleeName(ts, expression) {
|
|
34449
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
34450
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
34451
|
+
return void 0;
|
|
34452
|
+
}
|
|
34453
|
+
function helperParam(ts, fn, index) {
|
|
34454
|
+
const body = fn.body;
|
|
34455
|
+
const parameter = (fn.parameters ?? [])[index];
|
|
34456
|
+
if (!body || !parameter || parameter.dotDotDotToken) return void 0;
|
|
34457
|
+
if (ts.isIdentifier(parameter.name)) return { body, name: parameter.name.text };
|
|
34458
|
+
const keys = /* @__PURE__ */ new Set();
|
|
34459
|
+
bindingKeys(ts, parameter.name, keys);
|
|
34460
|
+
return keys.size > 0 ? { body, keys: [...keys] } : void 0;
|
|
34461
|
+
}
|
|
34462
|
+
function handleReads(ts, body, name, helperOf, out = /* @__PURE__ */ new Set(), depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
34463
|
+
const visitKey = `${body.getSourceFile().fileName}:${body.pos}:${body.end}:${name}`;
|
|
34464
|
+
if (seen.has(visitKey)) return out;
|
|
34465
|
+
seen.add(visitKey);
|
|
34466
|
+
const deeper = (scope, local) => {
|
|
34467
|
+
if (depth < READS_DEPTH) handleReads(ts, scope, local, helperOf, out, depth + 1, seen);
|
|
34468
|
+
};
|
|
34469
|
+
const follow = (node) => {
|
|
34470
|
+
let at = node;
|
|
34471
|
+
for (; ; ) {
|
|
34472
|
+
const up = at.parent;
|
|
34473
|
+
if (!up || out.size >= READS_CAP) return;
|
|
34474
|
+
if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up) || ts.isTypeAssertionExpression(up) || ts.isAwaitExpression(up) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(up)) {
|
|
34475
|
+
at = up;
|
|
34476
|
+
continue;
|
|
34477
|
+
}
|
|
34478
|
+
if (ts.isBinaryExpression(up) && (up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || up.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
|
|
34479
|
+
at = up;
|
|
34480
|
+
continue;
|
|
34481
|
+
}
|
|
34482
|
+
if (ts.isPropertyAccessExpression(up) && up.expression === at) {
|
|
34483
|
+
out.add(up.name.text);
|
|
34484
|
+
at = up;
|
|
34485
|
+
continue;
|
|
34486
|
+
}
|
|
34487
|
+
if (ts.isElementAccessExpression(up) && up.expression === at) {
|
|
34488
|
+
if (!ts.isStringLiteralLike(up.argumentExpression)) return;
|
|
34489
|
+
out.add(up.argumentExpression.text);
|
|
34490
|
+
at = up;
|
|
34491
|
+
continue;
|
|
34492
|
+
}
|
|
34493
|
+
if (ts.isCallExpression(up) && up.expression === at) {
|
|
34494
|
+
const [first] = up.arguments;
|
|
34495
|
+
if (first && ts.isStringLiteralLike(first)) {
|
|
34496
|
+
out.add(first.text);
|
|
34497
|
+
return;
|
|
34498
|
+
}
|
|
34499
|
+
if (up.arguments.length > 0) return;
|
|
34500
|
+
at = up;
|
|
34501
|
+
continue;
|
|
34502
|
+
}
|
|
34503
|
+
if ((ts.isCallExpression(up) || ts.isNewExpression(up)) && up.expression !== at) {
|
|
34504
|
+
const args = up.arguments ?? [];
|
|
34505
|
+
const index = args.indexOf(at);
|
|
34506
|
+
if (index < 0) return;
|
|
34507
|
+
const helper = ts.isCallExpression(up) && depth < READS_DEPTH ? helperOf?.(up, index) : void 0;
|
|
34508
|
+
if (helper) {
|
|
34509
|
+
for (const key2 of helper.keys ?? []) out.add(key2);
|
|
34510
|
+
if (helper.name) deeper(helper.body, helper.name);
|
|
34511
|
+
at = up;
|
|
34512
|
+
continue;
|
|
34513
|
+
}
|
|
34514
|
+
const callee = calleeName(ts, up.expression);
|
|
34515
|
+
if (callee !== void 0 && FLOW_CALLS.has(callee)) {
|
|
34516
|
+
at = up;
|
|
34517
|
+
continue;
|
|
34518
|
+
}
|
|
34519
|
+
return;
|
|
34520
|
+
}
|
|
34521
|
+
if (ts.isVariableDeclaration(up) && up.initializer === at) {
|
|
34522
|
+
if (ts.isIdentifier(up.name)) {
|
|
34523
|
+
if (up.name.text !== name) deeper(body, up.name.text);
|
|
34524
|
+
} else {
|
|
34525
|
+
bindingKeys(ts, up.name, out);
|
|
34526
|
+
}
|
|
34527
|
+
}
|
|
34528
|
+
return;
|
|
34529
|
+
}
|
|
34530
|
+
};
|
|
34531
|
+
const visit = (node) => {
|
|
34532
|
+
if (out.size >= READS_CAP) return;
|
|
34533
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
34534
|
+
const parent = node.parent;
|
|
34535
|
+
const declares2 = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node || (ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isBindingElement(parent)) && parent.name === node || ts.isBindingElement(parent) && parent.propertyName === node);
|
|
34536
|
+
if (!declares2) follow(node);
|
|
34537
|
+
}
|
|
34538
|
+
ts.forEachChild(node, visit);
|
|
34539
|
+
};
|
|
34540
|
+
visit(body);
|
|
34541
|
+
return out;
|
|
34542
|
+
}
|
|
34543
|
+
function sameFileHelpers(ts, sf) {
|
|
34544
|
+
const functions = /* @__PURE__ */ new Map();
|
|
34545
|
+
for (const statement of sf.statements) {
|
|
34546
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) functions.set(statement.name.text, statement);
|
|
34547
|
+
if (ts.isVariableStatement(statement)) {
|
|
34548
|
+
for (const declaration2 of statement.declarationList.declarations) {
|
|
34549
|
+
const initializer = declaration2.initializer && bareExpression(ts, declaration2.initializer);
|
|
34550
|
+
if (ts.isIdentifier(declaration2.name) && initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
|
|
34551
|
+
functions.set(declaration2.name.text, initializer);
|
|
34552
|
+
}
|
|
34553
|
+
}
|
|
34554
|
+
}
|
|
34555
|
+
}
|
|
34556
|
+
return (call, index) => {
|
|
34557
|
+
const callee = call.expression;
|
|
34558
|
+
if (ts.isIdentifier(callee)) {
|
|
34559
|
+
const fn = functions.get(callee.text);
|
|
34560
|
+
return fn ? helperParam(ts, fn, index) : void 0;
|
|
34561
|
+
}
|
|
34562
|
+
if (ts.isPropertyAccessExpression(callee) && callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
34563
|
+
let owner = call.parent;
|
|
34564
|
+
while (owner && !ts.isClassLike(owner)) owner = owner.parent;
|
|
34565
|
+
const member = owner && ts.isClassLike(owner) ? owner.members.find((m) => ts.isMethodDeclaration(m) && m.name && propertyKeyText(ts, m.name) === callee.name.text) : void 0;
|
|
34566
|
+
return member ? helperParam(ts, member, index) : void 0;
|
|
34567
|
+
}
|
|
34568
|
+
return void 0;
|
|
34569
|
+
};
|
|
34570
|
+
}
|
|
34571
|
+
var REQUEST_HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
|
|
33927
34572
|
function walkExact(ts, sourceText, fileName) {
|
|
33928
34573
|
const sf = ts.createSourceFile(
|
|
33929
34574
|
fileName,
|
|
@@ -34220,47 +34865,8 @@ function walkExact(ts, sourceText, fileName) {
|
|
|
34220
34865
|
const container = containerOf(owner);
|
|
34221
34866
|
return container !== void 0 ? { container } : nested;
|
|
34222
34867
|
};
|
|
34223
|
-
const
|
|
34224
|
-
|
|
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
|
-
};
|
|
34868
|
+
const helpersHere = sameFileHelpers(ts, sf);
|
|
34869
|
+
const readsOff = (body, name) => [...handleReads(ts, body, name, helpersHere)].sort();
|
|
34264
34870
|
const declaredParameters = (fn) => {
|
|
34265
34871
|
const parameters = fn.parameters ?? [];
|
|
34266
34872
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -34976,7 +35582,7 @@ function checkerOptions(ts, projectRoot) {
|
|
|
34976
35582
|
maxNodeModuleJsDepth: 0
|
|
34977
35583
|
};
|
|
34978
35584
|
}
|
|
34979
|
-
function resolveCalls(ts, files, projectRoot) {
|
|
35585
|
+
function resolveCalls(ts, files, projectRoot, claimed = /* @__PURE__ */ new Set()) {
|
|
34980
35586
|
const out = /* @__PURE__ */ new Map();
|
|
34981
35587
|
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() };
|
|
34982
35588
|
const options = checkerOptions(ts, projectRoot);
|
|
@@ -35533,9 +36139,14 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
35533
36139
|
const reading = {};
|
|
35534
36140
|
if (kind) reading.kind = kind;
|
|
35535
36141
|
if (kind === "object") {
|
|
35536
|
-
const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).map((p) => p.getName());
|
|
36142
|
+
const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).filter((p) => (p.flags & ts.SymbolFlags.Method) === 0).map((p) => p.getName());
|
|
35537
36143
|
if (props.length > 0) reading.fields = props.slice(0, 200);
|
|
35538
36144
|
}
|
|
36145
|
+
if (opaqueType(type)) reading.opaque = true;
|
|
36146
|
+
if (ts.isIdentifier(parameter.name) && REQUEST_HANDLE_NAMES.has(parameter.name.text.replace(/^_+/, "").toLowerCase())) {
|
|
36147
|
+
const read2 = handleReads(ts, body, parameter.name.text, checkerHelpers);
|
|
36148
|
+
if (read2.size > 0) reading.reads = [...read2].sort();
|
|
36149
|
+
}
|
|
35539
36150
|
return reading;
|
|
35540
36151
|
} catch {
|
|
35541
36152
|
return {};
|
|
@@ -35560,6 +36171,16 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
35560
36171
|
} catch {
|
|
35561
36172
|
}
|
|
35562
36173
|
}
|
|
36174
|
+
if (ts.isImportSpecifier(node) && !node.isTypeOnly && !fileRoutes.has(node.name.text)) {
|
|
36175
|
+
try {
|
|
36176
|
+
const declaration2 = symbolOf(node.name)?.valueDeclaration;
|
|
36177
|
+
if (declaration2 && ts.isVariableDeclaration(declaration2) && declaration2.initializer && (ts.getCombinedNodeFlags(declaration2) & ts.NodeFlags.Const) !== 0) {
|
|
36178
|
+
const table = tableRoutes(ts, declaration2.initializer, initializerOf, literalOf);
|
|
36179
|
+
if (table.length > 0) fileRoutes.set(node.name.text, new Map(table.map((route2) => [routeKey2(route2), route2])));
|
|
36180
|
+
}
|
|
36181
|
+
} catch {
|
|
36182
|
+
}
|
|
36183
|
+
}
|
|
35563
36184
|
ts.forEachChild(node, visit);
|
|
35564
36185
|
};
|
|
35565
36186
|
visit(sf);
|
|
@@ -35618,6 +36239,29 @@ function resolveCalls(ts, files, projectRoot) {
|
|
|
35618
36239
|
if (broken.length > 0) crossProjectImports.set(key2, broken);
|
|
35619
36240
|
}
|
|
35620
36241
|
return { calls: out, imports, kinds, routes, prefixes, crossProjectImports };
|
|
36242
|
+
function opaqueType(type) {
|
|
36243
|
+
const at = checker.getNonNullableType(type);
|
|
36244
|
+
if (at.intrinsicName === "error") return false;
|
|
36245
|
+
if ((at.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) !== 0) return true;
|
|
36246
|
+
if ((at.flags & ts.TypeFlags.Object) === 0 || at.getProperties().length > 0) return false;
|
|
36247
|
+
if (at.getCallSignatures().length > 0 || at.getConstructSignatures().length > 0) return false;
|
|
36248
|
+
const lists = checker;
|
|
36249
|
+
return !(lists.isArrayType?.(at) || lists.isTupleType?.(at));
|
|
36250
|
+
}
|
|
36251
|
+
function checkerHelpers(call, index) {
|
|
36252
|
+
let declaration2;
|
|
36253
|
+
try {
|
|
36254
|
+
declaration2 = checker.getResolvedSignature(call)?.getDeclaration();
|
|
36255
|
+
} catch {
|
|
36256
|
+
return void 0;
|
|
36257
|
+
}
|
|
36258
|
+
if (!declaration2 || !declaration2.body) return void 0;
|
|
36259
|
+
const home = declaration2.getSourceFile();
|
|
36260
|
+
if (home.isDeclarationFile || !ownKey(home)) return void 0;
|
|
36261
|
+
const absolute = path20.resolve(home.fileName);
|
|
36262
|
+
if (absolute !== path20.resolve(call.getSourceFile().fileName) && claimed.has(absolute)) return void 0;
|
|
36263
|
+
return helperParam(ts, declaration2, index);
|
|
36264
|
+
}
|
|
35621
36265
|
function platformKind(annotation) {
|
|
35622
36266
|
if (!annotation || !ts.isTypeReferenceNode(annotation)) return void 0;
|
|
35623
36267
|
const name = ts.isIdentifier(annotation.typeName) ? annotation.typeName.text : annotation.typeName.right.text;
|
|
@@ -35694,8 +36338,10 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
35694
36338
|
const packages = {};
|
|
35695
36339
|
for (const [specifier, absolute] of localPackages) packages[specifier] = pathKey(path20.relative(projectRoot, absolute));
|
|
35696
36340
|
const declaredPaths = [];
|
|
36341
|
+
const claimedFiles = /* @__PURE__ */ new Set();
|
|
35697
36342
|
for (const impl of implementations) {
|
|
35698
36343
|
declaredPaths.push(...implementationSourceFiles(impl));
|
|
36344
|
+
for (const file of implementationSourceFiles(impl)) claimedFiles.add(path20.resolve(projectRoot, file));
|
|
35699
36345
|
if (impl.simPath) declaredPaths.push(impl.simPath);
|
|
35700
36346
|
const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
|
|
35701
36347
|
if (routerFile) declaredPaths.push(routerFile);
|
|
@@ -35795,7 +36441,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
35795
36441
|
let imports = /* @__PURE__ */ new Map();
|
|
35796
36442
|
let reading;
|
|
35797
36443
|
try {
|
|
35798
|
-
reading = resolveCalls(ts, checkerRoots, projectRoot);
|
|
36444
|
+
reading = resolveCalls(ts, checkerRoots, projectRoot, claimedFiles);
|
|
35799
36445
|
({ calls: resolved, imports } = reading);
|
|
35800
36446
|
} catch {
|
|
35801
36447
|
resolved = /* @__PURE__ */ new Map();
|
|
@@ -35818,6 +36464,8 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
|
|
|
35818
36464
|
const reading2 = settled[index][at];
|
|
35819
36465
|
if (reading2.kind) param.kind = reading2.kind;
|
|
35820
36466
|
if (reading2.fields) param.fields = reading2.fields;
|
|
36467
|
+
if (reading2.opaque) param.opaque = true;
|
|
36468
|
+
if (reading2.reads) param.reads = [.../* @__PURE__ */ new Set([...param.reads ?? [], ...reading2.reads])].sort();
|
|
35821
36469
|
});
|
|
35822
36470
|
});
|
|
35823
36471
|
}
|
|
@@ -36684,6 +37332,7 @@ function buildRuleContext(opts) {
|
|
|
36684
37332
|
...opts.typeSpellingFacts ? { typeSpellingFacts: opts.typeSpellingFacts } : {},
|
|
36685
37333
|
pinnedExternals,
|
|
36686
37334
|
...opts.bindingModules ? { bindingModules: opts.bindingModules } : {},
|
|
37335
|
+
...opts.memberApprovedSurfaces ? { memberApprovedSurfaces: opts.memberApprovedSurfaces } : {},
|
|
36687
37336
|
codeModel: opts.codeModel ?? emptyCodeModel(),
|
|
36688
37337
|
roundTripIssues: opts.roundTripIssues,
|
|
36689
37338
|
lintAllows: lintAllows2,
|
|
@@ -37214,6 +37863,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
|
|
|
37214
37863
|
const parts = new Set(graph().nodes.find((n) => n.namespace === "")?.parts.map((p) => p.alias) ?? []);
|
|
37215
37864
|
const pinnedExternals = listPinnedExternals2().filter((p) => !parts.has(p.alias));
|
|
37216
37865
|
const bindingModules = reachOnly ? [] : readBindingModules(implementations.filter((impl) => !impl.id.includes("::")).flatMap((impl) => impl.bindings ?? []), getProjectRoot());
|
|
37866
|
+
const approvedMemberSurfaces = bindingModules.length > 0 && declaredMembers(boundConfig ?? {}).length > 0 ? memberApprovedSurfacesOrNone() : void 0;
|
|
37217
37867
|
const conformance = rules?.conformance;
|
|
37218
37868
|
const codeModel = reachOnly ? buildCodeModel([], [], getProjectRoot(), [], []) : buildCodeModel(implementations, types, getProjectRoot(), conformance?.sourceRoots ?? [], conformance?.exclude ?? []);
|
|
37219
37869
|
const statusBearing = treatAllAsComplete ? [...subsystems, ...components, ...interfaces, ...implementations] : settledStatusBearing({ subsystems, components, interfaces, implementations });
|
|
@@ -37300,6 +37950,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
|
|
|
37300
37950
|
typeSpellingFacts: typeSpellings,
|
|
37301
37951
|
pinnedExternals,
|
|
37302
37952
|
bindingModules,
|
|
37953
|
+
...approvedMemberSurfaces ? { memberApprovedSurfaces: approvedMemberSurfaces } : {},
|
|
37303
37954
|
// By-name selections only: a legacy path ref pins nothing to check. A dry
|
|
37304
37955
|
// run supplies its candidate's; otherwise the stored ones.
|
|
37305
37956
|
packSelections: packSelections ?? projectPackSelections(),
|
|
@@ -37642,6 +38293,13 @@ function familyApprovals(depth) {
|
|
|
37642
38293
|
function familyRelations2() {
|
|
37643
38294
|
return familyRelations();
|
|
37644
38295
|
}
|
|
38296
|
+
function memberApprovedSurfacesOrNone() {
|
|
38297
|
+
try {
|
|
38298
|
+
return memberApprovedSurfaces2();
|
|
38299
|
+
} catch {
|
|
38300
|
+
return void 0;
|
|
38301
|
+
}
|
|
38302
|
+
}
|
|
37645
38303
|
function implementsProblem2(ref) {
|
|
37646
38304
|
return implementsProblem(ref);
|
|
37647
38305
|
}
|
|
@@ -39755,6 +40413,7 @@ function respellStoredReferences(kind, doc, map) {
|
|
|
39755
40413
|
break;
|
|
39756
40414
|
case "interface":
|
|
39757
40415
|
at(doc, "component", "contract");
|
|
40416
|
+
at(doc, "implements", "implements");
|
|
39758
40417
|
typedSlots("interface", doc, map);
|
|
39759
40418
|
break;
|
|
39760
40419
|
case "implementation":
|
|
@@ -40258,6 +40917,57 @@ function parseOrThrow(schema, value, kind, id) {
|
|
|
40258
40917
|
return res.data;
|
|
40259
40918
|
}
|
|
40260
40919
|
var SIGNATURE_TTL_MS = 2e3;
|
|
40920
|
+
var REPLACED_LINKAGE_LISTS = /* @__PURE__ */ new Set(["injectedParams", "bindings"]);
|
|
40921
|
+
function respelledField(kind, position) {
|
|
40922
|
+
switch (position) {
|
|
40923
|
+
case "narrative":
|
|
40924
|
+
case "calls":
|
|
40925
|
+
case "auth":
|
|
40926
|
+
return "methods";
|
|
40927
|
+
case "type":
|
|
40928
|
+
return kind === "type" ? "fields" : "methods";
|
|
40929
|
+
case "contract":
|
|
40930
|
+
return kind === "interface" ? "component" : "contract";
|
|
40931
|
+
default:
|
|
40932
|
+
return position;
|
|
40933
|
+
}
|
|
40934
|
+
}
|
|
40935
|
+
function omittedListNotices(kind, stored, delta) {
|
|
40936
|
+
const out = [];
|
|
40937
|
+
const fields = kind === "type" ? ["methods", "fields"] : kind === "interface" || kind === "implementation" ? ["methods"] : [];
|
|
40938
|
+
for (const field of fields) {
|
|
40939
|
+
const given = delta[field];
|
|
40940
|
+
if (!Array.isArray(given) || given.length === 0) continue;
|
|
40941
|
+
const named2 = new Set(given.map((e) => e && typeof e === "object" ? e.name : void 0).filter((n) => typeof n === "string"));
|
|
40942
|
+
const kept = (Array.isArray(stored[field]) ? stored[field] : []).map((e) => e?.name).filter((n) => typeof n === "string" && !named2.has(n));
|
|
40943
|
+
if (kept.length === 0) continue;
|
|
40944
|
+
out.push(
|
|
40945
|
+
`${field}: the delta left out ${kept.map((n) => `"${n}"`).join(", ")}, and a list delta upserts \u2014 what it leaves out is kept, never removed. To remove one, name it with a delete marker: {"${field}": [{"name": "${kept[0]}", "action": "delete"}]}.`
|
|
40946
|
+
);
|
|
40947
|
+
}
|
|
40948
|
+
return out;
|
|
40949
|
+
}
|
|
40950
|
+
var PATH_SAFE_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
40951
|
+
function pathSafeIdProblem(id) {
|
|
40952
|
+
if (typeof id !== "string" || id === "") return "it is empty";
|
|
40953
|
+
const segments = id.split("::");
|
|
40954
|
+
const named2 = id.startsWith("::") ? segments.slice(1) : segments;
|
|
40955
|
+
const bad = named2.find((s) => !PATH_SAFE_SEGMENT.test(s));
|
|
40956
|
+
if (bad === void 0) return null;
|
|
40957
|
+
if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
|
|
40958
|
+
if (bad === "") return "it has an empty segment";
|
|
40959
|
+
if (/^\.+$/.test(bad) || bad.includes(".")) return `"${bad}" holds a dot`;
|
|
40960
|
+
return `"${bad}" holds a character outside letters, digits, "-" and "_"`;
|
|
40961
|
+
}
|
|
40962
|
+
function assertPathSafe(id, what) {
|
|
40963
|
+
const problem = pathSafeIdProblem(id);
|
|
40964
|
+
if (problem !== null) {
|
|
40965
|
+
throw new Error(`not-an-id: the ${what} id "${id}" cannot name a spec \u2014 ${problem}. An id becomes a folder or a file name, so it is a plain id (letters, digits, "-" and "_", qualified by "::"), never a path. Nothing was written.`);
|
|
40966
|
+
}
|
|
40967
|
+
}
|
|
40968
|
+
function storedUnder(storedId, askedId) {
|
|
40969
|
+
return typeof storedId !== "string" || storedId === askedId.split("::").pop();
|
|
40970
|
+
}
|
|
40261
40971
|
function computeSpecTreeSignature(dirs, files = []) {
|
|
40262
40972
|
const parts = [];
|
|
40263
40973
|
for (const f of files) {
|
|
@@ -41806,10 +42516,117 @@ var SpecWorkspace = class {
|
|
|
41806
42516
|
const mapped = mapSpecReferences(kind, spec, (position, value) => position === "signatureFrom" && !components.has(value) ? value : this.writeReference(from, spec.id, position, value, carrying));
|
|
41807
42517
|
return { ...mapped, id: localOf(from, spec.id).split("::").pop() };
|
|
41808
42518
|
}
|
|
42519
|
+
/**
|
|
42520
|
+
* The keys an `alias::name` read lands on when no spec holds the id as
|
|
42521
|
+
* written: the id bound through the bound project's alias table as a write
|
|
42522
|
+
* binds it (a public name to the member's key), and the member's own key for
|
|
42523
|
+
* a name it does not export — so a read resolves the form the guide
|
|
42524
|
+
* prescribes exactly as a write resolves it to refuse. None for a bare id.
|
|
42525
|
+
*/
|
|
42526
|
+
aliasedReadKeys(id) {
|
|
42527
|
+
if (!id.includes("::") || pathSafeIdProblem(id) !== null) return [];
|
|
42528
|
+
const out = [];
|
|
42529
|
+
try {
|
|
42530
|
+
const bound2 = this.bindReference("", id);
|
|
42531
|
+
if (bound2 !== id) out.push(bound2);
|
|
42532
|
+
} catch {
|
|
42533
|
+
}
|
|
42534
|
+
const at = id.indexOf("::");
|
|
42535
|
+
const namespace = this.cachedRawRoots[0]?.record.aliases.get(id.slice(0, at));
|
|
42536
|
+
if (namespace !== void 0 && namespace !== "" && this.rootCovering(namespace) !== null) out.push(`${namespace}::${id.slice(at + 2)}`);
|
|
42537
|
+
return [...new Set(out)];
|
|
42538
|
+
}
|
|
42539
|
+
/**
|
|
42540
|
+
* The references a delta writes in ANOTHER TEXT than the stored spec holds
|
|
42541
|
+
* at the same position while both bind to one target (core_orchestrator
|
|
42542
|
+
* updateSpec step 14): the stored file's authored texts read per position,
|
|
42543
|
+
* the delta's read the same way, and a delta text the position does not hold
|
|
42544
|
+
* whose binding equals a stored text's binding is a respelling of it.
|
|
42545
|
+
*/
|
|
42546
|
+
referenceRespellingsOf(kind, id, delta) {
|
|
42547
|
+
if (kind === "system") return [];
|
|
42548
|
+
const file = this.storedFileOf(kind, id);
|
|
42549
|
+
if (!file || !pathExists(file)) return [];
|
|
42550
|
+
let stored;
|
|
42551
|
+
try {
|
|
42552
|
+
stored = JSON.parse(JSON.stringify(readSpecFile(file)));
|
|
42553
|
+
} catch {
|
|
42554
|
+
return [];
|
|
42555
|
+
}
|
|
42556
|
+
const held = /* @__PURE__ */ new Map();
|
|
42557
|
+
respellStoredReferences(kind, stored, (position, value) => {
|
|
42558
|
+
held.set(position, (held.get(position) ?? /* @__PURE__ */ new Set()).add(value));
|
|
42559
|
+
return value;
|
|
42560
|
+
});
|
|
42561
|
+
const written = [];
|
|
42562
|
+
respellStoredReferences(kind, JSON.parse(JSON.stringify(delta)), (position, value) => {
|
|
42563
|
+
written.push({ position, value });
|
|
42564
|
+
return value;
|
|
42565
|
+
});
|
|
42566
|
+
const prefix = this.writePrefixFor(id);
|
|
42567
|
+
const bind = (value) => {
|
|
42568
|
+
try {
|
|
42569
|
+
return this.bindReference(prefix, value);
|
|
42570
|
+
} catch {
|
|
42571
|
+
return value;
|
|
42572
|
+
}
|
|
42573
|
+
};
|
|
42574
|
+
const out = [];
|
|
42575
|
+
for (const w of written) {
|
|
42576
|
+
const texts = held.get(w.position);
|
|
42577
|
+
if (!texts || texts.has(w.value)) continue;
|
|
42578
|
+
const target = bind(w.value);
|
|
42579
|
+
const from = [...texts].find((s) => s !== w.value && bind(s) === target);
|
|
42580
|
+
if (from !== void 0 && !out.some((o) => o.position === w.position && o.from === from)) out.push({ position: w.position, from, to: w.value });
|
|
42581
|
+
}
|
|
42582
|
+
return out;
|
|
42583
|
+
}
|
|
42584
|
+
/**
|
|
42585
|
+
* spec_loader.readPinnedSpec — an EXTERNAL's spec read by `alias::name` from
|
|
42586
|
+
* the pinned snapshot the bound project holds of it: the contract entry (a
|
|
42587
|
+
* component or an interface asked for, or no kind) or the exported type (a
|
|
42588
|
+
* type). Null when the alias is no declared external (a member's alias is
|
|
42589
|
+
* read under its key instead), its pin is absent or unreadable, or the pin
|
|
42590
|
+
* holds no such public name. Read-only, never cached.
|
|
42591
|
+
*/
|
|
42592
|
+
readPinnedSpec(kind, id) {
|
|
42593
|
+
const at = id.indexOf("::");
|
|
42594
|
+
const pin2 = at > 0 ? this.pinnedSnapshotOf(id.slice(0, at)) : null;
|
|
42595
|
+
if (!pin2) return null;
|
|
42596
|
+
const name = id.slice(at + 2);
|
|
42597
|
+
const entry = kind === "type" ? void 0 : pin2.snapshot.interfaces.find((e) => e.id === name);
|
|
42598
|
+
if (entry) return { ...pin2.base, kind: "interface", id: name, spec: entry };
|
|
42599
|
+
const definition = kind === void 0 || kind === "type" ? pinnedTypeDefinition(pin2.snapshot, name) : void 0;
|
|
42600
|
+
return definition ? { ...pin2.base, kind: "type", id: name, spec: definition } : null;
|
|
42601
|
+
}
|
|
42602
|
+
/** The pinned snapshot of the bound project's external `alias` — none for a member's alias, an undeclared one, or an absent or unreadable pin. */
|
|
42603
|
+
pinnedSnapshotOf(alias) {
|
|
42604
|
+
this.scanAll();
|
|
42605
|
+
const root = this.cachedRawRoots[0];
|
|
42606
|
+
const config = root?.record.config;
|
|
42607
|
+
if (!root || !config || root.record.aliases.has(alias)) return null;
|
|
42608
|
+
const declared = declaredExternals(config).find((d) => d.alias === alias && !d.problem);
|
|
42609
|
+
const file = path23.join(aiPathsAt(root.dir).root(), "externals", `${alias}.yaml`);
|
|
42610
|
+
if (!declared || !pathExists(file)) return null;
|
|
42611
|
+
try {
|
|
42612
|
+
const snapshot = SurfaceSnapshotSchema.parse(readSpecFile(file));
|
|
42613
|
+
const rel2 = path23.relative(root.dir, file).split(path23.sep).join("/");
|
|
42614
|
+
return { snapshot, base: { alias, project: declared.project, file: rel2, ...snapshot.stateId !== void 0 ? { stateId: snapshot.stateId } : {} } };
|
|
42615
|
+
} catch {
|
|
42616
|
+
return null;
|
|
42617
|
+
}
|
|
42618
|
+
}
|
|
41809
42619
|
// -------------------------------------------------------------------------
|
|
41810
42620
|
// Path builders
|
|
42621
|
+
//
|
|
42622
|
+
// Every id a path is built from must be path-safe FIRST: a subsystem, a
|
|
42623
|
+
// component, a contract, an implementation or a type id becomes a folder or
|
|
42624
|
+
// a file name, and one carrying a separator or a `..` segment would build a
|
|
42625
|
+
// path out of the tree — into another project's specs. Refused here, before
|
|
42626
|
+
// any path exists, whichever write asked.
|
|
41811
42627
|
// -------------------------------------------------------------------------
|
|
41812
42628
|
getSubsystemPath(id) {
|
|
42629
|
+
assertPathSafe(id, "subsystem");
|
|
41813
42630
|
const index = this.scanAll();
|
|
41814
42631
|
if (index.paths.subsystem[id]) {
|
|
41815
42632
|
return index.paths.subsystem[id];
|
|
@@ -41834,6 +42651,8 @@ var SpecWorkspace = class {
|
|
|
41834
42651
|
return path23.join(this.paths.specsDir(), id, ".index.yaml");
|
|
41835
42652
|
}
|
|
41836
42653
|
getComponentPath(id, subsystemId) {
|
|
42654
|
+
assertPathSafe(id, "component");
|
|
42655
|
+
if (subsystemId !== void 0) assertPathSafe(subsystemId, "subsystem");
|
|
41837
42656
|
const index = this.scanAll();
|
|
41838
42657
|
if (index.paths.component[id]) {
|
|
41839
42658
|
return index.paths.component[id];
|
|
@@ -41868,6 +42687,8 @@ var SpecWorkspace = class {
|
|
|
41868
42687
|
return path23.join(this.paths.specsDir(), targetSubsystem, id, ".index.yaml");
|
|
41869
42688
|
}
|
|
41870
42689
|
getInterfacePath(id, componentId) {
|
|
42690
|
+
assertPathSafe(id, "interface");
|
|
42691
|
+
if (componentId !== void 0) assertPathSafe(componentId, "component");
|
|
41871
42692
|
const index = this.scanAll();
|
|
41872
42693
|
if (index.paths.interface[id]) {
|
|
41873
42694
|
return index.paths.interface[id];
|
|
@@ -41903,6 +42724,8 @@ var SpecWorkspace = class {
|
|
|
41903
42724
|
return path23.join(this.paths.specsDir(), "default", targetComponent2, ".interface.yaml");
|
|
41904
42725
|
}
|
|
41905
42726
|
getImplementationPath(id, contractId) {
|
|
42727
|
+
assertPathSafe(id, "implementation");
|
|
42728
|
+
if (contractId !== void 0) assertPathSafe(contractId, "interface");
|
|
41906
42729
|
const index = this.scanAll();
|
|
41907
42730
|
if (index.paths.implementation[id]) {
|
|
41908
42731
|
return index.paths.implementation[id];
|
|
@@ -41938,6 +42761,9 @@ var SpecWorkspace = class {
|
|
|
41938
42761
|
return path23.join(this.paths.specsDir(), "default", targetContract, ".implementation.yaml");
|
|
41939
42762
|
}
|
|
41940
42763
|
getTypePath(id, subsystemId, group) {
|
|
42764
|
+
assertPathSafe(id, "type");
|
|
42765
|
+
if (subsystemId !== void 0 && subsystemId !== "") assertPathSafe(subsystemId, "subsystem");
|
|
42766
|
+
if (group !== void 0 && group !== "") assertPathSafe(group, "group");
|
|
41941
42767
|
const index = this.scanAll();
|
|
41942
42768
|
if (index.paths.type[id]) return index.paths.type[id];
|
|
41943
42769
|
if (id.includes("::")) {
|
|
@@ -42080,11 +42906,15 @@ var SpecWorkspace = class {
|
|
|
42080
42906
|
const index = this.scanAll();
|
|
42081
42907
|
const cached = index.subsystems.find((s) => s.id === id);
|
|
42082
42908
|
if (cached) return cached;
|
|
42909
|
+
const aliased = this.aliasedReadKeys(id).map((key2) => index.subsystems.find((s) => s.id === key2)).find((s) => s !== void 0);
|
|
42910
|
+
if (aliased) return aliased;
|
|
42911
|
+
if (pathSafeIdProblem(id) !== null) return null;
|
|
42083
42912
|
const p = this.getSubsystemPath(id);
|
|
42084
42913
|
if (!pathExists(p)) return null;
|
|
42085
42914
|
try {
|
|
42086
42915
|
const raw = readSpecFile(p);
|
|
42087
|
-
|
|
42916
|
+
const parsed = SubsystemSpecSchema.parse(raw);
|
|
42917
|
+
return storedUnder(parsed.id, id) ? parsed : null;
|
|
42088
42918
|
} catch (e) {
|
|
42089
42919
|
this.loaderIssues.push({
|
|
42090
42920
|
severity: "error",
|
|
@@ -42252,10 +43082,14 @@ var SpecWorkspace = class {
|
|
|
42252
43082
|
const index = this.scanAll();
|
|
42253
43083
|
const spec = index.components.find((c) => c.id === id);
|
|
42254
43084
|
if (spec) return spec;
|
|
43085
|
+
const aliased = this.aliasedReadKeys(id).map((key2) => index.components.find((c) => c.id === key2)).find((c) => c !== void 0);
|
|
43086
|
+
if (aliased) return aliased;
|
|
43087
|
+
if (pathSafeIdProblem(id) !== null) return null;
|
|
42255
43088
|
const p = this.getComponentPath(id);
|
|
42256
43089
|
if (!pathExists(p)) return null;
|
|
42257
43090
|
try {
|
|
42258
43091
|
const raw = readSpecFile(p);
|
|
43092
|
+
if (!storedUnder(raw.id, id)) return null;
|
|
42259
43093
|
readRetiredReachForms("component", raw);
|
|
42260
43094
|
return attachRetiredMounts(ComponentSpecSchema.parse(raw), raw);
|
|
42261
43095
|
} catch (e) {
|
|
@@ -42355,10 +43189,14 @@ var SpecWorkspace = class {
|
|
|
42355
43189
|
const index = this.scanAll();
|
|
42356
43190
|
const spec = index.interfaces.find((i) => i.id === id);
|
|
42357
43191
|
if (spec) return spec;
|
|
43192
|
+
const aliased = this.aliasedReadKeys(id).map((key2) => index.interfaces.find((i) => i.id === key2)).find((i) => i !== void 0);
|
|
43193
|
+
if (aliased) return aliased;
|
|
43194
|
+
if (pathSafeIdProblem(id) !== null) return null;
|
|
42358
43195
|
const p = this.getInterfacePath(id);
|
|
42359
43196
|
if (!pathExists(p)) return null;
|
|
42360
43197
|
try {
|
|
42361
43198
|
const raw = readSpecFile(p);
|
|
43199
|
+
if (!storedUnder(raw.id, id)) return null;
|
|
42362
43200
|
const owner = typeof raw.component === "string" ? index.components.find((c) => c.id === raw.component) : void 0;
|
|
42363
43201
|
readRetiredReachForms("interface", raw, owner?.componentType === "Portal");
|
|
42364
43202
|
return resolveTree([interfaceCanonicalTypes(InterfaceSpecSchema.parse(raw)).spec], index.components, index.types).interfaces[0];
|
|
@@ -42454,10 +43292,14 @@ var SpecWorkspace = class {
|
|
|
42454
43292
|
const index = this.scanAll();
|
|
42455
43293
|
const spec = index.implementations.find((impl) => impl.id === id);
|
|
42456
43294
|
if (spec) return spec;
|
|
43295
|
+
const aliased = this.aliasedReadKeys(id).map((key2) => index.implementations.find((impl) => impl.id === key2)).find((impl) => impl !== void 0);
|
|
43296
|
+
if (aliased) return aliased;
|
|
43297
|
+
if (pathSafeIdProblem(id) !== null) return null;
|
|
42457
43298
|
const p = this.getImplementationPath(id);
|
|
42458
43299
|
if (!pathExists(p)) return null;
|
|
42459
43300
|
try {
|
|
42460
43301
|
const raw = readSpecFile(p);
|
|
43302
|
+
if (!storedUnder(raw.id, id)) return null;
|
|
42461
43303
|
return ImplementationSpecSchema.parse(raw);
|
|
42462
43304
|
} catch (e) {
|
|
42463
43305
|
this.loaderIssues.push({
|
|
@@ -42507,7 +43349,8 @@ var SpecWorkspace = class {
|
|
|
42507
43349
|
return this.scanAll().types;
|
|
42508
43350
|
}
|
|
42509
43351
|
loadTypeSpec(id) {
|
|
42510
|
-
|
|
43352
|
+
const types = this.scanAll().types;
|
|
43353
|
+
return types.find((t) => t.id === id) ?? this.aliasedReadKeys(id).map((key2) => types.find((t) => t.id === key2)).find((t) => t !== void 0) ?? null;
|
|
42511
43354
|
}
|
|
42512
43355
|
/**
|
|
42513
43356
|
* Returns non-fatal placement notices (empty when there is nothing to
|
|
@@ -43720,6 +44563,8 @@ var SpecWorkspace = class {
|
|
|
43720
44563
|
res.dispatch = inDeltaOrder(key2, existing.dispatch, value, mergeKeyedArray(existing.dispatch, value, (b) => String(b?.capability)));
|
|
43721
44564
|
} else if (key2 === "lifecycle" && Array.isArray(value) && Array.isArray(existing.lifecycle)) {
|
|
43722
44565
|
res.lifecycle = inDeltaOrder(key2, existing.lifecycle, value, mergeKeyedArray(existing.lifecycle, value, (le) => `${le?.phase} ${le?.component} ${le?.method}`));
|
|
44566
|
+
} else if (kind === "implementation" && delta2 === qualifiedDelta && REPLACED_LINKAGE_LISTS.has(key2) && Array.isArray(value) && !value.some(isValueMarker)) {
|
|
44567
|
+
res[key2] = value;
|
|
43723
44568
|
} else if (Array.isArray(value) && isPlainValueList(key2, existing[key2], value)) {
|
|
43724
44569
|
res[key2] = inDeltaOrder(key2, existing[key2] ?? [], value, mergePlainValues(key2, existing[key2] ?? [], value));
|
|
43725
44570
|
} else if (Array.isArray(value) && value.length > 0 && Array.isArray(existing[key2]) && isIdentifiedArray(key2, existing[key2], value)) {
|
|
@@ -43820,12 +44665,14 @@ var SpecWorkspace = class {
|
|
|
43820
44665
|
const canonicalStored = canonical3(storedBefore);
|
|
43821
44666
|
const canonicalMerged = canonical3(mergedResult);
|
|
43822
44667
|
const changes = specChanges(canonicalStored, canonicalMerged);
|
|
44668
|
+
const referenceRespellings = this.referenceRespellingsOf(kind, id, mergeableDelta);
|
|
44669
|
+
for (const r of referenceRespellings) changes.push({ path: respelledField(kind, r.position), change: "set", before: r.from, after: r.to });
|
|
43823
44670
|
const ineffective = ineffectiveDeltaPaths(
|
|
43824
44671
|
{ ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
|
|
43825
44672
|
canonicalMerged,
|
|
43826
44673
|
canonicalStored,
|
|
43827
44674
|
respellings
|
|
43828
|
-
);
|
|
44675
|
+
).filter((line) => !referenceRespellings.some((r) => line.startsWith(respelledField(kind, r.position)) && line.includes("already held")));
|
|
43829
44676
|
if (changes.length === 0) {
|
|
43830
44677
|
return {
|
|
43831
44678
|
kind,
|
|
@@ -43834,7 +44681,7 @@ var SpecWorkspace = class {
|
|
|
43834
44681
|
dryRun,
|
|
43835
44682
|
changes,
|
|
43836
44683
|
ineffective,
|
|
43837
|
-
notices,
|
|
44684
|
+
notices: [...notices, ...omittedListNotices(kind, storedBefore, mergeableDelta)],
|
|
43838
44685
|
respellings: [],
|
|
43839
44686
|
testsToRevisit: [],
|
|
43840
44687
|
summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
|
|
@@ -43864,6 +44711,9 @@ var SpecWorkspace = class {
|
|
|
43864
44711
|
allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
|
|
43865
44712
|
};
|
|
43866
44713
|
notices.push(...this.saveOfKind(kind, mergedResult, opts));
|
|
44714
|
+
if (referenceRespellings.length > 0) {
|
|
44715
|
+
this.respellReferences(kind, id, referenceRespellings.map((r) => ({ kind, specId: id, position: r.position, from: r.from, to: r.to })));
|
|
44716
|
+
}
|
|
43867
44717
|
return {
|
|
43868
44718
|
kind,
|
|
43869
44719
|
id,
|
|
@@ -44527,6 +45377,23 @@ function registryInvalidateCache() {
|
|
|
44527
45377
|
function invalidateSpecCache() {
|
|
44528
45378
|
registryInvalidateCache();
|
|
44529
45379
|
}
|
|
45380
|
+
function registryLockSpecTree() {
|
|
45381
|
+
lockTree(getProjectRoot());
|
|
45382
|
+
for (const ws of workspaces.values()) ws.expireFreshness();
|
|
45383
|
+
}
|
|
45384
|
+
function lockTree2() {
|
|
45385
|
+
registryLockSpecTree();
|
|
45386
|
+
}
|
|
45387
|
+
function pinnedTypeDefinition(snapshot, name) {
|
|
45388
|
+
const exported = (snapshot.exportedTypes ?? []).find((t) => t.id === name);
|
|
45389
|
+
return exported ? snapshot.types.find((t) => t.id === exported.type) : void 0;
|
|
45390
|
+
}
|
|
45391
|
+
function readPinnedSpec(kind, id) {
|
|
45392
|
+
return current().readPinnedSpec(kind, id);
|
|
45393
|
+
}
|
|
45394
|
+
function unlockTree2() {
|
|
45395
|
+
unlockTree(getProjectRoot());
|
|
45396
|
+
}
|
|
44530
45397
|
function getLoaderIssues() {
|
|
44531
45398
|
return current().loaderIssues;
|
|
44532
45399
|
}
|
|
@@ -45006,9 +45873,49 @@ function listConsumers(search) {
|
|
|
45006
45873
|
const bound2 = path25.resolve(getProjectRoot());
|
|
45007
45874
|
const { top } = climb(bound2);
|
|
45008
45875
|
const family = familyConsumers(bound2, top);
|
|
45009
|
-
const seen = new Set(family.map((c) =>
|
|
45010
|
-
const
|
|
45011
|
-
|
|
45876
|
+
const seen = new Set([bound2, top, ...family.map((c) => c.directory)].map(dirKey));
|
|
45877
|
+
const parents = composingParents(bound2, top, seen);
|
|
45878
|
+
for (const c of parents) seen.add(dirKey(c.directory));
|
|
45879
|
+
const found = searchedConsumers(bound2, search ?? [], seen).filter((c) => !seen.has(dirKey(c.directory)));
|
|
45880
|
+
return [...family, ...parents, ...found];
|
|
45881
|
+
}
|
|
45882
|
+
function composesByPath(config, root, bound2) {
|
|
45883
|
+
return declaredMembers(config).some((m) => !m.problem && m.source.path !== void 0 && m.source.path.trim() !== "" && dirKey(path25.resolve(root, m.source.path)) === dirKey(bound2));
|
|
45884
|
+
}
|
|
45885
|
+
function composingParents(bound2, top, seen) {
|
|
45886
|
+
if (getHostedLookup() !== null) return [];
|
|
45887
|
+
const reach2 = getRequestParentReach();
|
|
45888
|
+
if (reach2 && !reach2.parentReach) return [];
|
|
45889
|
+
const folders = [...new Set([path25.dirname(top), path25.dirname(bound2)].map((d) => path25.resolve(d)))];
|
|
45890
|
+
const out = [];
|
|
45891
|
+
for (const root of [...new Set(folders.flatMap(enclosedRoots).map((r) => path25.resolve(r)))]) {
|
|
45892
|
+
if (seen.has(dirKey(root)) || out.some((c) => dirKey(c.directory) === dirKey(root))) continue;
|
|
45893
|
+
const config = membersConfigAt(root);
|
|
45894
|
+
if (!config || !composesByPath(config, root, bound2)) continue;
|
|
45895
|
+
out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))));
|
|
45896
|
+
}
|
|
45897
|
+
return out;
|
|
45898
|
+
}
|
|
45899
|
+
var ENCLOSING_FOLDER_LIMIT = 256;
|
|
45900
|
+
function enclosedRoots(folder) {
|
|
45901
|
+
let entries;
|
|
45902
|
+
try {
|
|
45903
|
+
entries = fs17.readdirSync(folder, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules");
|
|
45904
|
+
} catch {
|
|
45905
|
+
return [];
|
|
45906
|
+
}
|
|
45907
|
+
if (entries.length > ENCLOSING_FOLDER_LIMIT) return [];
|
|
45908
|
+
return entries.map((d) => path25.join(folder, d.name)).filter((dir) => fs17.existsSync(path25.join(dir, ".wai", "project.yaml")));
|
|
45909
|
+
}
|
|
45910
|
+
function membersConfigAt(root) {
|
|
45911
|
+
try {
|
|
45912
|
+
const text3 = fs17.readFileSync(path25.join(root, ".wai", "project.yaml"), "utf8");
|
|
45913
|
+
if (!/^members\s*:/m.test(text3)) return null;
|
|
45914
|
+
const parsed = parseYaml(text3);
|
|
45915
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
45916
|
+
} catch {
|
|
45917
|
+
return null;
|
|
45918
|
+
}
|
|
45012
45919
|
}
|
|
45013
45920
|
function familyConsumers(bound2, top) {
|
|
45014
45921
|
return runWithProjectRoot(top, () => {
|
|
@@ -45051,7 +45958,7 @@ function projectRootsUnder(folder) {
|
|
|
45051
45958
|
if (isRoot(at)) return [at];
|
|
45052
45959
|
return fs17.readdirSync(at, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules").map((d) => path25.join(at, d.name)).filter(isRoot);
|
|
45053
45960
|
}
|
|
45054
|
-
function searchedConsumers(bound2, search) {
|
|
45961
|
+
function searchedConsumers(bound2, search, seen = /* @__PURE__ */ new Set()) {
|
|
45055
45962
|
if (search.length === 0) return [];
|
|
45056
45963
|
if (getHostedLookup() !== null) throw new Error("searching folders for consumers is a local read: a hosted request reads no folder outside its project");
|
|
45057
45964
|
const producerId = runWithProjectRoot(bound2, () => {
|
|
@@ -45061,6 +45968,13 @@ function searchedConsumers(bound2, search) {
|
|
|
45061
45968
|
const roots = [...new Set(search.flatMap(projectRootsUnder).map((r) => path25.resolve(r)))].filter((r) => dirKey(r) !== dirKey(bound2));
|
|
45062
45969
|
const out = [];
|
|
45063
45970
|
for (const root of roots) {
|
|
45971
|
+
if (seen.has(dirKey(root))) continue;
|
|
45972
|
+
const members = membersConfigAt(root);
|
|
45973
|
+
const composing = members !== null && composesByPath(members, root, bound2);
|
|
45974
|
+
if (composing) {
|
|
45975
|
+
out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))).map((c) => ({ ...c, found: "search" })));
|
|
45976
|
+
continue;
|
|
45977
|
+
}
|
|
45064
45978
|
const answer = runWithProjectRoot(root, () => {
|
|
45065
45979
|
let config;
|
|
45066
45980
|
try {
|
|
@@ -45088,6 +46002,47 @@ function searchedConsumers(bound2, search) {
|
|
|
45088
46002
|
}
|
|
45089
46003
|
return out.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
|
|
45090
46004
|
}
|
|
46005
|
+
function recordedMemberSubjects(root) {
|
|
46006
|
+
try {
|
|
46007
|
+
const record = JSON.parse(fs17.readFileSync(path25.join(root, ".wai", "lock.json"), "utf8"));
|
|
46008
|
+
return new Map(Object.entries(record.members ?? {}).filter(([, m]) => typeof m?.subject === "string").map(([alias, m]) => [alias, m.subject]));
|
|
46009
|
+
} catch {
|
|
46010
|
+
return /* @__PURE__ */ new Map();
|
|
46011
|
+
}
|
|
46012
|
+
}
|
|
46013
|
+
function memberRevisions() {
|
|
46014
|
+
if (getHostedLookup() !== null) return [];
|
|
46015
|
+
const bound2 = path25.resolve(getProjectRoot());
|
|
46016
|
+
const family = graph();
|
|
46017
|
+
const own2 = consumerNode(family, bound2);
|
|
46018
|
+
if (!own2) return [];
|
|
46019
|
+
const subjects = recordedMemberSubjects(bound2);
|
|
46020
|
+
const out = [];
|
|
46021
|
+
for (const node of family.nodes) {
|
|
46022
|
+
if (node.parent !== own2.namespace || node === own2 || node.mountAlias === void 0) continue;
|
|
46023
|
+
const alias = node.mountAlias;
|
|
46024
|
+
const usage = exportUsage(own2.namespace, node.namespace);
|
|
46025
|
+
const entry = { alias, project: node.id ?? node.namespace, root: node.directory, usage };
|
|
46026
|
+
const repo = repositoryRoot(node.directory);
|
|
46027
|
+
if (repo) {
|
|
46028
|
+
const lockPath3 = path25.join(".wai", "lock.json");
|
|
46029
|
+
const subject = subjects.get(alias);
|
|
46030
|
+
const digest3 = subject?.slice(subject.lastIndexOf(":") + 1);
|
|
46031
|
+
const recorded = digest3 ? commitIntroducing(node.directory, lockPath3, digest3) : null;
|
|
46032
|
+
const commit2 = recorded ?? lastCommitOf(node.directory, lockPath3);
|
|
46033
|
+
if (commit2) {
|
|
46034
|
+
try {
|
|
46035
|
+
const relative27 = path25.relative(repo, node.directory);
|
|
46036
|
+
const directory = fetch(repo, commit2, relative27 === "" ? void 0 : relative27);
|
|
46037
|
+
entry.revision = { directory, commit: commit2, label: recorded ? `the approval this project's lock records for it (${commit2.slice(0, 12)})` : `its own last committed approval (${commit2.slice(0, 12)})` };
|
|
46038
|
+
} catch {
|
|
46039
|
+
}
|
|
46040
|
+
}
|
|
46041
|
+
}
|
|
46042
|
+
out.push(entry);
|
|
46043
|
+
}
|
|
46044
|
+
return out;
|
|
46045
|
+
}
|
|
45091
46046
|
function approvedRevision(against) {
|
|
45092
46047
|
const bound2 = path25.resolve(getProjectRoot());
|
|
45093
46048
|
const repo = repositoryRoot(bound2);
|
|
@@ -46704,6 +47659,19 @@ function moveMemberSpecs(scan2, remap) {
|
|
|
46704
47659
|
});
|
|
46705
47660
|
}
|
|
46706
47661
|
const rewritten = rewriteRefFields(parentSpecsDir, remap);
|
|
47662
|
+
for (const file of listFilesRecursive(parentSpecsDir, ".yaml")) {
|
|
47663
|
+
let raw;
|
|
47664
|
+
try {
|
|
47665
|
+
raw = readYamlFile(file);
|
|
47666
|
+
} catch {
|
|
47667
|
+
continue;
|
|
47668
|
+
}
|
|
47669
|
+
const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
|
|
47670
|
+
if (!kind || !respellTypeTokens(raw, remap)) continue;
|
|
47671
|
+
writeYamlFile(file, raw);
|
|
47672
|
+
const id = kind === "system" ? "system" : String(raw.id);
|
|
47673
|
+
if (!rewritten.some((s) => s.kind === kind && s.id === id)) rewritten.push({ kind, id });
|
|
47674
|
+
}
|
|
46707
47675
|
invalidateSpecCache();
|
|
46708
47676
|
return rewritten;
|
|
46709
47677
|
}
|
|
@@ -47253,6 +48221,8 @@ function patchSubsystemIndex(indexPath, mutate) {
|
|
|
47253
48221
|
writeYamlFile(indexPath, raw);
|
|
47254
48222
|
}
|
|
47255
48223
|
function renameComponent(componentId, newId, dryRun) {
|
|
48224
|
+
const foreign = foreignIdRefusal(componentId, "chained-component", "rename it", `sdd_rename_component ${componentId.slice(componentId.lastIndexOf("::") + 2)} <new id>`);
|
|
48225
|
+
if (foreign !== null) throw new WaironError(foreign);
|
|
47256
48226
|
const component = loadComponentSpec(componentId);
|
|
47257
48227
|
if (!component) {
|
|
47258
48228
|
throw new WaironError(`component-missing: no component has the id "${componentId}".`);
|
|
@@ -47439,6 +48409,8 @@ function renameSpecId(kind, id, newId, dryRun) {
|
|
|
47439
48409
|
const owner = kind === "component" ? "sdd_rename_component" : kind === "type" ? "sdd_rename_type" : "no tool yet";
|
|
47440
48410
|
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
48411
|
}
|
|
48412
|
+
const foreign = foreignIdRefusal(id, "chained-spec", "rename it", `sdd_rename_spec ${kind} ${id.slice(id.lastIndexOf("::") + 2)} <new id>`);
|
|
48413
|
+
if (foreign !== null) throw new WaironError(foreign);
|
|
47442
48414
|
const spec = kind === "interface" ? loadInterfaceSpec(id) : loadImplementationSpec(id);
|
|
47443
48415
|
if (!spec) throw new WaironError(`spec-missing: no ${kind} has the id "${id}".`);
|
|
47444
48416
|
if (id.includes("::")) {
|
|
@@ -47595,11 +48567,11 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
|
|
|
47595
48567
|
if (target === void 0 && exportsMoving.length + repointed.length > 0) {
|
|
47596
48568
|
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
48569
|
}
|
|
47598
|
-
const
|
|
47599
|
-
const
|
|
48570
|
+
const ownId = effectiveProjectId(projectConfigRepository.load() ?? { name: loadSystemSpec()?.name ?? "" });
|
|
48571
|
+
const moveToken = movedTypeToken(from, bare2, target, ownId);
|
|
47600
48572
|
const specsDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
47601
48573
|
const rewriteAll2 = (write6) => {
|
|
47602
|
-
if (
|
|
48574
|
+
if (from === "") return [];
|
|
47603
48575
|
const out = [];
|
|
47604
48576
|
for (const file of listFilesRecursive(specsDir, ".yaml")) {
|
|
47605
48577
|
let raw;
|
|
@@ -47609,7 +48581,7 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
|
|
|
47609
48581
|
continue;
|
|
47610
48582
|
}
|
|
47611
48583
|
const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
|
|
47612
|
-
if (!kind || !
|
|
48584
|
+
if (!kind || !respellTypeTokens(raw, moveToken)) continue;
|
|
47613
48585
|
if (write6) writeYamlFile(file, raw);
|
|
47614
48586
|
out.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
|
|
47615
48587
|
}
|
|
@@ -47656,22 +48628,33 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
|
|
|
47656
48628
|
return report3;
|
|
47657
48629
|
}
|
|
47658
48630
|
var TYPE_POSITION_KEYS = /* @__PURE__ */ new Set(["type", "returns", "signature", "signatureFrom", "typeDef", "references", "linkedEntity", "holds"]);
|
|
47659
|
-
|
|
48631
|
+
var QUALIFIED_TYPE_TOKEN = /(^|[^A-Za-z0-9_:.-])((?:::)?[A-Za-z0-9_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z0-9_][A-Za-z0-9_-]*)+)/g;
|
|
48632
|
+
function respellTypeTokens(raw, map) {
|
|
47660
48633
|
let changed = false;
|
|
47661
|
-
const
|
|
48634
|
+
const respell = (value) => value.replace(QUALIFIED_TYPE_TOKEN, (_m, lead, token) => `${lead}${map(token)}`);
|
|
47662
48635
|
const walk2 = (node) => {
|
|
47663
48636
|
if (Array.isArray(node)) {
|
|
47664
48637
|
node.forEach(walk2);
|
|
47665
48638
|
return;
|
|
47666
48639
|
}
|
|
47667
48640
|
if (!node || typeof node !== "object") return;
|
|
47668
|
-
|
|
48641
|
+
const holder = node;
|
|
48642
|
+
for (const [key2, value] of Object.entries(holder)) {
|
|
47669
48643
|
if (typeof value === "string" && TYPE_POSITION_KEYS.has(key2)) {
|
|
47670
|
-
const next = value
|
|
48644
|
+
const next = respell(value);
|
|
47671
48645
|
if (next !== value) {
|
|
47672
|
-
|
|
48646
|
+
holder[key2] = next;
|
|
47673
48647
|
changed = true;
|
|
47674
48648
|
}
|
|
48649
|
+
} else if (key2 === "assertsInvariants" && Array.isArray(value)) {
|
|
48650
|
+
value.forEach((ref, i) => {
|
|
48651
|
+
if (typeof ref !== "string") return;
|
|
48652
|
+
const next = respell(ref);
|
|
48653
|
+
if (next !== ref) {
|
|
48654
|
+
value[i] = next;
|
|
48655
|
+
changed = true;
|
|
48656
|
+
}
|
|
48657
|
+
});
|
|
47675
48658
|
} else if (value && typeof value === "object" && key2 !== "previousIds" && key2 !== "previousNames") {
|
|
47676
48659
|
walk2(value);
|
|
47677
48660
|
}
|
|
@@ -47680,6 +48663,23 @@ function respellQualifiedType(raw, oldToken, newToken) {
|
|
|
47680
48663
|
walk2(raw);
|
|
47681
48664
|
return changed;
|
|
47682
48665
|
}
|
|
48666
|
+
function movedTypeToken(from, bare2, target, ownId) {
|
|
48667
|
+
return (token) => {
|
|
48668
|
+
const lead = token.startsWith("::") ? "::" : "";
|
|
48669
|
+
const parts = token.slice(lead.length).split(/(::|\.)/);
|
|
48670
|
+
const segment = (i) => parts[2 * i];
|
|
48671
|
+
const count = (parts.length + 1) / 2;
|
|
48672
|
+
for (let i = 0; i + 1 < count && i <= 1; i++) {
|
|
48673
|
+
if (nameKey(segment(i)) !== nameKey(from) || nameKey(segment(i + 1)) !== nameKey(bare2)) continue;
|
|
48674
|
+
if (i === 1 && (lead !== "" || ownId === null || nameKey(segment(0)) !== nameKey(ownId))) continue;
|
|
48675
|
+
const next = [...parts];
|
|
48676
|
+
if (target !== void 0) next[2 * i] = target;
|
|
48677
|
+
else next.splice(2 * i, 2);
|
|
48678
|
+
return `${lead}${next.join("")}`;
|
|
48679
|
+
}
|
|
48680
|
+
return token;
|
|
48681
|
+
};
|
|
48682
|
+
}
|
|
47683
48683
|
function publishedUsesOf(removed, methods) {
|
|
47684
48684
|
const contracts = loadInterfaceSpecs();
|
|
47685
48685
|
const uses = [];
|
|
@@ -47748,7 +48748,16 @@ var PROSE_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
47748
48748
|
"note",
|
|
47749
48749
|
"caller"
|
|
47750
48750
|
]);
|
|
48751
|
+
function shouldPinSymbol(implementation, methodName, pinSymbol) {
|
|
48752
|
+
const realization = implementation.methods.find((m) => m.name === methodName);
|
|
48753
|
+
if (!realization || realization.symbol !== void 0 || pinSymbol === false) return false;
|
|
48754
|
+
if (pinSymbol === true) return true;
|
|
48755
|
+
const file = realization.sourcePath ?? implementation.sourcePath;
|
|
48756
|
+
return file !== void 0 && file !== "" && fs19.existsSync(path27.resolve(getProjectRoot(), file));
|
|
48757
|
+
}
|
|
47751
48758
|
function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
|
|
48759
|
+
const foreign = foreignIdRefusal(componentId, "chained-component", "rename its method", `sdd_rename_method ${componentId.slice(componentId.lastIndexOf("::") + 2)} <method> <new name>`);
|
|
48760
|
+
if (foreign !== null) throw new WaironError(foreign);
|
|
47752
48761
|
const component = loadComponentSpec(componentId);
|
|
47753
48762
|
if (!component) {
|
|
47754
48763
|
throw new WaironError(`component-missing: no component has the id "${componentId}".`);
|
|
@@ -47809,7 +48818,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
|
|
|
47809
48818
|
);
|
|
47810
48819
|
if (dryRun) {
|
|
47811
48820
|
const realizing = implementations.filter((impl) => movingContracts.has(impl.contract) && impl.methods.some((m) => m.name === methodName));
|
|
47812
|
-
const wouldPin =
|
|
48821
|
+
const wouldPin = realizing.some((impl) => shouldPinSymbol(impl, methodName, pinSymbol));
|
|
47813
48822
|
const wouldRewrite = [...rewriteRefFields(reachDir, methodRemap, void 0, true), ...rekeyLintAllows(reachDir, rename, true)];
|
|
47814
48823
|
return {
|
|
47815
48824
|
component: componentId,
|
|
@@ -47841,7 +48850,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
|
|
|
47841
48850
|
if (!movingContracts.has(implementation.contract)) continue;
|
|
47842
48851
|
const realization = implementation.methods.find((m) => m.name === methodName);
|
|
47843
48852
|
if (!realization) continue;
|
|
47844
|
-
const pin2 =
|
|
48853
|
+
const pin2 = shouldPinSymbol(implementation, methodName, pinSymbol);
|
|
47845
48854
|
if (pin2) pinnedSymbol = methodName;
|
|
47846
48855
|
saveImplementationSpec({
|
|
47847
48856
|
...implementation,
|
|
@@ -47935,15 +48944,29 @@ function respellLike(written, oldId, newId) {
|
|
|
47935
48944
|
return /^[A-Z]/.test(written) ? pascal(newId) : newId;
|
|
47936
48945
|
}
|
|
47937
48946
|
function memberRootWay(qualifiedId, call) {
|
|
48947
|
+
return projectRootWay(qualifiedId, call) ?? `It belongs to the member project "${qualifiedId.slice(0, qualifiedId.indexOf("::"))}": 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 ${call} there.`;
|
|
48948
|
+
}
|
|
48949
|
+
function projectRootWay(qualifiedId, call) {
|
|
47938
48950
|
const alias = qualifiedId.slice(0, qualifiedId.indexOf("::"));
|
|
47939
|
-
let folder;
|
|
47940
48951
|
try {
|
|
47941
48952
|
const node = graph().nodes.find((n) => n.namespace === alias || n.parent === "" && n.mountAlias === alias);
|
|
47942
|
-
if (node)
|
|
48953
|
+
if (node) {
|
|
48954
|
+
const folder = path27.relative(getProjectRoot(), node.directory).split(path27.sep).join("/") || ".";
|
|
48955
|
+
return `It belongs to the member project "${node.id ?? alias}" 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 ${call} there.`;
|
|
48956
|
+
}
|
|
47943
48957
|
} catch {
|
|
47944
48958
|
}
|
|
47945
|
-
const
|
|
47946
|
-
|
|
48959
|
+
const external = ownGet(projectConfigRepository.load()?.externals, alias);
|
|
48960
|
+
if (external !== void 0) {
|
|
48961
|
+
const producer = external?.project ?? alias;
|
|
48962
|
+
return `"${alias}" is the external "${producer}", a project this one only reads through its pin: a project's specs are written from its own root \u2014 open a session in that project's folder and call ${call} there.`;
|
|
48963
|
+
}
|
|
48964
|
+
return null;
|
|
48965
|
+
}
|
|
48966
|
+
function foreignIdRefusal(id, code, doing, call) {
|
|
48967
|
+
if (!id.includes("::")) return null;
|
|
48968
|
+
const way = projectRootWay(id, call);
|
|
48969
|
+
return way === null ? null : `${code}: "${id}" lives in another project; ${doing} from that project's own root. ${way}`;
|
|
47947
48970
|
}
|
|
47948
48971
|
function renameType(typeId, newId, dryRun) {
|
|
47949
48972
|
const all = loadTypeSpecs();
|
|
@@ -48219,6 +49242,8 @@ function publishedCarriers(type) {
|
|
|
48219
49242
|
return mergeUses([...out.values()]);
|
|
48220
49243
|
}
|
|
48221
49244
|
function renameParam(componentId, methodName, param, newName, dryRun) {
|
|
49245
|
+
const foreign = foreignIdRefusal(componentId, "chained-component", "rename its parameter", `sdd_rename_param ${componentId.slice(componentId.lastIndexOf("::") + 2)} ${methodName} ${param} <new name>`);
|
|
49246
|
+
if (foreign !== null) throw new WaironError(foreign);
|
|
48222
49247
|
const component = loadComponentSpec(componentId);
|
|
48223
49248
|
if (!component) throw new WaironError(`component-missing: no component has the id "${componentId}".`);
|
|
48224
49249
|
if (componentId.includes("::")) {
|
|
@@ -48557,8 +49582,8 @@ function walkForPackages(projectRoot, currentDir, depth, results) {
|
|
|
48557
49582
|
}
|
|
48558
49583
|
}
|
|
48559
49584
|
function pathToId(relPath) {
|
|
48560
|
-
const
|
|
48561
|
-
return
|
|
49585
|
+
const basename16 = path28.basename(relPath);
|
|
49586
|
+
return basename16.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
48562
49587
|
}
|
|
48563
49588
|
function pathToName(relPath) {
|
|
48564
49589
|
const id = pathToId(relPath);
|
|
@@ -49211,7 +50236,10 @@ function resolveLayer(delegate, implementers = false) {
|
|
|
49211
50236
|
if (owners.size === 1 && owners.has(comp.id)) add2(file);
|
|
49212
50237
|
}
|
|
49213
50238
|
for (const type of types.filter((t) => t.componentClass === comp.id && t.subsystem === comp.subsystem)) {
|
|
49214
|
-
for (const file of typeSourceFiles(type))
|
|
50239
|
+
for (const file of typeSourceFiles(type)) {
|
|
50240
|
+
const owners = claims.get(file);
|
|
50241
|
+
if (!owners || [...owners].every((o) => o === comp.id)) add2(file);
|
|
50242
|
+
}
|
|
49215
50243
|
}
|
|
49216
50244
|
const namesAny = compImpls.some((impl) => codeLocationsOf(impl).length > 0);
|
|
49217
50245
|
if (ownedPaths.length === 0 && !namesAny) {
|
|
@@ -49252,6 +50280,14 @@ function resolveLayer(delegate, implementers = false) {
|
|
|
49252
50280
|
updatedAt: comp.updatedAt
|
|
49253
50281
|
});
|
|
49254
50282
|
}
|
|
50283
|
+
const implementerRecords = agents.filter((a) => a.template === "implementer");
|
|
50284
|
+
const fencedBy = /* @__PURE__ */ new Map();
|
|
50285
|
+
for (const r of implementerRecords) for (const p of r.ownedPaths) fencedBy.set(p, (fencedBy.get(p) ?? 0) + 1);
|
|
50286
|
+
for (const r of implementerRecords) {
|
|
50287
|
+
if (!r.ownedPaths.some((p) => (fencedBy.get(p) ?? 0) > 1)) continue;
|
|
50288
|
+
r.ownedPaths = r.ownedPaths.filter((p) => (fencedBy.get(p) ?? 0) <= 1);
|
|
50289
|
+
r.writePaths = r.ownedPaths;
|
|
50290
|
+
}
|
|
49255
50291
|
}
|
|
49256
50292
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
49257
50293
|
for (const dom of loadConfig().domains) {
|
|
@@ -49316,7 +50352,7 @@ function composeAgentBrief(agentId) {
|
|
|
49316
50352
|
${codeFence.map(shown2).join("\n")}
|
|
49317
50353
|
` : sharedPaths.length > 0 && namesAnyCode(record) ? "Every file this agent's specs name is named by another component's specs too, so none is this agent's alone: they are listed as shared below.\n" : "No spec names a code location yet, so no code location is declared. The spawning session should declare the planned `sourcePath` on the implementation now \u2014 it is code linkage, not part of the approval, so declaring it costs no re-lock \u2014 and that file is then this agent's fence.\n";
|
|
49318
50354
|
const sharedSection = sharedPaths.length > 0 ? `
|
|
49319
|
-
Shared, owned by no single agent \u2014 create or extend these only for what this component needs (an import, a wiring line, a module setting), and name each one you touch in your report. A shared type file is created at its planned home exactly as its spec declares it, never redeclared in your own file:
|
|
50355
|
+
Shared, owned by no single agent \u2014 create or extend these only for what this component needs (an import, a wiring line, a module setting), and name each one you touch in your report. A shared type file is created at its planned home exactly as its spec declares it, never redeclared in your own file. A module root listed here (\`mod.rs\`, \`index.ts\`, \`__init__.py\`) may be another agent's file: add only the line that declares or re-exports your module:
|
|
49320
50356
|
|
|
49321
50357
|
${sharedPaths.map(shown2).join("\n")}
|
|
49322
50358
|
` : "";
|
|
@@ -49326,6 +50362,9 @@ ${sharedPaths.map(shown2).join("\n")}
|
|
|
49326
50362
|
## Code write fence
|
|
49327
50363
|
|
|
49328
50364
|
${ownSection}${sharedSection}${rule}`;
|
|
50365
|
+
instructions = `${instructions.trimEnd()}
|
|
50366
|
+
|
|
50367
|
+
${linkageSection(record)}`;
|
|
49329
50368
|
}
|
|
49330
50369
|
const guidance = loadAgentOverride(agentId);
|
|
49331
50370
|
if (guidance !== null) {
|
|
@@ -49352,9 +50391,15 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
|
|
|
49352
50391
|
let readPaths = record.readPaths;
|
|
49353
50392
|
if (externals.length > 0) {
|
|
49354
50393
|
const bindings = bindingModulesOf2(record);
|
|
50394
|
+
const membersOnly = externals.every((e) => e.member);
|
|
50395
|
+
const pinsOnly = externals.every((e) => !e.member);
|
|
50396
|
+
const source = membersOnly ? "the member's live L0 export table names them now" : pinsOnly ? "the pin has them" : "the pin has them (for a member, as its live L0 export table names them now)";
|
|
50397
|
+
const comparedWith = membersOnly ? "that table" : pinsOnly ? "the pin" : "the pin or the table";
|
|
49355
50398
|
const bindingNote = bindings.length > 0 ? `
|
|
49356
|
-
The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as
|
|
49357
|
-
` :
|
|
50399
|
+
The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as ${source} (\`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(", ")}.
|
|
50400
|
+
` : `
|
|
50401
|
+
When 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 ${comparedWith}.
|
|
50402
|
+
`;
|
|
49358
50403
|
instructions = `${instructions.trimEnd()}
|
|
49359
50404
|
|
|
49360
50405
|
## Externals used
|
|
@@ -49511,6 +50556,23 @@ var SHARED_SETUP_FILES = [
|
|
|
49511
50556
|
var SHARED_ROOT_FILES = ["index.ts", "index.js", "lib.rs", "main.rs", "mod.rs", "__init__.py"];
|
|
49512
50557
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".rs", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".kt", ".cs"]);
|
|
49513
50558
|
var MAX_UNNAMED_SIBLINGS = 8;
|
|
50559
|
+
function linkageSection(record) {
|
|
50560
|
+
const comps = scopeComponents(record);
|
|
50561
|
+
const ids = new Set(comps.map((c) => c.id));
|
|
50562
|
+
const contractOwner = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
|
|
50563
|
+
const impls = loadImplementationSpecs().filter((impl) => ids.has(contractOwner.get(impl.contract) ?? ""));
|
|
50564
|
+
const parts = [];
|
|
50565
|
+
if (comps.some((c) => c.componentType === "Portal")) {
|
|
50566
|
+
parts.push("## Handler shape\n\nEach verb's function takes the contract's OWN parameters, in order, under the contract's names. What its framework hands it besides those \u2014 a request, a response, a context, `next` \u2014 is wiring: name it in the implementation's `injectedParams` (it matches at the start or the end of the list, with or without a leading `_`). Two honest shapes: the router unpacks the path, query and body and calls the verb with the contract's parameters; or the function gets the request alone and reads each contract parameter off it by its own name (`req.params.<name>`, `req.query.<name>`, `req.body.<name>`, `url.searchParams.get('<name>')`). A `(req, res)` handler that reads nothing by the contract's names is reported as substitutions. Set the implementation's `router` to the entry its file exports, or to a central route table (`src/routes.ts#ROUTES`). `conformance: off` is only for generated or vendored code: it switches the realization checks off, never the doctrine. The full rules: `sdd-implement`, **Handler shape** and **Routers**.\n");
|
|
50567
|
+
}
|
|
50568
|
+
const declared = impls.filter((impl) => (impl.injectedParams ?? []).length > 0);
|
|
50569
|
+
const listed = declared.length > 0 ? ` Declared now: ${declared.map((impl) => `\`${impl.id}\`: [${(impl.injectedParams ?? []).join(", ")}]`).join("; ")}. These may be guesses written before any code existed: keep the ones your code really takes and remove the rest with \`sdd_update_spec\` (\`{"injectedParams": [{"value": "<name>", "action": "delete"}]}\`, or \`[]\` to clear) \u2014 an injection no function takes is UNUSED_INJECTED_PARAM.` : "";
|
|
50570
|
+
parts.push(`## Code linkage you own
|
|
50571
|
+
|
|
50572
|
+
\`injectedParams\` are code linkage, not design: declare them only when your code takes a parameter its framework or wiring imposes beside the contract's own (a request handle, a context), with \`sdd_write_narrative\` (\`injectedParams\`) or \`sdd_update_spec\` \u2014 no re-lock. A dependency held as a field or passed to a constructor is not one.${listed}
|
|
50573
|
+
`);
|
|
50574
|
+
return parts.join("\n");
|
|
50575
|
+
}
|
|
49514
50576
|
function namesAnyCode(record) {
|
|
49515
50577
|
const ids = new Set(scopeComponents(record).map((c) => c.id));
|
|
49516
50578
|
const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
|
|
@@ -49544,15 +50606,18 @@ function sharedFilesOf(record, fence) {
|
|
|
49544
50606
|
const parts = folder === "." ? [] : folder.split("/");
|
|
49545
50607
|
for (let i = 1; i <= parts.length; i++) chain.add(parts.slice(0, i).join("/"));
|
|
49546
50608
|
}
|
|
49547
|
-
const
|
|
50609
|
+
const join32 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
|
|
49548
50610
|
for (const dir of [...chain].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))) {
|
|
49549
|
-
for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(
|
|
49550
|
-
}
|
|
49551
|
-
for (const dir of ownFolders) {
|
|
49552
|
-
for (const file of SHARED_ROOT_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join31(dir, file));
|
|
50611
|
+
for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join32(dir, file));
|
|
49553
50612
|
}
|
|
49554
50613
|
const named2 = new Set(claims.keys());
|
|
49555
50614
|
for (const type of types) for (const file of typeSourceFiles(type)) named2.add(file);
|
|
50615
|
+
for (const dir of ownFolders) {
|
|
50616
|
+
for (const file of SHARED_ROOT_FILES) {
|
|
50617
|
+
const rootFile = join32(dir, file);
|
|
50618
|
+
if (named2.has(rootFile) || fs22.existsSync(path31.join(root, dir, file))) add2(rootFile);
|
|
50619
|
+
}
|
|
50620
|
+
}
|
|
49556
50621
|
let siblings = 0;
|
|
49557
50622
|
for (const dir of ownFolders) {
|
|
49558
50623
|
let entries = [];
|
|
@@ -49562,7 +50627,7 @@ function sharedFilesOf(record, fence) {
|
|
|
49562
50627
|
continue;
|
|
49563
50628
|
}
|
|
49564
50629
|
for (const entry of entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
49565
|
-
const file =
|
|
50630
|
+
const file = join32(dir, entry.name);
|
|
49566
50631
|
if (siblings >= MAX_UNNAMED_SIBLINGS) break;
|
|
49567
50632
|
if (!SOURCE_EXTENSIONS2.has(path31.extname(entry.name)) || named2.has(file) || owned.has(file) || out.includes(file)) continue;
|
|
49568
50633
|
if (/\.(test|spec)\.[a-z]+$/.test(entry.name)) continue;
|
|
@@ -49624,7 +50689,9 @@ function externalsUsedBy(record) {
|
|
|
49624
50689
|
const node = externals.has(alias) ? void 0 : memberOf(alias);
|
|
49625
50690
|
if (node) {
|
|
49626
50691
|
const system = path31.relative(root, path31.join(node.directory, ".wai", "specs", ".index.yaml")).replace(/\\/g, "/");
|
|
49627
|
-
|
|
50692
|
+
const publicName = memberPublicNames(path31.join(node.directory, ".wai", "specs", ".index.yaml"));
|
|
50693
|
+
const spelled = [...new Set([...names].map((n) => publicName.get(n) ?? n))].sort();
|
|
50694
|
+
out.push({ alias, names: spelled, pin: system, pinned: true, bindings: /* @__PURE__ */ new Map(), member: { id: node.id ?? node.namespace, system } });
|
|
49628
50695
|
continue;
|
|
49629
50696
|
}
|
|
49630
50697
|
const pin2 = `.wai/externals/${alias}.yaml`;
|
|
@@ -49642,6 +50709,19 @@ function externalsUsedBy(record) {
|
|
|
49642
50709
|
}
|
|
49643
50710
|
return out;
|
|
49644
50711
|
}
|
|
50712
|
+
function memberPublicNames(systemSpec) {
|
|
50713
|
+
const out = /* @__PURE__ */ new Map();
|
|
50714
|
+
try {
|
|
50715
|
+
const doc = readYamlFile(systemSpec);
|
|
50716
|
+
for (const entry of doc?.publicInterfaces ?? []) {
|
|
50717
|
+
const exported = entry.as ?? entry.name;
|
|
50718
|
+
const internal = entry.component ?? entry.typeDef;
|
|
50719
|
+
if (internal && exported && !out.has(internal)) out.set(internal, exported);
|
|
50720
|
+
}
|
|
50721
|
+
} catch {
|
|
50722
|
+
}
|
|
50723
|
+
return out;
|
|
50724
|
+
}
|
|
49645
50725
|
function bindingModulesOf2(record) {
|
|
49646
50726
|
const ids = new Set(implementedComponents(record).map((c) => c.id));
|
|
49647
50727
|
const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
|
|
@@ -51337,6 +52417,19 @@ function readStampVersion(content) {
|
|
|
51337
52417
|
// src/utils/ai-guide.ts
|
|
51338
52418
|
var GUIDE_MARKER_START = "<!-- wairon-guide-start -->";
|
|
51339
52419
|
var GUIDE_MARKER_END = "<!-- wairon-guide-end -->";
|
|
52420
|
+
var HUMAN_COMMANDS = `### What the human runs \u2014 recommend these, never run them
|
|
52421
|
+
The \`wairon\` CLI is the human developer's tool: you never run it, but when the human asks for something only it does, tell them the exact command:
|
|
52422
|
+
- \`wairon lock\` \u2014 approve the design (then commit \`.wai/lock.json\`); \`wairon lock-check\` is the CI merge gate on that approval.
|
|
52423
|
+
- \`wairon validate --ci\` \u2014 the CI gate. It FAILS on any error and on any warning, except the draft-related ones (a \`DRAFT_*\` warning, or an \`UNUSED_COMPONENT\` whose component is itself still draft or design status); notices never fail it, nor do the advisory live-externals findings. Say so plainly \u2014 no run is needed to know it.
|
|
52424
|
+
- \`wairon surface export --format openapi --portal <portal-id> --out <file>\` \u2014 a Portal's OpenAPI document (one per Portal); \`wairon surface diff\` \u2014 the public-surface changelog since the last approval; \`wairon export\` \u2014 the whole resolved design as one JSON document.
|
|
52425
|
+
- \`wairon externals pin <alias>\` \u2014 re-pin an external once its uses are adapted; \`wairon externals status\` \u2014 the live compatibility gate (exit 1 incompatible, 2 not compared).
|
|
52426
|
+
- \`wairon network declare\` \u2014 declare this project's network boundary (your tool for it is \`sdd_set_network\`).
|
|
52427
|
+
- \`wairon member add | attach | detach | adopt | promote | demote | internalize | move | rename-alias | update\` \u2014 the human's twins of the member tools; their \`--report\` is your \`dryRun\`.
|
|
52428
|
+
- \`wairon doctor --fix\` \u2014 rewrite deprecated forms; \`wairon generate\` \u2014 refresh the generated guides, skills and context.
|
|
52429
|
+
- \`wairon agent customize <id>\` \u2014 scaffold \`.wai/agents/<id>.md\`, guidance folded into every brief of that agent; \`wairon agent brief <id>\` \u2014 print a live brief.
|
|
52430
|
+
- \`wairon status\` \u2014 readiness and approval; \`wairon diagram\` \u2014 architecture diagrams; \`wairon network flows\` \u2014 the allowed-flows matrix.`;
|
|
52431
|
+
var LINKAGE_FACTS = `- **\`injectedParams\` are set when the code exists, by the implementer**: never declare them at design time. They name a parameter a framework imposes on written code (a request handle, a context) beside the contract's own; the implementer declares them with \`sdd_write_narrative\` (\`injectedParams\`) or \`sdd_update_spec\` once its code takes one, and removes a design-time guess its code does not take (\`UNUSED_INJECTED_PARAM\`) the same way \u2014 code linkage, no re-lock.
|
|
52432
|
+
- **Plain JavaScript is checked too**: a JSDoc \`@typedef\` with \`@property\` lines declares a plain-JS shape, and in a binding module its field names are compared with the producer's type like a TypeScript interface's \u2014 add one rather than telling the human a \`.js\`/\`.cjs\` binding's fields cannot be checked.`;
|
|
51340
52433
|
var GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
|
|
51341
52434
|
|
|
51342
52435
|
If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
|
|
@@ -51353,7 +52446,12 @@ If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore i
|
|
|
51353
52446
|
6. **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 waits for the human's re-lock; sequence spec turn \u2192 lock \u2192 code turn). Only code linkage is outside it.
|
|
51354
52447
|
7. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
|
|
51355
52448
|
8. **Members & References**: A project may declare **members** in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`). A **part** (the default) stores some of this project's subsystems in another folder or repository: local ids, this project's lock. A **project** member is an independent boundary with its own spec tree and lock, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.
|
|
51356
|
-
9. **Reachability**: every Portal verb is reached by a modelled caller or declared an entry (\`invokedBy: { kind: entry }\`) for real callers outside the design \u2014 never an entry invented to silence a finding
|
|
52449
|
+
9. **Reachability**: every Portal verb is reached by a modelled caller or declared an entry (\`invokedBy: { kind: entry }\`) for real callers outside the design \u2014 never an entry invented to silence a finding.
|
|
52450
|
+
|
|
52451
|
+
### Code linkage facts
|
|
52452
|
+
${LINKAGE_FACTS}
|
|
52453
|
+
|
|
52454
|
+
${HUMAN_COMMANDS}`;
|
|
51357
52455
|
var LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
|
|
51358
52456
|
|
|
51359
52457
|
This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 Narrative); agent topology and code are derived from it.
|
|
@@ -51369,14 +52467,17 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
|
|
|
51369
52467
|
- **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.
|
|
51370
52468
|
- **\`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\`).
|
|
51371
52469
|
- **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\`).
|
|
51372
|
-
- **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
|
|
52470
|
+
- **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands \u2014 and name the human's command when they ask for what only it does (see *What the human runs* below).
|
|
51373
52471
|
- **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.
|
|
51374
52472
|
- **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.
|
|
51375
52473
|
- **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.
|
|
51376
52474
|
- **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)\`.
|
|
52475
|
+
${LINKAGE_FACTS}
|
|
51377
52476
|
- **Externals \u2014 the pin gates, live drift is visible**: declare a project this one consumes with \`sdd_add_external\` (alias, and a source \`../sibling\`, \`hosted:<id>\`, \`<git url>\` or \`<git url>#<commit>\`) \u2014 never by hand-editing \`.wai/project.yaml\`; it is checked against the producer and pinned. Change its \`use\` imports with \`sdd_update_external\` and remove it (declaration and pin together) with \`sdd_remove_external\`. A name the producer exports to a narrower audience (\`project\` < \`department\` < \`instance\` < \`partner\` < \`external\`) than this project is read at is refused, naming both. The owner's gate judges every external against its pin. \`sdd_validate_tree\` and \`sdd_get_status\` also compare each external with its LIVE producer, offline, as ADVISORY findings (\`advisory: true\`): \`EXTERNAL_LIVE_INCOMPATIBLE\` (a used member changed, was renamed \u2014 the new name is given \u2014 or is gone), \`EXTERNAL_DRIFTED\`, \`EXTERNAL_LIVE_UNCOMPARED\`. They never make the tree invalid or fail \`--ci\`; the fix is to adapt the uses, then ask the human to re-pin (\`wairon externals pin <alias>\`). A git producer is compared live only by \`wairon externals status\` \u2014 the opt-in live CI gate (exit 1 incompatible, 2 not compared, 0 otherwise).
|
|
51378
52477
|
- **To implement code**: Delegate via the \`sdd-delegate\` skill: fetch the component's live brief with the \`sdd_get_agent_brief\` MCP tool (or the \`wairon-agent://\` resource) and spawn a subagent from it. Briefs are composed per call from the current spec tree, so they are always current \u2014 never wait for a restart. Implementations must match L3 interfaces and L5 narratives exactly. Generated agent files under \`.claude/agents/\` are an optional materialized view of the same topology \u2014 the live briefs are canonical.
|
|
51379
52478
|
|
|
52479
|
+
${HUMAN_COMMANDS}
|
|
52480
|
+
|
|
51380
52481
|
### Rules (enforced by \`sdd_validate_tree\`)
|
|
51381
52482
|
1. **Design before code**: Complete spec and pass validator before writing source code.
|
|
51382
52483
|
2. **Human-in-the-loop**: Ask user approval for each spec layer before proceeding.
|
|
@@ -52001,6 +53102,15 @@ function getStatusReport(options = {}, decor) {
|
|
|
52001
53102
|
const referencedKeys2 = new Set(family.nodes.flatMap((n) => n.externals.filter((e) => e.role === "member").map((e) => n.namespace === "" ? e.alias : `${n.namespace}::${e.alias}`)));
|
|
52002
53103
|
for (const absent of (options.approvals ?? []).filter((a) => a.key !== "" && a.as !== "part" && !referencedKeys2.has(a.key) && !family.nodes.some((n) => n.namespace === a.key))) {
|
|
52003
53104
|
output += `${mark.structure(" ")}${mark.missing(`[Project] ${absent.alias ?? absent.key} (no project on disk)`)}${approvalTag(absent)}
|
|
53105
|
+
`;
|
|
53106
|
+
}
|
|
53107
|
+
const dialledOff = implementations.flatMap((impl) => {
|
|
53108
|
+
if (impl.conformance === "off") return [`${impl.id} (every method)`];
|
|
53109
|
+
const off = impl.methods.filter((m) => m.conformance === "off").map((m) => m.name);
|
|
53110
|
+
return off.length > 0 ? [`${impl.id} (${off.join(", ")})`] : [];
|
|
53111
|
+
});
|
|
53112
|
+
if (dialledOff.length > 0) {
|
|
53113
|
+
output += `${mark.layer("system", "Conformance off:")} ${dialledOff.join(", ")} \u2014 realization not checked (method, parameters, async, narrated calls); the doctrine checks still run
|
|
52004
53114
|
`;
|
|
52005
53115
|
}
|
|
52006
53116
|
const own2 = options.approvals?.find((a) => a.key === "");
|
|
@@ -52218,7 +53328,14 @@ brief with \`sdd_get_agent_brief\`, and spawn a scoped subagent from it \u2014 i
|
|
|
52218
53328
|
its \`sharedPaths\` as the files it may touch only for its own needs. Briefs are composed
|
|
52219
53329
|
from the current spec tree on every call, so a re-lock never requires a session
|
|
52220
53330
|
restart \u2014 fetch fresh per delegation. \`wairon-skill://sdd-delegate\` carries the
|
|
52221
|
-
full flow
|
|
53331
|
+
full flow.
|
|
53332
|
+
|
|
53333
|
+
## Facts to state, not guess
|
|
53334
|
+
|
|
53335
|
+
\`injectedParams\` are never set at design time: the implementer sets them once its
|
|
53336
|
+
code takes one. The human runs the CLI: \`wairon lock\`; \`wairon validate --ci\`
|
|
53337
|
+
(fails on errors and non-draft warnings, never on notices); \`wairon surface export
|
|
53338
|
+
--format openapi --portal <id>\` for a Portal's OpenAPI.`;
|
|
52222
53339
|
}
|
|
52223
53340
|
function buildServerInstructions() {
|
|
52224
53341
|
const profile = governingProfile();
|
|
@@ -52384,6 +53501,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
52384
53501
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
52385
53502
|
var import_zod13 = require("zod");
|
|
52386
53503
|
var import_types17 = require("@modelcontextprotocol/sdk/types.js");
|
|
53504
|
+
var fs33 = __toESM(require("fs"));
|
|
52387
53505
|
var path51 = __toESM(require("path"));
|
|
52388
53506
|
var crypto10 = __toESM(require("crypto"));
|
|
52389
53507
|
|
|
@@ -52840,6 +53958,13 @@ function writeSpec(restatement) {
|
|
|
52840
53958
|
CREATE_TOOL[restatement.kind]
|
|
52841
53959
|
);
|
|
52842
53960
|
if (foreign !== void 0) throw new Error(foreign);
|
|
53961
|
+
const pathNames = pathBoundNames(restatement, id, parentRef);
|
|
53962
|
+
const notAnId = pathNames.find((n) => idPathProblem(n.value) !== null);
|
|
53963
|
+
if (notAnId) {
|
|
53964
|
+
throw new Error(
|
|
53965
|
+
`not-an-id: the ${notAnId.what} "${notAnId.value}" cannot name a spec \u2014 ${idPathProblem(notAnId.value)}. A spec id, and the subsystem, component or contract a spec is written under, become folder and file names: each is a plain id (letters, digits, "-" and "_", qualified by "::"), never a path. Nothing was written.`
|
|
53966
|
+
);
|
|
53967
|
+
}
|
|
52843
53968
|
const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
|
|
52844
53969
|
const existing = loadSpec(restatement.kind, id);
|
|
52845
53970
|
if (!existing && TRACED_KINDS.has(restatement.kind)) {
|
|
@@ -52902,6 +54027,24 @@ function writeSpec(restatement) {
|
|
|
52902
54027
|
...cascaded.length > 0 ? { cascaded } : {}
|
|
52903
54028
|
};
|
|
52904
54029
|
}
|
|
54030
|
+
var ID_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
54031
|
+
function idPathProblem(value) {
|
|
54032
|
+
const segments = value.split("::");
|
|
54033
|
+
const named2 = value.startsWith("::") ? segments.slice(1) : segments;
|
|
54034
|
+
const bad = named2.find((s) => !ID_SEGMENT.test(s));
|
|
54035
|
+
if (bad === void 0) return null;
|
|
54036
|
+
if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
|
|
54037
|
+
if (bad.includes(".")) return `"${bad}" holds a dot`;
|
|
54038
|
+
return bad === "" ? "it has an empty segment" : `"${bad}" holds a character outside letters, digits, "-" and "_"`;
|
|
54039
|
+
}
|
|
54040
|
+
function pathBoundNames(restatement, id, parentRef) {
|
|
54041
|
+
const out = [];
|
|
54042
|
+
if (restatement.kind !== "system" && typeof id === "string") out.push({ what: `${restatement.kind} id`, value: id });
|
|
54043
|
+
if (parentRef && typeof parentRef.id === "string" && parentRef.kind !== "system") out.push({ what: `${parentRef.kind} it is written under`, value: parentRef.id });
|
|
54044
|
+
const group = restatement.spec.group;
|
|
54045
|
+
if (restatement.kind === "type" && typeof group === "string" && group !== "") out.push({ what: "type group", value: group });
|
|
54046
|
+
return out;
|
|
54047
|
+
}
|
|
52905
54048
|
var CREATE_TOOL = {
|
|
52906
54049
|
system: "sdd_initialize_system",
|
|
52907
54050
|
subsystem: "sdd_add_subsystem",
|
|
@@ -53044,9 +54187,9 @@ function referencedRefusal(kind, id, references) {
|
|
|
53044
54187
|
${references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`).join("\n")}
|
|
53045
54188
|
Edit those specs first, or pass force: true to delete anyway and leave them dangling. Nothing was deleted; dryRun lists the whole plan.`;
|
|
53046
54189
|
}
|
|
53047
|
-
function updateSpecGated(kind, id, delta, dryRun) {
|
|
54190
|
+
function updateSpecGated(kind, id, delta, dryRun, tool = "sdd_update_spec") {
|
|
53048
54191
|
const bound2 = candidateOptions();
|
|
53049
|
-
const foreign = crossProjectWriteRefusal([{ kind, id }],
|
|
54192
|
+
const foreign = crossProjectWriteRefusal([{ kind, id }], tool);
|
|
53050
54193
|
if (foreign !== void 0) throw new Error(foreign);
|
|
53051
54194
|
const testRoots = bound2.rules?.conformance?.testRoots ?? [];
|
|
53052
54195
|
const stored = loadSpec(kind, id);
|
|
@@ -53140,30 +54283,69 @@ function moveMethods2(from, to, methods, dryRun) {
|
|
|
53140
54283
|
if (foreign !== void 0) throw new Error(foreign);
|
|
53141
54284
|
return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
|
|
53142
54285
|
}
|
|
53143
|
-
function moveSpec2(kind, id, subsystem, dryRun) {
|
|
54286
|
+
function moveSpec2(kind, id, subsystem, dryRun, together) {
|
|
54287
|
+
const ids = [.../* @__PURE__ */ new Set([id, ...together ?? []])];
|
|
53144
54288
|
const foreign = crossProjectWriteRefusal(
|
|
53145
|
-
[{ kind, id }, ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
|
|
54289
|
+
[...ids.map((sid) => ({ kind, id: sid })), ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
|
|
53146
54290
|
"sdd_move_spec"
|
|
53147
54291
|
);
|
|
53148
54292
|
if (foreign !== void 0) throw new Error(foreign);
|
|
53149
|
-
if (kind !== "component")
|
|
54293
|
+
if (kind !== "component") {
|
|
54294
|
+
const plans2 = ids.map((sid) => moveSpec(kind, sid, subsystem, true));
|
|
54295
|
+
if (dryRun) return foldMoves(plans2, true);
|
|
54296
|
+
return foldMoves(ids.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
|
|
54297
|
+
}
|
|
53150
54298
|
const bound2 = candidateOptions();
|
|
53151
|
-
const
|
|
54299
|
+
const owners = ids.filter((sid) => !ids.some((other) => other !== sid && ownsTransitively(other, sid)));
|
|
54300
|
+
const plans = owners.map((sid) => moveSpec(kind, sid, subsystem, true));
|
|
54301
|
+
const plan6 = foldMoves(plans, true);
|
|
53152
54302
|
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
54303
|
const [first, ...rest] = moving;
|
|
53154
54304
|
const verdict = first ? introducedFindings("component", first, bound2, rest) : { errors: [], warnings: [], notices: [] };
|
|
53155
54305
|
if (verdict.errors.length > 0) {
|
|
54306
|
+
const left = collaboratorsLeftBehind(moving.map((c) => c.id), plans.map((p) => p.from));
|
|
54307
|
+
const named2 = ids.length > 1 ? `components ${ids.map((sid) => `"${sid}"`).join(", ")}` : `component "${id}"`;
|
|
53156
54308
|
throw new Error(
|
|
53157
|
-
`Refused: moving
|
|
54309
|
+
`Refused: moving ${named2} 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
54310
|
${verdict.errors.map((e) => `- ${e.code}: ${e.message}`).join("\n")}
|
|
53159
54311
|
|
|
53160
|
-
Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it.`
|
|
54312
|
+
Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it` + (left.length > 0 ? ` in the same call \u2014 together: [${[...ids.filter((sid) => sid !== id), ...left].map((sid) => `"${sid}"`).join(", ")}] moves ${left.map((sid) => `"${sid}"`).join(", ")} along and judges the whole set as one move (dryRun first shows the plan).` : ".")
|
|
53161
54313
|
);
|
|
53162
54314
|
}
|
|
53163
54315
|
const notices = noticesFrom(verdict);
|
|
53164
|
-
const report3 = dryRun ? plan6 : moveSpec(kind,
|
|
54316
|
+
const report3 = dryRun ? plan6 : foldMoves(owners.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
|
|
53165
54317
|
return { ...report3, notices: [...report3.notices, ...notices] };
|
|
53166
54318
|
}
|
|
54319
|
+
function ownsTransitively(owner, member) {
|
|
54320
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54321
|
+
const visit = (cid) => {
|
|
54322
|
+
if (seen.has(cid)) return false;
|
|
54323
|
+
seen.add(cid);
|
|
54324
|
+
const owns = loadSpec("component", cid)?.owns ?? [];
|
|
54325
|
+
return owns.includes(member) || owns.some(visit);
|
|
54326
|
+
};
|
|
54327
|
+
return visit(owner);
|
|
54328
|
+
}
|
|
54329
|
+
function collaboratorsLeftBehind(moved, from) {
|
|
54330
|
+
const movedSet = new Set(moved);
|
|
54331
|
+
const sources = new Set(from);
|
|
54332
|
+
const all = scanAllSpecs({ memberDepth: 0 }).components;
|
|
54333
|
+
const components = all.filter((c) => sources.has(c.subsystem) && !movedSet.has(c.id));
|
|
54334
|
+
const movedSpecs = all.filter((c) => movedSet.has(c.id));
|
|
54335
|
+
const linked = components.filter((c) => (c.dependsOn ?? []).some((d) => movedSet.has(d)) || movedSpecs.some((m) => (m.dependsOn ?? []).includes(c.id)));
|
|
54336
|
+
const owned = new Set(components.flatMap((c) => c.owns ?? []));
|
|
54337
|
+
return [...new Set(linked.map((c) => owned.has(c.id) ? components.find((o) => (o.owns ?? []).includes(c.id))?.id ?? c.id : c.id))].sort();
|
|
54338
|
+
}
|
|
54339
|
+
function foldMoves(moves, dryRun) {
|
|
54340
|
+
const [first] = moves;
|
|
54341
|
+
return {
|
|
54342
|
+
...first,
|
|
54343
|
+
moved: moves.flatMap((m) => m.moved),
|
|
54344
|
+
rewritten: [...new Set(moves.flatMap((m) => m.rewritten))],
|
|
54345
|
+
notices: moves.flatMap((m) => m.notices),
|
|
54346
|
+
...dryRun ? { dryRun: true } : {}
|
|
54347
|
+
};
|
|
54348
|
+
}
|
|
53167
54349
|
|
|
53168
54350
|
// src/mcp/build.ts
|
|
53169
54351
|
var fs26 = __toESM(require("fs"));
|
|
@@ -53240,7 +54422,7 @@ function buildServerInstructions3() {
|
|
|
53240
54422
|
}
|
|
53241
54423
|
|
|
53242
54424
|
// src/network/adapters/files.ts
|
|
53243
|
-
var
|
|
54425
|
+
var import_yaml16 = require("yaml");
|
|
53244
54426
|
|
|
53245
54427
|
// src/network/types.ts
|
|
53246
54428
|
function workload(party) {
|
|
@@ -55077,7 +56259,10 @@ function planAttach(family, bound2, request) {
|
|
|
55077
56259
|
const config = configAt3(node.directory) ?? {};
|
|
55078
56260
|
const held = ownGet(config.members, alias);
|
|
55079
56261
|
const heldPath = held !== void 0 ? memberLocationOf(held) : void 0;
|
|
55080
|
-
if (dir !== null && heldPath !== void 0 && path45.resolve(node.directory, heldPath) === dir)
|
|
56262
|
+
if (dir !== null && heldPath !== void 0 && path45.resolve(node.directory, heldPath) === dir) {
|
|
56263
|
+
plan6.notes.push(`"${alias}" is already attached: ${label4(bound2)}'s members already declare ${alias} \u2192 ${heldPath}. Nothing to do.`);
|
|
56264
|
+
return plan6;
|
|
56265
|
+
}
|
|
55081
56266
|
if (!EXTERNAL_ALIAS_RE.test(alias)) refuse(plan6, "alias-invalid", bound2, `"${alias}" is no alias: ${aliasGrammarProblem(alias)}`);
|
|
55082
56267
|
else if (ownGet(config.members, alias) !== void 0 || ownGet(config.externals, alias) !== void 0) refuse(plan6, "alias-taken", bound2, `${label4(bound2)} already declares "${alias}"`);
|
|
55083
56268
|
if (dir !== null) {
|
|
@@ -57775,6 +58960,16 @@ var getSpecOutput = {
|
|
|
57775
58960
|
}).optional().describe(
|
|
57776
58961
|
"Present only when `methods` filtered the read. A filtered spec is a partial one: sdd_define_interface and sdd_write_narrative REPLACE the method list, so re-authoring from it would delete every method left out."
|
|
57777
58962
|
),
|
|
58963
|
+
pinnedSnapshot: import_zod13.z.object({
|
|
58964
|
+
alias: import_zod13.z.string().describe("The external's alias the read went through."),
|
|
58965
|
+
project: import_zod13.z.string().describe("The producer project the alias names."),
|
|
58966
|
+
file: import_zod13.z.string().describe("The pinned snapshot file the entry was read from, project-relative."),
|
|
58967
|
+
stateId: import_zod13.z.string().optional().describe("The producer's StateId the pin was taken at, when recorded."),
|
|
58968
|
+
readOnly: import_zod13.z.literal(true).describe("Always true: a pinned entry is the pin's record, written from the producer's own root."),
|
|
58969
|
+
note: import_zod13.z.string().describe("One sentence saying where the entry comes from and where it is written.")
|
|
58970
|
+
}).optional().describe(
|
|
58971
|
+
"Present only when the id named an EXTERNAL through its alias (`alias::name`): `spec` is then the entry the pinned snapshot records \u2014 read-only, never a spec of this tree, never written back."
|
|
58972
|
+
),
|
|
57778
58973
|
variantGuidance: import_zod13.z.object({
|
|
57779
58974
|
variant: import_zod13.z.string().describe("The variant id the component declares."),
|
|
57780
58975
|
base: import_zod13.z.string().describe("The core stereotype it specializes."),
|
|
@@ -57973,14 +59168,44 @@ function withArgumentNames(description, inputSchema) {
|
|
|
57973
59168
|
if (names.length === 0) return description;
|
|
57974
59169
|
return `${description.trimEnd()} Arguments: ${names.join(", ")}.`;
|
|
57975
59170
|
}
|
|
59171
|
+
var sessionFolder = null;
|
|
59172
|
+
function sameFolder(a, b) {
|
|
59173
|
+
const norm = (p) => {
|
|
59174
|
+
const resolved = path51.resolve(p);
|
|
59175
|
+
return process.platform === "win32" || process.platform === "darwin" ? resolved.toLowerCase() : resolved;
|
|
59176
|
+
};
|
|
59177
|
+
return norm(a) === norm(b);
|
|
59178
|
+
}
|
|
59179
|
+
function initializeFromElsewhereRefusal() {
|
|
59180
|
+
const existing = loadSystemSpec();
|
|
59181
|
+
if (sessionFolder === null || existing === null) return null;
|
|
59182
|
+
const root = getProjectRoot();
|
|
59183
|
+
if (sameFolder(sessionFolder, root)) return null;
|
|
59184
|
+
const rel2 = path51.relative(root, sessionFolder).split(path51.sep).join("/");
|
|
59185
|
+
const inside = rel2 !== "" && !rel2.startsWith("..") && !path51.isAbsolute(rel2);
|
|
59186
|
+
const alias = path51.basename(sessionFolder).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "member";
|
|
59187
|
+
return `Refused: this session was opened in ${sessionFolder}, which is not the root of the project it is bound to \u2014 ${root}, whose L0 "${existing.name}" already exists. sdd_initialize_system here would re-author THAT project's L0 (its name, vision, boundaries, requirements and targetLanguage). Nothing was written. ` + (inside ? `To make this folder a project of its own, as \`wairon init\` there says: from the root run \`wairon member add ${alias} ${rel2} --project\`, then open a session in this folder (its own guide and .mcp.json) and design it from there. ` : "") + `To re-author ${existing.name}'s L0 itself, open the session at ${root}.`;
|
|
59188
|
+
}
|
|
59189
|
+
function underTreeLock(handler) {
|
|
59190
|
+
try {
|
|
59191
|
+
lockTree2();
|
|
59192
|
+
} catch (e) {
|
|
59193
|
+
return errText(e instanceof Error ? e.message : String(e));
|
|
59194
|
+
}
|
|
59195
|
+
try {
|
|
59196
|
+
return handler();
|
|
59197
|
+
} finally {
|
|
59198
|
+
unlockTree2();
|
|
59199
|
+
}
|
|
59200
|
+
}
|
|
57976
59201
|
function reg(server, name, config, cb) {
|
|
57977
59202
|
const guarded2 = (args) => {
|
|
57978
59203
|
const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
|
|
57979
59204
|
if (freshness.writesRefused && SPEC_WRITE_TOOLS.has(name)) {
|
|
57980
59205
|
return errText(`${name} refused \u2014 ${freshness.reason}`);
|
|
57981
59206
|
}
|
|
57982
|
-
const
|
|
57983
|
-
const result = freshness.state === "fresh" ?
|
|
59207
|
+
const answered2 = SPEC_WRITE_TOOLS.has(name) ? underTreeLock(() => cb(args)) : cb(args);
|
|
59208
|
+
const result = freshness.state === "fresh" ? answered2 : markStale(answered2, freshness, name === "sdd_get_status" ? "lead" : "trail");
|
|
57984
59209
|
if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
|
|
57985
59210
|
return result;
|
|
57986
59211
|
};
|
|
@@ -58126,12 +59351,14 @@ function createBareMcpServer(options = {}) {
|
|
|
58126
59351
|
server,
|
|
58127
59352
|
"sdd_initialize_system",
|
|
58128
59353
|
{
|
|
58129
|
-
description: "Initialize the L0 System Specification (system.yaml). Re-running it on an existing system RE-AUTHORS it: the fields above are replaced, and everything this tool cannot express (databases, the project gateway publicInterfaces, diagram defaults) is carried forward. The answer carries a write receipt as structured content beside the sentence \u2014 what was written, and whether a system spec already existed \u2014 so a caller never has to read English to find out.",
|
|
59354
|
+
description: "Initialize the L0 System Specification (system.yaml). Re-running it on an existing system RE-AUTHORS it: the fields above are replaced, and everything this tool cannot express (databases, the project gateway publicInterfaces, diagram defaults) is carried forward. Refused, writing nothing, when the session was opened in a folder that is not the bound project's root (it bound upward to the project above it) and that project already has an L0: the refusal names the root and the next step \u2014 `wairon member add <alias> <path> --project` from the root makes the folder a project of its own. The answer carries a write receipt as structured content beside the sentence \u2014 what was written, and whether a system spec already existed \u2014 so a caller never has to read English to find out.",
|
|
58130
59355
|
inputSchema: systemInput,
|
|
58131
59356
|
outputSchema: specWriteReceiptOutput
|
|
58132
59357
|
},
|
|
58133
59358
|
({ name, vision, boundaries, globalRequirements, targetLanguage }) => {
|
|
58134
59359
|
try {
|
|
59360
|
+
const elsewhere = initializeFromElsewhereRefusal();
|
|
59361
|
+
if (elsewhere !== null) return errText(elsewhere);
|
|
58135
59362
|
const restatement = {
|
|
58136
59363
|
kind: "system",
|
|
58137
59364
|
spec: {
|
|
@@ -58237,7 +59464,7 @@ function createBareMcpServer(options = {}) {
|
|
|
58237
59464
|
if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
|
|
58238
59465
|
const delta = publicInterfacesReplacement(subsystem, sub.publicInterfaces ?? [], publicInterfaces);
|
|
58239
59466
|
if (typeof delta === "string") return errText(delta);
|
|
58240
|
-
const report3 = updateSpecGated("subsystem", subsystem, delta);
|
|
59467
|
+
const report3 = updateSpecGated("subsystem", subsystem, delta, void 0, "sdd_set_public_interfaces");
|
|
58241
59468
|
return structured(
|
|
58242
59469
|
`Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).
|
|
58243
59470
|
${renderChangeReport(report3)}`,
|
|
@@ -58263,9 +59490,15 @@ ${renderChangeReport(report3)}`,
|
|
|
58263
59490
|
},
|
|
58264
59491
|
({ alias, source, description, as }) => {
|
|
58265
59492
|
try {
|
|
59493
|
+
const root = getProjectRoot();
|
|
59494
|
+
const dir = typeof source === "string" && !/^[a-z][a-z0-9+.-]*:|^git@|#/i.test(source.trim()) ? path51.resolve(root, source.trim()) : null;
|
|
59495
|
+
const relToRoot = dir === null ? null : path51.relative(root, dir);
|
|
59496
|
+
const outside = dir !== null && relToRoot !== null && (relToRoot.startsWith("..") || path51.isAbsolute(relToRoot));
|
|
59497
|
+
const createdOutside = outside && !fs33.existsSync(dir);
|
|
58266
59498
|
const creation = createMember(alias, source, description, as);
|
|
58267
59499
|
const next = creation.as === "project" ? `${describeMemberPacks(creation)} Design it from its own root and export what others consume from its L0; reference it here as ${alias}::<name>.` : ` Its subsystems are this project's own: write them by their local ids. sdd_promote_member makes it a project when it needs its own team, release, approval or public surface.`;
|
|
58268
|
-
|
|
59500
|
+
const created = createdOutside ? `Created a new folder OUTSIDE this project, at ${dir} (beside ${root}). ` : "";
|
|
59501
|
+
return structured(`${created}Added the ${creation.as} "${alias}" at ${source}${creation.commit ? ` (pinned at ${creation.commit})` : ""}, declared in project.yaml \`members\`.${next}`, creation);
|
|
58269
59502
|
} catch (e) {
|
|
58270
59503
|
return errText(String(e));
|
|
58271
59504
|
}
|
|
@@ -58474,12 +59707,12 @@ ${renderChangeReport(report3)}`,
|
|
|
58474
59707
|
server,
|
|
58475
59708
|
"sdd_rename_method",
|
|
58476
59709
|
{
|
|
58477
|
-
description: "Rename a contract method and retarget every reference to it in the bound tree. The method moves on every interface of the component that declares it \u2014 its name, and the name inside its signature \u2014 and on the implementations of those contracts, carrying narrative, sourcePath, symbol, detail, intent and findings unchanged; an implementation that declared no symbol is pinned to the old name
|
|
59710
|
+
description: "Rename a contract method and retarget every reference to it in the bound tree. The method moves on every interface of the component that declares it \u2014 its name, and the name inside its signature \u2014 and on the implementations of those contracts, carrying narrative, sourcePath, symbol, detail, intent and findings unchanged; an implementation that declared no symbol is pinned to the old name when its code exists (the file realizing the method is on disk), so the function it already binds to keeps binding \u2014 with no code written yet nothing is pinned unless pinSymbol is true, and pinSymbol false never pins. Narrative call, register and dispatch steps naming this component and method, dispatch-table bindings and lifecycle entrypoints are retargeted, and so are the findings keyed on it: a lint allow at the method on a spec it moved in or covering `<component>.<method>`, and every debt-register entry (.wai/project.yaml rules.conformance.carried) keyed the same way \u2014 rewritten in place with every comment and all other formatting kept (a register that cannot be rewritten that precisely is refused before the first write). Prose is never rewritten and a gRPC endpoint binding keeps its wire method \u2014 renaming a contract method must not silently rename an RPC; both are reported as mentions. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename its method from that project's own root), a new name that is not an identifier in the method casing of the tree \u2014 the configured rules.naming.methods, else the convention of its targetLanguage: snake_case for Rust or Python, camelCase for TypeScript; for a component one of whose contracts implements another project's extension point, any identifier, since the producer names those methods (invalid-name), a method the component does not declare (method-missing), and a name a moving contract already declares (name-taken). Returns the specs the method moved in, the specs retargeted (lint allows included), the specs whose prose still names it, the pinned symbol when one was set, and `carried`: each register edit.",
|
|
58478
59711
|
inputSchema: {
|
|
58479
59712
|
id: import_zod13.z.string().describe("The component whose method is renamed (namespaced if needed)"),
|
|
58480
59713
|
method: import_zod13.z.string().describe("The method name as it stands"),
|
|
58481
59714
|
newName: import_zod13.z.string().describe("Its new name: an identifier in the method casing of the tree (snake_case in a Rust or Python tree, camelCase in a TypeScript one)"),
|
|
58482
|
-
pinSymbol: import_zod13.z.boolean().optional().describe("Whether an implementation that declares no symbol is pinned to the old name so its function still binds; true
|
|
59715
|
+
pinSymbol: import_zod13.z.boolean().optional().describe("Whether an implementation that declares no symbol is pinned to the old name so its function still binds: omitted, only when the file realizing the method exists; true always pins, false never does"),
|
|
58483
59716
|
dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would move, retarget and break (the consumers that call the method through an export), and write nothing"),
|
|
58484
59717
|
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")
|
|
58485
59718
|
}
|
|
@@ -58621,17 +59854,18 @@ ${renderChangeReport(report3)}`,
|
|
|
58621
59854
|
server,
|
|
58622
59855
|
"sdd_move_spec",
|
|
58623
59856
|
{
|
|
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
|
|
59857
|
+
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, in either spelling and wherever it stands in a type expression (`old::type` becomes `new::type`, `list<old.type>` becomes `list<new.type>`); omit subsystem to make a type system-level. Several specs of the same kind move as ONE move with `together` \u2014 a connected cluster (a subsystem split) whose edges no single move could keep inside one subsystem: every one is planned, the components are judged together in their new subsystem, and all are written or none. 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 \u2014 naming the collaborators left behind as the `together` list that takes them along \u2014 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
59858
|
inputSchema: {
|
|
58626
59859
|
kind: import_zod13.z.enum(["component", "type"]).describe("component or type"),
|
|
58627
59860
|
id: import_zod13.z.string().describe("The spec to move (a type by its bare id, or as <subsystem>::<id>)"),
|
|
58628
59861
|
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")
|
|
59862
|
+
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"),
|
|
59863
|
+
together: import_zod13.z.array(import_zod13.z.string()).optional().describe("More specs of the same kind that move with it to the same subsystem, as one move judged together \u2014 the collaborators a single move's refusal names")
|
|
58630
59864
|
}
|
|
58631
59865
|
},
|
|
58632
|
-
({ kind, id, subsystem, dryRun }) => {
|
|
59866
|
+
({ kind, id, subsystem, dryRun, together }) => {
|
|
58633
59867
|
try {
|
|
58634
|
-
return json(moveSpec2(kind, id, subsystem, dryRun));
|
|
59868
|
+
return json(moveSpec2(kind, id, subsystem, dryRun, together));
|
|
58635
59869
|
} catch (e) {
|
|
58636
59870
|
return errText(String(e));
|
|
58637
59871
|
}
|
|
@@ -58885,7 +60119,7 @@ ${renderChangeReport(report3)}`,
|
|
|
58885
60119
|
methods.push({ name: e.method, endpoint: parsed.data });
|
|
58886
60120
|
bound2.push(`${e.method}\u2192${transport}`);
|
|
58887
60121
|
}
|
|
58888
|
-
const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun);
|
|
60122
|
+
const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun, "sdd_set_endpoints");
|
|
58889
60123
|
return structured(
|
|
58890
60124
|
`${dryRun ? "Would bind" : "Bound"} ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
|
|
58891
60125
|
${renderChangeReport(report3)}`,
|
|
@@ -58957,7 +60191,7 @@ ${renderChangeReport(report3)}`,
|
|
|
58957
60191
|
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)"),
|
|
58958
60192
|
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"),
|
|
58959
60193
|
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.`),
|
|
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"),
|
|
60194
|
+
injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("Code linkage, set by the implementer once the code exists \u2014 never at design time: leave it out while designing, and remove a name the code does not take. 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"),
|
|
58961
60195
|
detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
|
|
58962
60196
|
conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
|
|
58963
60197
|
methods: import_zod13.z.array(import_zod13.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
|
|
@@ -58968,7 +60202,7 @@ ${renderChangeReport(report3)}`,
|
|
|
58968
60202
|
server,
|
|
58969
60203
|
"sdd_write_narrative",
|
|
58970
60204
|
{
|
|
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.",
|
|
60205
|
+
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). injectedParams are code linkage, set when the code exists: do NOT declare them while designing \u2014 the implementer declares them once its code takes such a parameter, and removes a design-time guess its code does not take (UNUSED_INJECTED_PARAM); neither asks for a re-lock. 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.",
|
|
58972
60206
|
inputSchema: implInput,
|
|
58973
60207
|
outputSchema: specWriteReceiptOutput
|
|
58974
60208
|
},
|
|
@@ -59152,7 +60386,7 @@ ${renderChangeReport(report3)}`,
|
|
|
59152
60386
|
server,
|
|
59153
60387
|
"sdd_get_spec",
|
|
59154
60388
|
{
|
|
59155
|
-
description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
|
|
60389
|
+
description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. An \`alias::name\` id resolves through the bound project's alias table exactly as a write resolves it: a member's spec reads under its key, and an EXTERNAL's spec reads from the pinned snapshot this project holds of it \u2014 the pinned contract entry or exported type as the spec, a read-only "pinnedSnapshot" marker beside it (it is written from the producer's own root). Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
|
|
59156
60390
|
inputSchema: {
|
|
59157
60391
|
kind: import_zod13.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).optional().describe("The kind of specification. Omit it to infer the kind from the id: the read is refused, naming the candidate kinds, when the id names specs at more than one level (an interface and an implementation sharing an id, say), or at none"),
|
|
59158
60392
|
id: import_zod13.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
|
|
@@ -59162,11 +60396,34 @@ ${renderChangeReport(report3)}`,
|
|
|
59162
60396
|
},
|
|
59163
60397
|
({ kind: askedKind, id, methods }) => {
|
|
59164
60398
|
try {
|
|
60399
|
+
const pinnedAnswer = (asked) => {
|
|
60400
|
+
if (!id.includes("::") || methods !== void 0) return null;
|
|
60401
|
+
const read2 = readPinnedSpec(asked, id);
|
|
60402
|
+
if (!read2) return null;
|
|
60403
|
+
const marker = {
|
|
60404
|
+
alias: read2.alias,
|
|
60405
|
+
project: read2.project,
|
|
60406
|
+
file: read2.file,
|
|
60407
|
+
...read2.stateId !== void 0 ? { stateId: read2.stateId } : {},
|
|
60408
|
+
readOnly: true,
|
|
60409
|
+
note: `"${read2.alias}" is an external of this project: this is its pinned ${read2.kind === "type" ? "type" : "contract entry"} "${read2.id}" as ${read2.file} records it \u2014 written from ${read2.project}'s own root, never here.`
|
|
60410
|
+
};
|
|
60411
|
+
return structured(JSON.stringify({ ...read2.spec, pinnedSnapshot: marker }, null, 2), {
|
|
60412
|
+
kind: read2.kind === "type" ? "type" : asked ?? "interface",
|
|
60413
|
+
id,
|
|
60414
|
+
spec: read2.spec,
|
|
60415
|
+
pinnedSnapshot: marker
|
|
60416
|
+
});
|
|
60417
|
+
};
|
|
59165
60418
|
let kind;
|
|
59166
60419
|
if (askedKind) {
|
|
59167
60420
|
kind = askedKind;
|
|
59168
60421
|
} else {
|
|
59169
60422
|
const holders = specKindsHolding(id);
|
|
60423
|
+
if (holders.length === 0) {
|
|
60424
|
+
const pinned = pinnedAnswer(void 0);
|
|
60425
|
+
if (pinned) return pinned;
|
|
60426
|
+
}
|
|
59170
60427
|
if (holders.length !== 1) {
|
|
59171
60428
|
return errText(holders.length === 0 ? `No spec of any kind has the ID "${id}".` : `The ID "${id}" names specs of more than one kind (${holders.join(", ")}). Pass "kind" to choose one.`);
|
|
59172
60429
|
}
|
|
@@ -59194,7 +60451,7 @@ ${renderChangeReport(report3)}`,
|
|
|
59194
60451
|
result = loadTypeSpec(id);
|
|
59195
60452
|
break;
|
|
59196
60453
|
}
|
|
59197
|
-
if (!result) return errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
|
|
60454
|
+
if (!result) return pinnedAnswer(kind) ?? errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
|
|
59198
60455
|
let resolvedSignatures = [];
|
|
59199
60456
|
if (kind === "interface") {
|
|
59200
60457
|
const answer = storedContractAnswer(result);
|
|
@@ -60005,6 +61262,7 @@ function loadProjectConfig3() {
|
|
|
60005
61262
|
localApprover,
|
|
60006
61263
|
localGuideFilePath,
|
|
60007
61264
|
localTypesOf,
|
|
61265
|
+
lockTree,
|
|
60008
61266
|
logger,
|
|
60009
61267
|
makeScopeFilter,
|
|
60010
61268
|
markSelectionsBundled,
|
|
@@ -60014,6 +61272,7 @@ function loadProjectConfig3() {
|
|
|
60014
61272
|
memberDeclarationOf,
|
|
60015
61273
|
memberDigest,
|
|
60016
61274
|
memberLocationOf,
|
|
61275
|
+
memberRevisions,
|
|
60017
61276
|
memberTypesOf,
|
|
60018
61277
|
methodCasingFor,
|
|
60019
61278
|
methodGenericParameters,
|
|
@@ -60062,6 +61321,7 @@ function loadProjectConfig3() {
|
|
|
60062
61321
|
readJsonFile,
|
|
60063
61322
|
readLockRecord,
|
|
60064
61323
|
readLockState,
|
|
61324
|
+
readPinnedSpec,
|
|
60065
61325
|
readRetiredReachForms,
|
|
60066
61326
|
readStampVersion,
|
|
60067
61327
|
readYamlFile,
|
|
@@ -60170,6 +61430,7 @@ function loadProjectConfig3() {
|
|
|
60170
61430
|
typeSourceFiles,
|
|
60171
61431
|
typeSpellingFacts,
|
|
60172
61432
|
uninstallPack,
|
|
61433
|
+
unlockTree,
|
|
60173
61434
|
updateMember,
|
|
60174
61435
|
updateSpec,
|
|
60175
61436
|
upsertPackSelection,
|