@wairon/cli 5.1.1-dev.10 → 5.1.1-dev.12

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 CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.10";
68
+ WAIRON_VERSION = "5.1.1-dev.12";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -186,11 +186,13 @@ __export(fs_exports, {
186
186
  fromProjectRoot: () => fromProjectRoot,
187
187
  getProjectRoot: () => getProjectRoot,
188
188
  getProjectRootOverride: () => getProjectRootOverride,
189
+ getRequestParentReach: () => getRequestParentReach,
189
190
  getRequestProjectRoot: () => getRequestProjectRoot,
190
191
  listFiles: () => listFiles,
191
192
  listFilesRecursive: () => listFilesRecursive,
192
193
  pathExists: () => pathExists,
193
194
  readFileOrNull: () => readFileOrNull,
195
+ runWithProjectBinding: () => runWithProjectBinding,
194
196
  runWithProjectRoot: () => runWithProjectRoot,
195
197
  setProjectRoot: () => setProjectRoot,
196
198
  writeFile: () => writeFile,
@@ -241,10 +243,21 @@ function listFilesRecursive(dirPath, ext) {
241
243
  return files;
242
244
  }
243
245
  function runWithProjectRoot(dir, fn) {
244
- return requestRootStore.run(path4.resolve(dir), fn);
246
+ return requestRootStore.run({ ...requestRootStore.getStore(), root: path4.resolve(dir) }, fn);
247
+ }
248
+ function runWithProjectBinding(dir, reach, fn) {
249
+ return requestRootStore.run(
250
+ { root: path4.resolve(dir), topRoot: path4.resolve(reach.topRoot), parentReach: reach.parentReach },
251
+ fn
252
+ );
245
253
  }
246
254
  function getRequestProjectRoot() {
247
- return requestRootStore.getStore() ?? null;
255
+ return requestRootStore.getStore()?.root ?? null;
256
+ }
257
+ function getRequestParentReach() {
258
+ const scope = requestRootStore.getStore();
259
+ if (!scope || scope.parentReach === void 0) return null;
260
+ return { topRoot: scope.topRoot, parentReach: scope.parentReach };
248
261
  }
249
262
  function setProjectRoot(dir) {
250
263
  projectRootOverride = dir === null ? null : path4.resolve(dir);
@@ -269,7 +282,7 @@ function findSystemRoot(startDir) {
269
282
  }
270
283
  }
271
284
  function getProjectRoot() {
272
- const scoped = requestRootStore.getStore();
285
+ const scoped = requestRootStore.getStore()?.root;
273
286
  if (scoped) return scoped;
274
287
  if (projectRootOverride) return projectRootOverride;
275
288
  const systemRoot = findSystemRoot(process.cwd());
@@ -3534,733 +3547,6 @@ var init_type_references = __esm({
3534
3547
  }
3535
3548
  });
3536
3549
 
3537
- // src/utils/filenames.ts
3538
- function safeFilenamePart(value) {
3539
- return value.replace(/[^a-zA-Z0-9._-]/g, "-");
3540
- }
3541
- var init_filenames = __esm({
3542
- "src/utils/filenames.ts"() {
3543
- "use strict";
3544
- }
3545
- });
3546
-
3547
- // src/core/openapi.ts
3548
- function schemaFor(typeRef, closureIds) {
3549
- const trimmed = typeRef.trim().replace(/^promise\s*<(.+)>$/i, "$1").trim();
3550
- const arrayMatch = /^(.+)\[\]$/.exec(trimmed);
3551
- if (arrayMatch) {
3552
- return { type: "array", items: schemaFor(arrayMatch[1], closureIds) };
3553
- }
3554
- const lower = trimmed.toLowerCase();
3555
- if (lower in PRIMITIVES) {
3556
- const p = PRIMITIVES[lower];
3557
- return p.type ? { ...p } : {};
3558
- }
3559
- const local = trimmed.split(/::|\./).pop().toLowerCase().replace(/[^a-z0-9_-]/g, "");
3560
- const hit = [...closureIds].find((id) => id.toLowerCase() === local);
3561
- if (hit) return { $ref: `#/components/schemas/${hit}` };
3562
- return { type: "object", description: `Unresolved type: ${trimmed}` };
3563
- }
3564
- function operationFor(method2, closureIds) {
3565
- const endpoint = method2.endpoint;
3566
- const httpVerb = endpoint && endpoint.transport === "HTTP" ? endpoint.method.toLowerCase() : "post";
3567
- const bodyVerbs = /* @__PURE__ */ new Set(["post", "put", "patch"]);
3568
- const params = method2.params ?? [];
3569
- const op = {
3570
- operationId: method2.name,
3571
- summary: method2.description,
3572
- responses: {
3573
- "200": {
3574
- description: method2.returns || "Success",
3575
- ...method2.returns && method2.returns.toLowerCase() !== "void" ? { content: { "application/json": { schema: schemaFor(method2.returns, closureIds) } } } : {}
3576
- }
3577
- }
3578
- };
3579
- if (method2.guarantees?.length) op["x-wairon-guarantees"] = method2.guarantees;
3580
- if (method2.effect) op["x-wairon-effect"] = method2.effect;
3581
- if (method2.ext && Object.keys(method2.ext).length) op["x-wairon-ext"] = method2.ext;
3582
- if (params.length) {
3583
- if (bodyVerbs.has(httpVerb)) {
3584
- op.requestBody = {
3585
- required: true,
3586
- content: {
3587
- "application/json": {
3588
- schema: {
3589
- type: "object",
3590
- properties: Object.fromEntries(params.map((p) => [p.name, schemaFor(p.type, closureIds)])),
3591
- required: params.filter((p) => !p.optional).map((p) => p.name)
3592
- }
3593
- }
3594
- }
3595
- };
3596
- } else {
3597
- op.parameters = params.map((p) => ({
3598
- name: p.name,
3599
- in: "query",
3600
- required: !p.optional,
3601
- ...p.description ? { description: p.description } : {},
3602
- schema: schemaFor(p.type, closureIds)
3603
- }));
3604
- }
3605
- }
3606
- return op;
3607
- }
3608
- function securitySchemeObject(auth) {
3609
- if (auth.scheme === "none") return null;
3610
- const desc = auth.description ? { description: auth.description } : {};
3611
- switch (auth.scheme) {
3612
- case "apiKey":
3613
- return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
3614
- case "bearer":
3615
- return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
3616
- case "basic":
3617
- return { type: "http", scheme: "basic", ...desc };
3618
- case "oauth2": {
3619
- const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
3620
- if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
3621
- if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
3622
- if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
3623
- return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
3624
- }
3625
- case "openIdConnect":
3626
- return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
3627
- case "custom":
3628
- return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
3629
- default:
3630
- return null;
3631
- }
3632
- }
3633
- function schemeBaseName(scheme) {
3634
- return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
3635
- }
3636
- function buildSecurity(entries) {
3637
- const schemes = {};
3638
- const nameByContent = /* @__PURE__ */ new Map();
3639
- const securityByEntry = /* @__PURE__ */ new Map();
3640
- for (const entry of entries) {
3641
- if (!entry.auth) continue;
3642
- const obj = securitySchemeObject(entry.auth);
3643
- if (!obj) continue;
3644
- const content = JSON.stringify(obj);
3645
- let name = nameByContent.get(content);
3646
- if (!name) {
3647
- name = schemeBaseName(entry.auth.scheme);
3648
- for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
3649
- schemes[name] = obj;
3650
- nameByContent.set(content, name);
3651
- }
3652
- const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
3653
- securityByEntry.set(entry.id, { [name]: scopeNames });
3654
- }
3655
- return { schemes, securityByEntry };
3656
- }
3657
- function httpEntriesOf(snapshot) {
3658
- return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
3659
- }
3660
- function renderDoc(snapshot, entries, closureIds, opts = {}) {
3661
- const { schemes, securityByEntry } = buildSecurity(entries);
3662
- const paths = {};
3663
- for (const entry of entries) {
3664
- const security = securityByEntry.get(entry.id);
3665
- for (const method2 of entry.methods) {
3666
- const endpoint = method2.endpoint;
3667
- if (!endpoint || endpoint.transport !== "HTTP") continue;
3668
- const p = endpoint.path.startsWith("/") ? endpoint.path : `/${endpoint.path}`;
3669
- paths[p] = paths[p] ?? {};
3670
- paths[p][endpoint.method.toLowerCase()] = {
3671
- tags: [entry.id],
3672
- ...operationFor(method2, closureIds),
3673
- ...security ? { security: [security] } : {}
3674
- };
3675
- }
3676
- }
3677
- const schemas = {};
3678
- for (const t of snapshot.types) {
3679
- schemas[t.id] = {
3680
- type: "object",
3681
- title: t.name,
3682
- properties: Object.fromEntries(t.fields.map((f) => [f.name, schemaFor(f.type, closureIds)])),
3683
- required: t.fields.filter((f) => !f.optional).map((f) => f.name)
3684
- };
3685
- }
3686
- const components = {};
3687
- if (Object.keys(schemas).length) components.schemas = schemas;
3688
- if (Object.keys(schemes).length) components.securitySchemes = schemes;
3689
- const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
3690
- const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
3691
- return {
3692
- openapi: "3.1.0",
3693
- info: {
3694
- title: opts.title ?? snapshot.projectName,
3695
- version: snapshot.version ?? "0.0.0",
3696
- ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
3697
- "x-wairon-origin": snapshot.origin,
3698
- "x-wairon-generated-at": snapshot.generatedAt
3699
- },
3700
- ...servers ? { servers } : {},
3701
- paths,
3702
- ...Object.keys(components).length ? { components } : {}
3703
- };
3704
- }
3705
- function toOpenApiSet(snapshot) {
3706
- const closureIds = new Set(snapshot.types.map((t) => t.id));
3707
- const byPortal = /* @__PURE__ */ new Map();
3708
- const order = [];
3709
- for (const entry of httpEntriesOf(snapshot)) {
3710
- if (!byPortal.has(entry.component)) {
3711
- byPortal.set(entry.component, []);
3712
- order.push(entry.component);
3713
- }
3714
- byPortal.get(entry.component).push(entry);
3715
- }
3716
- return order.map((portalId) => {
3717
- const entries = byPortal.get(portalId);
3718
- const name = entries[0]?.name ?? portalId;
3719
- return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
3720
- });
3721
- }
3722
- function isOpenApiDocument(body) {
3723
- try {
3724
- const parsed = yaml2.load(body);
3725
- return !!parsed && typeof parsed === "object" && typeof parsed.openapi === "string";
3726
- } catch {
3727
- return false;
3728
- }
3729
- }
3730
- function typeRefFromSchema(schema) {
3731
- if (!schema) return "json";
3732
- const ref = schema.$ref;
3733
- if (typeof ref === "string") return ref.split("/").pop() ?? "json";
3734
- if (schema.type === "array") {
3735
- return `${typeRefFromSchema(schema.items)}[]`;
3736
- }
3737
- const t = schema.type;
3738
- if (t === "integer") return "int";
3739
- if (typeof t === "string" && t !== "object") return t;
3740
- return "json";
3741
- }
3742
- function authFromSecurityScheme(scheme) {
3743
- const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
3744
- if (scheme.type === "apiKey") {
3745
- return {
3746
- scheme: "apiKey",
3747
- ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
3748
- ...typeof scheme.name === "string" ? { name: scheme.name } : {},
3749
- ...desc
3750
- };
3751
- }
3752
- if (scheme.type === "http") {
3753
- if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
3754
- if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
3755
- }
3756
- if (scheme.type === "oauth2") {
3757
- const flows = scheme.flows ?? {};
3758
- const flowKey = Object.keys(flows)[0];
3759
- const f = flows[flowKey] ?? {};
3760
- return {
3761
- scheme: "oauth2",
3762
- ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
3763
- ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
3764
- ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
3765
- ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
3766
- scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
3767
- ...desc
3768
- };
3769
- }
3770
- if (scheme.type === "openIdConnect") {
3771
- return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
3772
- }
3773
- return void 0;
3774
- }
3775
- function fromOpenApi(document, projectName) {
3776
- let parsed;
3777
- try {
3778
- parsed = yaml2.load(document);
3779
- } catch (e) {
3780
- throw new Error(`Invalid surface document: not parseable as JSON/YAML (${e instanceof Error ? e.message : String(e)})`);
3781
- }
3782
- if (!parsed || typeof parsed !== "object" || typeof parsed.openapi !== "string" || typeof parsed.paths !== "object") {
3783
- throw new Error('Invalid surface document: missing OpenAPI "openapi"/"paths" structure.');
3784
- }
3785
- const info = parsed.info ?? {};
3786
- const methods = [];
3787
- for (const [rawPath, ops] of Object.entries(parsed.paths)) {
3788
- for (const [verb, opRaw] of Object.entries(ops ?? {})) {
3789
- if (!["get", "post", "put", "delete", "patch", "options", "head"].includes(verb)) continue;
3790
- const op = opRaw ?? {};
3791
- const name = typeof op.operationId === "string" && /^[a-zA-Z0-9_]+$/.test(op.operationId) ? op.operationId : `${verb}_${rawPath.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
3792
- const params = [];
3793
- for (const p of op.parameters ?? []) {
3794
- if (typeof p.name !== "string") continue;
3795
- params.push({
3796
- name: p.name,
3797
- type: typeRefFromSchema(p.schema),
3798
- ...p.required === true ? {} : { optional: true },
3799
- ...typeof p.description === "string" ? { description: p.description } : {}
3800
- });
3801
- }
3802
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
3803
- if (bodySchema) {
3804
- const props = bodySchema.properties ?? {};
3805
- const required = new Set(bodySchema.required ?? []);
3806
- if (Object.keys(props).length) {
3807
- for (const [pname, pschema] of Object.entries(props)) {
3808
- params.push({ name: pname, type: typeRefFromSchema(pschema), ...required.has(pname) ? {} : { optional: true } });
3809
- }
3810
- } else {
3811
- params.push({ name: "body", type: typeRefFromSchema(bodySchema) });
3812
- }
3813
- }
3814
- const okResponse = op.responses?.["200"] ?? op.responses?.["201"];
3815
- const responseSchema = okResponse?.content?.["application/json"]?.schema;
3816
- const returns = responseSchema ? typeRefFromSchema(responseSchema) : "void";
3817
- const rawGuarantees = op["x-wairon-guarantees"];
3818
- const guarantees = Array.isArray(rawGuarantees) ? rawGuarantees.filter((g) => typeof g === "string" && g.length > 0) : [];
3819
- const rawEffect = op["x-wairon-effect"];
3820
- const effect = rawEffect === "read" || rawEffect === "write" ? rawEffect : void 0;
3821
- const rawExt = op["x-wairon-ext"];
3822
- const ext = rawExt && typeof rawExt === "object" && !Array.isArray(rawExt) ? rawExt : void 0;
3823
- methods.push({
3824
- name,
3825
- description: typeof op.summary === "string" ? op.summary : typeof op.description === "string" ? op.description : name,
3826
- signature: `${name}(${params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${returns}`,
3827
- returns,
3828
- params,
3829
- endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath },
3830
- ...guarantees.length ? { guarantees } : {},
3831
- ...effect ? { effect } : {},
3832
- ...ext ? { ext } : {}
3833
- });
3834
- }
3835
- }
3836
- const types = [];
3837
- const schemas = parsed.components?.schemas ?? {};
3838
- for (const [id, schema] of Object.entries(schemas)) {
3839
- const props = schema.properties ?? {};
3840
- const required = new Set(schema.required ?? []);
3841
- types.push({
3842
- id,
3843
- name: typeof schema.title === "string" ? schema.title : id,
3844
- kind: "value-object",
3845
- fields: Object.entries(props).map(([fname, fschema]) => ({
3846
- name: fname,
3847
- type: typeRefFromSchema(fschema),
3848
- ...required.has(fname) ? {} : { optional: true }
3849
- }))
3850
- });
3851
- }
3852
- const securitySchemes = parsed.components?.securitySchemes ?? {};
3853
- const firstScheme = Object.values(securitySchemes)[0];
3854
- const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
3855
- const entry = {
3856
- id: `${projectName}-api`,
3857
- name: typeof info.title === "string" ? info.title : projectName,
3858
- audience: "external",
3859
- type: "REST",
3860
- component: `${projectName}-api`,
3861
- methods,
3862
- details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
3863
- ...typeof info.version === "string" ? { version: info.version } : {},
3864
- ...importedAuth ? { auth: importedAuth } : {}
3865
- };
3866
- return SurfaceSnapshotSchema.parse({
3867
- projectName,
3868
- origin: "authored",
3869
- ...typeof info.version === "string" ? { version: info.version } : {},
3870
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3871
- interfaces: [entry],
3872
- types
3873
- });
3874
- }
3875
- var yaml2, PRIMITIVES;
3876
- var init_openapi = __esm({
3877
- "src/core/openapi.ts"() {
3878
- "use strict";
3879
- yaml2 = __toESM(require("js-yaml"));
3880
- init_models();
3881
- PRIMITIVES = {
3882
- string: { type: "string" },
3883
- number: { type: "number" },
3884
- float: { type: "number" },
3885
- decimal: { type: "number" },
3886
- int: { type: "integer" },
3887
- integer: { type: "integer" },
3888
- boolean: { type: "boolean" },
3889
- bool: { type: "boolean" },
3890
- date: { type: "string", format: "date-time" },
3891
- datetime: { type: "string", format: "date-time" },
3892
- uuid: { type: "string", format: "uuid" },
3893
- json: { type: "object" },
3894
- object: { type: "object" },
3895
- any: {},
3896
- unknown: {},
3897
- void: {}
3898
- };
3899
- }
3900
- });
3901
-
3902
- // src/core/surfaces.ts
3903
- function surfacesDir(rootDir) {
3904
- return path11.join(rootDir, ".wai", SURFACES_DIRNAME);
3905
- }
3906
- function audienceRank(audience) {
3907
- const idx = SURFACE_AUDIENCES.indexOf(audience ?? "instance");
3908
- return idx === -1 ? SURFACE_AUDIENCES.indexOf("instance") : idx;
3909
- }
3910
- function stateIdString() {
3911
- const s = computeStateId();
3912
- return `${s.algorithm}:${s.digest}`;
3913
- }
3914
- function computeTypeClosure(entries, types) {
3915
- const included = /* @__PURE__ */ new Map();
3916
- const queue = [];
3917
- const enqueueRef = (ref) => {
3918
- if (BUILTIN_TYPES.has(ref.toLowerCase())) return;
3919
- for (const spec of types) {
3920
- const qualifiedId2 = spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
3921
- if (matchTypeRef(ref, qualifiedId2) && !included.has(spec.id)) {
3922
- included.set(spec.id, spec);
3923
- queue.push(spec.id);
3924
- }
3925
- }
3926
- };
3927
- for (const entry of entries) {
3928
- for (const m of entry.methods) {
3929
- for (const ref of methodTypeRefs(m)) enqueueRef(ref);
3930
- }
3931
- }
3932
- while (queue.length) {
3933
- const spec = included.get(queue.shift());
3934
- for (const field of spec.fields) {
3935
- for (const ref of extractTypeIdentifiers(field.type)) enqueueRef(ref);
3936
- }
3937
- }
3938
- return [...included.values()].map((t) => ({
3939
- id: t.id,
3940
- name: t.name,
3941
- kind: t.kind,
3942
- fields: t.fields.map((f) => ({
3943
- name: f.name,
3944
- type: f.type,
3945
- ...f.description ? { description: f.description } : {},
3946
- ...f.optional ? { optional: true } : {}
3947
- }))
3948
- }));
3949
- }
3950
- function projectOwnSurface(maxAudience) {
3951
- const system = loadSystemSpec();
3952
- if (!system) {
3953
- throw new Error("Cannot project a surface: the L0 system spec is missing.");
3954
- }
3955
- const subsystems = loadSubsystemSpecs();
3956
- const components = loadComponentSpecs();
3957
- const interfaces = loadInterfaceSpecs();
3958
- const types = loadTypeSpecs();
3959
- const floor = audienceRank(maxAudience);
3960
- const rawEntries = system.publicInterfaces ?? [];
3961
- const entries = [];
3962
- for (const raw of rawEntries) {
3963
- const audience = raw.audience ?? "instance";
3964
- if (audienceRank(audience) < floor) continue;
3965
- if (!raw.component) continue;
3966
- const comp = components.find((c) => c.id === raw.component);
3967
- if (!comp) continue;
3968
- const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!raw.interface || i.id === raw.interface));
3969
- const methods = compInterfaces.flatMap((i) => i.methods);
3970
- const subsystemType = subsystems.find((s) => s.id === comp.subsystem)?.publicInterfaces.find((pi) => pi.component === comp.id)?.type;
3971
- entries.push({
3972
- id: raw.id ?? raw.interface ?? comp.id,
3973
- name: raw.name ?? comp.name,
3974
- audience,
3975
- type: raw.type ?? subsystemType ?? "Custom",
3976
- component: comp.id,
3977
- methods,
3978
- ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
3979
- // Project the backing Portal's auth + basePath so the codec can emit
3980
- // OpenAPI security + per-portal servers self-contained from the snapshot.
3981
- ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
3982
- ...comp.basePath ? { basePath: comp.basePath } : {},
3983
- details: raw.details ?? "",
3984
- ...raw.version ? { version: raw.version } : {},
3985
- ...raw.stability ? { stability: raw.stability } : {}
3986
- });
3987
- }
3988
- return SurfaceSnapshotSchema.parse({
3989
- projectName: system.name,
3990
- origin: "generated",
3991
- stateId: stateIdString(),
3992
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3993
- interfaces: entries,
3994
- types: computeTypeClosure(entries, types)
3995
- });
3996
- }
3997
- function projectChildSurface() {
3998
- return projectOwnSurface("project");
3999
- }
4000
- function localName(id) {
4001
- return id.split("::").pop();
4002
- }
4003
- function projectSubsystemSurface(subsystemId) {
4004
- const system = loadSystemSpec();
4005
- if (!system) {
4006
- throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
4007
- }
4008
- const subsystems = loadSubsystemSpecs();
4009
- const target = subsystems.find((s) => s.id === subsystemId);
4010
- if (!target) {
4011
- throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
4012
- }
4013
- const components = loadComponentSpecs();
4014
- const interfaces = loadInterfaceSpecs();
4015
- const types = loadTypeSpecs();
4016
- const entries = [];
4017
- const unprojectable = [];
4018
- for (const pub of target.publicInterfaces ?? []) {
4019
- if (!pub.component) continue;
4020
- const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
4021
- if (!comp) continue;
4022
- if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
4023
- unprojectable.push({ component: pub.component, componentType: comp.componentType });
4024
- continue;
4025
- }
4026
- const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
4027
- const methods = compInterfaces.flatMap((i) => i.methods);
4028
- entries.push({
4029
- id: localName(pub.interface ?? comp.id),
4030
- name: comp.name,
4031
- // Family ceiling: a sibling surface is consumable by the system family only.
4032
- audience: "project",
4033
- type: pub.type ?? "Custom",
4034
- // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
4035
- // refs by their final segment.
4036
- component: localName(comp.id),
4037
- methods,
4038
- ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
4039
- // Project the backing component's auth + basePath so the codec can emit
4040
- // OpenAPI security + per-portal servers self-contained from the snapshot.
4041
- ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
4042
- ...comp.basePath ? { basePath: comp.basePath } : {},
4043
- details: pub.details ?? ""
4044
- });
4045
- }
4046
- for (const skipped of unprojectable) {
4047
- console.error(
4048
- `[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, a Gateway, or an Observer (for events).`
4049
- );
4050
- }
4051
- return SurfaceSnapshotSchema.parse({
4052
- projectName: `${system.name}::${subsystemId}`,
4053
- origin: "generated",
4054
- stateId: stateIdString(),
4055
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4056
- interfaces: entries,
4057
- types: computeTypeClosure(entries, types)
4058
- });
4059
- }
4060
- function listSnapshots(rootDir = getProjectRoot()) {
4061
- const dir = surfacesDir(rootDir);
4062
- if (!fs9.existsSync(dir)) return [];
4063
- const out = [];
4064
- for (const file of fs9.readdirSync(dir)) {
4065
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
4066
- try {
4067
- out.push(SurfaceSnapshotSchema.parse(readYamlFile(path11.join(dir, file))));
4068
- } catch {
4069
- }
4070
- }
4071
- return out;
4072
- }
4073
- function getSnapshot(projectName, rootDir = getProjectRoot()) {
4074
- return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
4075
- }
4076
- function snapshotFilename(projectName) {
4077
- return `${safeFilenamePart(projectName)}.yaml`;
4078
- }
4079
- function writeSnapshotIfChanged(snapshot, rootDir) {
4080
- const dir = surfacesDir(rootDir);
4081
- fs9.mkdirSync(dir, { recursive: true });
4082
- const p = path11.join(dir, snapshotFilename(snapshot.projectName));
4083
- const next = SurfaceSnapshotSchema.parse(snapshot);
4084
- if (fs9.existsSync(p)) {
4085
- try {
4086
- const existing = SurfaceSnapshotSchema.parse(readYamlFile(p));
4087
- if (surfaceContentKey(existing) === surfaceContentKey(next)) {
4088
- return { path: p, changed: false };
4089
- }
4090
- } catch {
4091
- }
4092
- }
4093
- writeYamlFile(p, next);
4094
- return { path: p, changed: true };
4095
- }
4096
- function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
4097
- return writeSnapshotIfChanged(snapshot, rootDir).path;
4098
- }
4099
- function loadSurfaceSnapshots() {
4100
- return listSnapshots();
4101
- }
4102
- function selectPortalSpec(renderedSet, portalId) {
4103
- const hit = renderedSet.find((spec) => spec.portalId === portalId);
4104
- if (!hit) {
4105
- const known = renderedSet.map((s) => s.portalId).join(", ");
4106
- throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
4107
- }
4108
- return [hit];
4109
- }
4110
- function perPortalPath(resolvedOut, portalId) {
4111
- const ext = path11.extname(resolvedOut);
4112
- const stem2 = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
4113
- return `${stem2}.${safeFilenamePart(portalId)}${ext}`;
4114
- }
4115
- function writeSurfaceFile(outPath, snapshot, renderedSet) {
4116
- const resolved = path11.resolve(outPath);
4117
- fs9.mkdirSync(path11.dirname(resolved), { recursive: true });
4118
- if (!renderedSet || renderedSet.length === 0) {
4119
- writeYamlFile(resolved, snapshot);
4120
- return [resolved];
4121
- }
4122
- if (renderedSet.length === 1) {
4123
- fs9.writeFileSync(resolved, renderedSet[0].document);
4124
- return [resolved];
4125
- }
4126
- return renderedSet.map((spec) => {
4127
- const target = perPortalPath(resolved, spec.portalId);
4128
- fs9.writeFileSync(target, spec.document);
4129
- return target;
4130
- });
4131
- }
4132
- function exportResult(snapshot, renderedSet, writtenPaths) {
4133
- const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
4134
- return {
4135
- snapshot,
4136
- ...rendered !== void 0 ? { rendered } : {},
4137
- ...renderedSet ? { renderedSet } : {},
4138
- ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
4139
- ...writtenPaths.length ? { writtenPaths } : {}
4140
- };
4141
- }
4142
- function exportSurface(maxAudience, format, outPath, portalId) {
4143
- const snapshot = projectOwnSurface(maxAudience);
4144
- let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
4145
- if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
4146
- const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
4147
- return exportResult(snapshot, renderedSet, writtenPaths);
4148
- }
4149
- function importSurface(sourcePath, origin) {
4150
- const resolved = path11.resolve(sourcePath);
4151
- if (!fs9.existsSync(resolved)) {
4152
- throw new Error(`Surface document not found: ${resolved}`);
4153
- }
4154
- const body = fs9.readFileSync(resolved, "utf8");
4155
- let snapshot;
4156
- if (isOpenApiDocument(body)) {
4157
- const projectName = path11.basename(resolved).replace(/\.(json|ya?ml)$/i, "");
4158
- snapshot = fromOpenApi(body, projectName);
4159
- snapshot = { ...snapshot, origin };
4160
- } else {
4161
- snapshot = SurfaceSnapshotSchema.parse(readYamlFile(resolved));
4162
- snapshot = { ...snapshot, origin };
4163
- }
4164
- saveSnapshot(snapshot);
4165
- return snapshot;
4166
- }
4167
- function generateChildSnapshots(rootDir = getProjectRoot()) {
4168
- const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
4169
- const children = topLevel.filter((s) => s.projectPath);
4170
- if (!children.length) return [];
4171
- const familySnapshot = projectChildSurface();
4172
- const siblingSnapshots = /* @__PURE__ */ new Map();
4173
- const siblingSurface = (subsystemId) => {
4174
- let snap = siblingSnapshots.get(subsystemId);
4175
- if (!snap) {
4176
- snap = projectSubsystemSurface(subsystemId);
4177
- siblingSnapshots.set(subsystemId, snap);
4178
- }
4179
- return snap;
4180
- };
4181
- const written = [];
4182
- const record2 = (r) => {
4183
- if (r.changed) written.push(r.path);
4184
- };
4185
- for (const child of children) {
4186
- const childDir = path11.resolve(rootDir, child.projectPath);
4187
- if (!fs9.existsSync(childDir)) continue;
4188
- record2(writeSnapshotIfChanged(familySnapshot, childDir));
4189
- for (const sibling of topLevel) {
4190
- if (sibling.id === child.id) continue;
4191
- record2(writeSnapshotIfChanged(siblingSurface(sibling.id), childDir));
4192
- }
4193
- }
4194
- return written;
4195
- }
4196
- function computeParentStateId(parentRoot) {
4197
- return computeStateIdAt(parentRoot);
4198
- }
4199
- function listExternalInterfaces() {
4200
- const snapshots = listSnapshots();
4201
- const chainingParent = resolveChainingParent();
4202
- const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
4203
- return snapshots.map((snapshot) => {
4204
- const generated = snapshot.origin === "generated";
4205
- const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
4206
- const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
4207
- return {
4208
- projectName: snapshot.projectName,
4209
- origin: snapshot.origin,
4210
- sourceKind,
4211
- generatedAt: snapshot.generatedAt,
4212
- ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
4213
- ...snapshot.version ? { version: snapshot.version } : {},
4214
- freshness,
4215
- interfaceIds: snapshot.interfaces.map((e) => e.id)
4216
- };
4217
- });
4218
- }
4219
- function surfaceContentKey(snapshot) {
4220
- const { stateId, generatedAt, origin, ...content } = snapshot;
4221
- return JSON.stringify(content);
4222
- }
4223
- function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
4224
- const system = loadSystemSpec();
4225
- if (!system) return [];
4226
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
4227
- if (!children.length) return [];
4228
- const current2 = surfaceContentKey(projectChildSurface());
4229
- const issues = [];
4230
- for (const child of children) {
4231
- const childDir = path11.resolve(rootDir, child.projectPath);
4232
- const held = getSnapshot(system.name, childDir);
4233
- if (!held || held.origin !== "generated") continue;
4234
- if (surfaceContentKey(held) !== current2) {
4235
- issues.push({
4236
- severity: "warning",
4237
- code: "SURFACE_STALE",
4238
- message: `Chained child "${child.id}" holds a parent surface snapshot whose contracts no longer match the current tree \u2014 rerun \`wairon surface generate-children\` so the child validates against the current surface.`,
4239
- specId: child.id
4240
- });
4241
- }
4242
- }
4243
- return issues;
4244
- }
4245
- var fs9, path11, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
4246
- var init_surfaces = __esm({
4247
- "src/core/surfaces.ts"() {
4248
- "use strict";
4249
- fs9 = __toESM(require("fs"));
4250
- path11 = __toESM(require("path"));
4251
- init_fs();
4252
- init_yaml();
4253
- init_filenames();
4254
- init_models();
4255
- init_specs2();
4256
- init_statehash();
4257
- init_type_analysis();
4258
- init_openapi();
4259
- SURFACES_DIRNAME = "surfaces";
4260
- CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
4261
- }
4262
- });
4263
-
4264
3550
  // src/core/rules/namespace.ts
4265
3551
  function isExternalNamespaceRef(ctx, ref) {
4266
3552
  if (ref.startsWith("::") || ref.startsWith("super::")) return true;
@@ -4268,21 +3554,36 @@ function isExternalNamespaceRef(ctx, ref) {
4268
3554
  if (sep9 === -1) return false;
4269
3555
  return !ctx.subsystemIds.has(ref.slice(0, sep9));
4270
3556
  }
4271
- function resolveSurfaceRef(ctx, ref) {
3557
+ function resolveSurfaceRef(ctx, ref, fromSubsystem) {
4272
3558
  const local = ref.split("::").filter((seg) => seg && seg !== "super").pop();
4273
3559
  if (!local) return null;
4274
- for (const snapshot of ctx.surfaceSnapshots) {
4275
- const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
4276
- if (entry) return { snapshot, entry };
3560
+ const mountPools = fromSubsystem ? enclosingMounts(ctx, fromSubsystem).reverse().map((ns) => ctx.mountSurfaceSnapshots.find((m) => m.namespace === ns)?.snapshots ?? []) : [];
3561
+ for (const pool of [...mountPools, ctx.surfaceSnapshots]) {
3562
+ for (const snapshot of pool) {
3563
+ const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
3564
+ if (entry) return { snapshot, entry };
3565
+ }
4277
3566
  }
4278
3567
  return null;
4279
3568
  }
4280
- var roundtripRule, surfaceFreshnessRule, namespaceHygieneRule;
3569
+ function enclosingMounts(ctx, subsystemId) {
3570
+ const mounts = [];
3571
+ let prefix = "";
3572
+ for (const segment of subsystemId.split("::")) {
3573
+ prefix = prefix ? `${prefix}::${segment}` : segment;
3574
+ if (ctx.subsystems.some((s) => s.id === prefix && s.projectPath)) mounts.push(prefix);
3575
+ }
3576
+ return mounts;
3577
+ }
3578
+ function isCollapsedCrossTreeRef(ctx, ref, fromSubsystem) {
3579
+ const nearest = enclosingMounts(ctx, fromSubsystem).pop();
3580
+ return nearest !== void 0 && ref !== nearest && !ref.startsWith(`${nearest}::`);
3581
+ }
3582
+ var roundtripRule, namespaceHygieneRule;
4281
3583
  var init_namespace = __esm({
4282
3584
  "src/core/rules/namespace.ts"() {
4283
3585
  "use strict";
4284
3586
  init_specs2();
4285
- init_surfaces();
4286
3587
  roundtripRule = {
4287
3588
  name: "roundtrip-serialization",
4288
3589
  description: "Every loaded spec must re-serialize through the exact writer pipeline (same relativization, same schema, no I/O) \u2014 validate must predict every refusal that lock's status promotion or any later save would otherwise raise mid-write.",
@@ -4295,18 +3596,6 @@ var init_namespace = __esm({
4295
3596
  }
4296
3597
  }
4297
3598
  };
4298
- surfaceFreshnessRule = {
4299
- name: "surface-freshness",
4300
- description: "Every chained child's stored parent-surface snapshot must match the parent's CURRENT exported contracts \u2014 a drifted snapshot means the child validates standalone against a stale truth. Regenerate with `wairon surface generate-children`.",
4301
- codes: [
4302
- { code: "SURFACE_STALE", defaultSeverity: "warning", summary: "A chained child holds a parent surface snapshot whose contracts no longer match the current tree" }
4303
- ],
4304
- check(ctx) {
4305
- for (const issue2 of checkChildSurfaceFreshness()) {
4306
- ctx.addIssue("warning", "SURFACE_STALE", issue2.message, issue2.specId);
4307
- }
4308
- }
4309
- };
4310
3599
  namespaceHygieneRule = {
4311
3600
  name: "namespace-hygiene",
4312
3601
  description: 'Ids must stay resolvable in the :: namespace grammar: no id segment may be the reserved keyword "super", and a subproject-local name must not shadow a root-level subsystem id \u2014 a bare reference to a shadowed name silently anchors to the ROOT subsystem, so the local spec becomes unaddressable.',
@@ -4413,11 +3702,13 @@ var init_contracts = __esm({
4413
3702
  continue;
4414
3703
  }
4415
3704
  const isCrossTreeForm = isExternalNamespaceRef(ctx, step.targetComponent);
3705
+ const fromSubsystem = ctx.componentMap.get(contract.component)?.subsystem;
3706
+ const isCollapsedForm = !isCrossTreeForm && fromSubsystem !== void 0 && isCollapsedCrossTreeRef(ctx, step.targetComponent, fromSubsystem);
4416
3707
  if (step.type === "dispatch") {
4417
3708
  const dispatchTarget = ctx.componentMap.get(step.targetComponent);
4418
3709
  if (!dispatchTarget) {
4419
- if (isCrossTreeForm) {
4420
- const resolved = resolveSurfaceRef(ctx, step.targetComponent);
3710
+ if (isCrossTreeForm || isCollapsedForm) {
3711
+ const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
4421
3712
  if (resolved) {
4422
3713
  if (step.capability && !(resolved.entry.dispatch ?? []).some((b) => b.capability === step.capability)) {
4423
3714
  ctx.addIssue(
@@ -4431,10 +3722,12 @@ var init_contracts = __esm({
4431
3722
  }
4432
3723
  continue;
4433
3724
  }
3725
+ }
3726
+ if (isCrossTreeForm) {
4434
3727
  ctx.addIssue(
4435
3728
  "warning",
4436
3729
  "CROSS_TREE_REF_UNRESOLVED",
4437
- `Method "${implMethod.name}" in implementation "${impl.id}" dispatches through cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), and no surface snapshot covers it \u2014 validate from the parent project, or import/generate the producing project's surface.`,
3730
+ `Method "${implMethod.name}" in implementation "${impl.id}" dispatches through cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), and no surface snapshot covers it \u2014 validate from the parent project, pin the family surfaces ("wairon surface pin"), or import the producing project's surface.`,
4438
3731
  impl.id,
4439
3732
  isDraftCtx
4440
3733
  );
@@ -4474,8 +3767,8 @@ var init_contracts = __esm({
4474
3767
  }
4475
3768
  const targetComp = ctx.componentMap.get(step.targetComponent);
4476
3769
  if (!targetComp) {
4477
- if (isCrossTreeForm) {
4478
- const resolved = resolveSurfaceRef(ctx, step.targetComponent);
3770
+ if (isCrossTreeForm || isCollapsedForm) {
3771
+ const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
4479
3772
  if (resolved) {
4480
3773
  const surfaceMethod = resolved.entry.methods.find((m) => m.name === step.targetMethod);
4481
3774
  if (!surfaceMethod) {
@@ -4504,10 +3797,12 @@ var init_contracts = __esm({
4504
3797
  }
4505
3798
  continue;
4506
3799
  }
3800
+ }
3801
+ if (isCrossTreeForm) {
4507
3802
  ctx.addIssue(
4508
3803
  "warning",
4509
3804
  "CROSS_TREE_REF_UNRESOLVED",
4510
- `Method "${implMethod.name}" in implementation "${impl.id}" ${verb} cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), and no surface snapshot covers it \u2014 validate from the parent project, or import/generate the producing project's surface.`,
3805
+ `Method "${implMethod.name}" in implementation "${impl.id}" ${verb} cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), and no surface snapshot covers it \u2014 validate from the parent project, pin the family surfaces ("wairon surface pin"), or import the producing project's surface.`,
4511
3806
  impl.id,
4512
3807
  isDraftCtx
4513
3808
  );
@@ -4995,7 +4290,7 @@ function resolveTypeScript(projectRoot2) {
4995
4290
  return cached.ts;
4996
4291
  }
4997
4292
  let ts = null;
4998
- const bases = [path12.join(projectRoot2, "package.json"), __filename];
4293
+ const bases = [path11.join(projectRoot2, "package.json"), __filename];
4999
4294
  for (const base of bases) {
5000
4295
  try {
5001
4296
  const req = (0, import_module2.createRequire)(base);
@@ -5151,7 +4446,7 @@ function walkExact(ts, sourceText, fileName) {
5151
4446
  }
5152
4447
  function resolveRelativeModule(fromFile, specifier) {
5153
4448
  if (!specifier.startsWith(".")) return null;
5154
- const base = path12.resolve(path12.dirname(fromFile), specifier);
4449
+ const base = path11.resolve(path11.dirname(fromFile), specifier);
5155
4450
  const candidates = [
5156
4451
  base,
5157
4452
  base.replace(/\.js$/, ".ts"),
@@ -5159,12 +4454,12 @@ function resolveRelativeModule(fromFile, specifier) {
5159
4454
  `${base}.ts`,
5160
4455
  `${base}.tsx`,
5161
4456
  `${base}.js`,
5162
- path12.join(base, "index.ts"),
5163
- path12.join(base, "index.js")
4457
+ path11.join(base, "index.ts"),
4458
+ path11.join(base, "index.js")
5164
4459
  ];
5165
4460
  for (const c of candidates) {
5166
4461
  try {
5167
- if (fs10.statSync(c).isFile()) return c;
4462
+ if (fs9.statSync(c).isFile()) return c;
5168
4463
  } catch {
5169
4464
  }
5170
4465
  }
@@ -5174,12 +4469,12 @@ function chaseStarExports(ts, facts, filePath, projectRoot2, visited, exactCache
5174
4469
  for (const spec of facts.starExports) {
5175
4470
  const target = resolveRelativeModule(filePath, spec);
5176
4471
  if (!target || visited.has(target)) continue;
5177
- if (path12.relative(projectRoot2, target).startsWith("..")) continue;
4472
+ if (path11.relative(projectRoot2, target).startsWith("..")) continue;
5178
4473
  visited.add(target);
5179
4474
  let targetFacts = exactCache.get(target);
5180
4475
  if (targetFacts === void 0) {
5181
4476
  try {
5182
- targetFacts = walkExact(ts, fs10.readFileSync(target, "utf8"), target);
4477
+ targetFacts = walkExact(ts, fs9.readFileSync(target, "utf8"), target);
5183
4478
  } catch {
5184
4479
  targetFacts = null;
5185
4480
  }
@@ -5219,23 +4514,23 @@ function buildCodeModel(implementations, projectRoot2) {
5219
4514
  if (seen.has(sourcePath)) continue;
5220
4515
  seen.add(sourcePath);
5221
4516
  const empty = { declaredNames: [], anchoredNames: [], exportedNames: [], imports: [], reexports: [] };
5222
- if (path12.isAbsolute(sourcePath) || path12.normalize(sourcePath).split(path12.sep)[0] === "..") {
4517
+ if (path11.isAbsolute(sourcePath) || path11.normalize(sourcePath).split(path11.sep)[0] === "..") {
5223
4518
  files.push({ path: sourcePath, status: "escaped", ...empty });
5224
4519
  continue;
5225
4520
  }
5226
- const absolute = path12.resolve(projectRoot2, sourcePath);
5227
- if (path12.relative(projectRoot2, absolute).startsWith("..")) {
4521
+ const absolute = path11.resolve(projectRoot2, sourcePath);
4522
+ if (path11.relative(projectRoot2, absolute).startsWith("..")) {
5228
4523
  files.push({ path: sourcePath, status: "escaped", ...empty });
5229
4524
  continue;
5230
4525
  }
5231
4526
  let buffer;
5232
4527
  try {
5233
- const stat = fs10.statSync(absolute);
4528
+ const stat = fs9.statSync(absolute);
5234
4529
  if (!stat.isFile()) {
5235
4530
  files.push({ path: sourcePath, status: "missing", ...empty });
5236
4531
  continue;
5237
4532
  }
5238
- buffer = fs10.readFileSync(absolute);
4533
+ buffer = fs9.readFileSync(absolute);
5239
4534
  } catch {
5240
4535
  files.push({ path: sourcePath, status: "missing", ...empty });
5241
4536
  continue;
@@ -5245,7 +4540,7 @@ function buildCodeModel(implementations, projectRoot2) {
5245
4540
  continue;
5246
4541
  }
5247
4542
  const text2 = buffer.toString("utf8");
5248
- const language = EXTENSION_LANGUAGE[path12.extname(sourcePath).toLowerCase()];
4543
+ const language = EXTENSION_LANGUAGE[path11.extname(sourcePath).toLowerCase()];
5249
4544
  let analyzed;
5250
4545
  if (language === "typescript" || language === "javascript") {
5251
4546
  const ts = resolveTypeScript(projectRoot2);
@@ -5279,12 +4574,12 @@ function buildCodeModel(implementations, projectRoot2) {
5279
4574
  }
5280
4575
  return { files, projectRoot: projectRoot2 };
5281
4576
  }
5282
- var fs10, path12, import_module2, EXTENSION_LANGUAGE, C_FAMILY_COMMENTS, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS;
4577
+ var fs9, path11, import_module2, EXTENSION_LANGUAGE, C_FAMILY_COMMENTS, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS;
5283
4578
  var init_source_analysis = __esm({
5284
4579
  "src/core/source-analysis.ts"() {
5285
4580
  "use strict";
5286
- fs10 = __toESM(require("fs"));
5287
- path12 = __toESM(require("path"));
4581
+ fs9 = __toESM(require("fs"));
4582
+ path11 = __toESM(require("path"));
5288
4583
  import_module2 = require("module");
5289
4584
  EXTENSION_LANGUAGE = {
5290
4585
  ".ts": "typescript",
@@ -6009,8 +5304,9 @@ var init_stereotype_deps = __esm({
6009
5304
  for (const depId of dependencies) {
6010
5305
  const depComp = ctx.componentMap.get(depId);
6011
5306
  if (!depComp) {
6012
- if (isExternalNamespaceRef(ctx, depId)) {
6013
- const resolved = resolveSurfaceRef(ctx, depId);
5307
+ const external = isExternalNamespaceRef(ctx, depId);
5308
+ if (external || isCollapsedCrossTreeRef(ctx, depId, comp.subsystem)) {
5309
+ const resolved = resolveSurfaceRef(ctx, depId, comp.subsystem);
6014
5310
  if (resolved) {
6015
5311
  if (comp.componentType !== "Adapter") {
6016
5312
  ctx.addIssue(
@@ -6024,14 +5320,16 @@ var init_stereotype_deps = __esm({
6024
5320
  }
6025
5321
  continue;
6026
5322
  }
6027
- ctx.addIssue(
6028
- "warning",
6029
- "CROSS_TREE_REF_UNRESOLVED",
6030
- `Component "${comp.id}" depends on cross-tree component "${depId}", and no surface snapshot covers it \u2014 validate from the parent project, or import/generate the producing project's surface.`,
6031
- comp.id,
6032
- isDraftCtx
6033
- );
6034
- continue;
5323
+ if (external) {
5324
+ ctx.addIssue(
5325
+ "warning",
5326
+ "CROSS_TREE_REF_UNRESOLVED",
5327
+ `Component "${comp.id}" depends on cross-tree component "${depId}", and no surface snapshot covers it \u2014 validate from the parent project, pin the family surfaces ("wairon surface pin"), or import the producing project's surface.`,
5328
+ comp.id,
5329
+ isDraftCtx
5330
+ );
5331
+ continue;
5332
+ }
6035
5333
  }
6036
5334
  ctx.addIssue(
6037
5335
  "error",
@@ -6101,12 +5399,12 @@ var init_stereotype_deps = __esm({
6101
5399
  }
6102
5400
  }
6103
5401
  if (comp.componentType === "Specialist") {
6104
- const forbiddenTypes = ["Portal", "Observer", "Orchestrator", "Store", "Supervisor"];
5402
+ const forbiddenTypes = ["Portal", "Observer", "Orchestrator", "Store", "Registry", "Supervisor", "Actor"];
6105
5403
  if (forbiddenTypes.includes(depComp.componentType)) {
6106
5404
  ctx.addIssue(
6107
5405
  "error",
6108
5406
  "ARCHITECTURE_VIOLATION_SPECIALIST_DEP",
6109
- `Architectural violation: Specialist component "${comp.id}" cannot depend on "${depComp.componentType}" component "${depComp.id}". Specialists are narrow capabilities \u2014 they may use Repositories, Indexes, and Adapters, but not Orchestrators, Supervisors, Stores, Portals, or Observers.` + (depComp.componentType === "Store" ? storeResolutionHint(comp.componentType, depComp.id) : ""),
5407
+ `Architectural violation: Specialist component "${comp.id}" cannot depend on "${depComp.componentType}" component "${depComp.id}". Specialists are pure capabilities \u2014 they may use Repository facades, Indexes, and Adapters, but never workflow/runtime blocks (Orchestrators, Supervisors, Actors) and never persistence directly (Stores, Registries \u2014 all storage, even in-memory, is reached through a Repository facade), nor Portals or Observers.` + (depComp.componentType === "Store" ? storeResolutionHint(comp.componentType, depComp.id) : ""),
6110
5408
  comp.id,
6111
5409
  isDraftCtx || ctx.isComponentDraft(depComp.id)
6112
5410
  );
@@ -8652,7 +7950,7 @@ var init_naming = __esm({
8652
7950
  // src/core/rules/integration-conformance.ts
8653
7951
  function resolveAgainst(mapped, fromFile, specifier) {
8654
7952
  if (!specifier.startsWith(".")) return null;
8655
- const joined = normalizePath(path13.posix.normalize(path13.posix.join(path13.posix.dirname(fromFile), specifier)));
7953
+ const joined = normalizePath(path12.posix.normalize(path12.posix.join(path12.posix.dirname(fromFile), specifier)));
8656
7954
  const candidates = [
8657
7955
  joined,
8658
7956
  joined.replace(/\.js$/, ".ts"),
@@ -8668,11 +7966,11 @@ function resolveAgainst(mapped, fromFile, specifier) {
8668
7966
  }
8669
7967
  return null;
8670
7968
  }
8671
- var path13, normalizePath, integrationConformanceRule;
7969
+ var path12, normalizePath, integrationConformanceRule;
8672
7970
  var init_integration_conformance = __esm({
8673
7971
  "src/core/rules/integration-conformance.ts"() {
8674
7972
  "use strict";
8675
- path13 = __toESM(require("path"));
7973
+ path12 = __toESM(require("path"));
8676
7974
  init_source_analysis();
8677
7975
  init_conformance();
8678
7976
  normalizePath = normalizeSourcePath;
@@ -8876,7 +8174,7 @@ var init_hidden_state = __esm({
8876
8174
  // src/core/rules/dependency-conformance.ts
8877
8175
  function resolveAgainst2(mapped, fromFile, specifier) {
8878
8176
  if (!specifier.startsWith(".")) return null;
8879
- const joined = normalizePath2(path14.posix.normalize(path14.posix.join(path14.posix.dirname(fromFile), specifier)));
8177
+ const joined = normalizePath2(path13.posix.normalize(path13.posix.join(path13.posix.dirname(fromFile), specifier)));
8880
8178
  const candidates = [
8881
8179
  joined,
8882
8180
  joined.replace(/\.js$/, ".ts"),
@@ -8892,11 +8190,11 @@ function resolveAgainst2(mapped, fromFile, specifier) {
8892
8190
  }
8893
8191
  return null;
8894
8192
  }
8895
- var path14, normalizePath2, dependencyConformanceRule;
8193
+ var path13, normalizePath2, dependencyConformanceRule;
8896
8194
  var init_dependency_conformance = __esm({
8897
8195
  "src/core/rules/dependency-conformance.ts"() {
8898
8196
  "use strict";
8899
- path14 = __toESM(require("path"));
8197
+ path13 = __toESM(require("path"));
8900
8198
  init_source_analysis();
8901
8199
  init_conformance();
8902
8200
  normalizePath2 = normalizeSourcePath;
@@ -9378,17 +8676,11 @@ function buildRuleContext(opts) {
9378
8676
  for (const i of interfaces) collectAllows(i.id, i.lint);
9379
8677
  for (const im of implementations) collectAllows(im.id, im.lint);
9380
8678
  for (const t of types) collectAllows(t.id, t.lint);
9381
- const knownIssueCodes = /* @__PURE__ */ new Set([
8679
+ const knownIssueCodes2 = /* @__PURE__ */ new Set([
9382
8680
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
9383
8681
  // Declarative assertions bring their own namespaced codes — lint.allow
9384
8682
  // and severity overrides treat them exactly like builtins.
9385
- ...extensions.assertions.map((a) => a.fullCode),
9386
- // Entry-point emitted codes: validateSddTree's chained-subproject pass
9387
- // raises these AFTER the rule run (it post-processes the aggregated issue
9388
- // list), so no registered rule declares them — but lint.allow validation
9389
- // must still recognize them as real codes.
9390
- "CHAINED_SUBPROJECT_CONTEXT",
9391
- "UNVERIFIED_EXTERNAL_REF"
8683
+ ...extensions.assertions.map((a) => a.fullCode)
9392
8684
  ]);
9393
8685
  const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
9394
8686
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
@@ -9444,9 +8736,10 @@ function buildRuleContext(opts) {
9444
8736
  ext: { profiles: extensions.profiles, languages: extensions.languages, patterns: extensions.patterns, guarantees: extensions.guarantees, assertions: extensions.assertions, packSelections: opts.packSelections ?? [], selectionFailures: extensions.selectionFailures ?? [] },
9445
8737
  variants: opts.variants ?? [],
9446
8738
  surfaceSnapshots: opts.surfaceSnapshots ?? [],
8739
+ mountSurfaceSnapshots: opts.mountSurfaceSnapshots ?? [],
9447
8740
  codeModel: opts.codeModel ?? emptyCodeModel(),
9448
8741
  lintAllows,
9449
- knownIssueCodes,
8742
+ knownIssueCodes: knownIssueCodes2,
9450
8743
  addIssue
9451
8744
  };
9452
8745
  }
@@ -9501,7 +8794,6 @@ var init_rules = __esm({
9501
8794
  // explain many downstream findings, so surface them early in the list.
9502
8795
  namespaceHygieneRule,
9503
8796
  roundtripRule,
9504
- surfaceFreshnessRule,
9505
8797
  typeReferencesRule,
9506
8798
  contractsRule,
9507
8799
  // Vocabulary check right after contracts: an unknown token explains why the
@@ -9771,14 +9063,14 @@ function describeApprover(who) {
9771
9063
  }
9772
9064
  function readLockRecord() {
9773
9065
  try {
9774
- return normalizeRecord(JSON.parse(fs11.readFileSync(lockPath(), "utf8")));
9066
+ return normalizeRecord(JSON.parse(fs10.readFileSync(lockPath(), "utf8")));
9775
9067
  } catch {
9776
9068
  return null;
9777
9069
  }
9778
9070
  }
9779
9071
  function readLockRecordAt(root) {
9780
9072
  try {
9781
- return normalizeRecord(JSON.parse(fs11.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
9073
+ return normalizeRecord(JSON.parse(fs10.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
9782
9074
  } catch {
9783
9075
  return null;
9784
9076
  }
@@ -9789,10 +9081,10 @@ function normalizeRecord(raw) {
9789
9081
  }
9790
9082
  function writeLockRecord(record2) {
9791
9083
  const p = lockPath();
9792
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
9084
+ fs10.mkdirSync(path14.dirname(p), { recursive: true });
9793
9085
  const tmp = `${p}.tmp`;
9794
- fs11.writeFileSync(tmp, JSON.stringify(withSortedMaps(record2), null, 2) + "\n");
9795
- fs11.renameSync(tmp, p);
9086
+ fs10.writeFileSync(tmp, JSON.stringify(withSortedMaps(record2), null, 2) + "\n");
9087
+ fs10.renameSync(tmp, p);
9796
9088
  }
9797
9089
  function withSortedMaps(record2) {
9798
9090
  const sorted = (m) => {
@@ -9809,12 +9101,12 @@ function withSortedMaps(record2) {
9809
9101
  ...children ? { children } : {}
9810
9102
  };
9811
9103
  }
9812
- var fs11, path15;
9104
+ var fs10, path14;
9813
9105
  var init_lockfile = __esm({
9814
9106
  "src/core/lockfile.ts"() {
9815
9107
  "use strict";
9816
- fs11 = __toESM(require("fs"));
9817
- path15 = __toESM(require("path"));
9108
+ fs10 = __toESM(require("fs"));
9109
+ path14 = __toESM(require("path"));
9818
9110
  init_fs();
9819
9111
  }
9820
9112
  });
@@ -10547,14 +9839,14 @@ function embedJson(value) {
10547
9839
  }
10548
9840
  function loadCytoscapeLib() {
10549
9841
  const candidates = [
10550
- path16.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
9842
+ path15.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
10551
9843
  // src/core & dist/cli
10552
- path16.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
9844
+ path15.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
10553
9845
  // dist (library entry)
10554
9846
  ];
10555
9847
  for (const p of candidates) {
10556
- if (fs12.existsSync(p)) {
10557
- return fs12.readFileSync(p, "utf-8").replace(/<\/script/gi, "<\\/script");
9848
+ if (fs11.existsSync(p)) {
9849
+ return fs11.readFileSync(p, "utf-8").replace(/<\/script/gi, "<\\/script");
10558
9850
  }
10559
9851
  }
10560
9852
  throw new Error("Vendored cytoscape bundle not found (templates/canvas/cytoscape.min.js) \u2014 the wairon installation is incomplete.");
@@ -10566,12 +9858,12 @@ function renderCanvasHtml(model) {
10566
9858
  function escapeHtml(s) {
10567
9859
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10568
9860
  }
10569
- var fs12, path16, CANVAS_TEMPLATE;
9861
+ var fs11, path15, CANVAS_TEMPLATE;
10570
9862
  var init_canvas = __esm({
10571
9863
  "src/core/canvas.ts"() {
10572
9864
  "use strict";
10573
- fs12 = __toESM(require("fs"));
10574
- path16 = __toESM(require("path"));
9865
+ fs11 = __toESM(require("fs"));
9866
+ path15 = __toESM(require("path"));
10575
9867
  init_specs2();
10576
9868
  init_type_analysis();
10577
9869
  init_diagram_export();
@@ -14640,110 +13932,822 @@ var MODEL = __MODEL_JSON__;
14640
13932
  }
14641
13933
  panel.innerHTML = '<div class="head">' + head + '</div><div class="body">' + body + '</div>';
14642
13934
 
14643
- var navs = panel.querySelectorAll('[data-kind]');
14644
- for (var i = 0; i < navs.length; i++) {
14645
- (function (n) {
14646
- n.addEventListener('click', function () { select(n.getAttribute('data-kind'), n.getAttribute('data-id'), true); });
14647
- n.addEventListener('mouseenter', function () {
14648
- var node = nodeForRef(n.getAttribute('data-kind'), n.getAttribute('data-id'));
14649
- if (node.length) node.addClass('hoverhl');
14650
- });
14651
- n.addEventListener('mouseleave', function () { cy.nodes().removeClass('hoverhl'); });
14652
- })(navs[i]);
14653
- }
14654
- var opens = panel.querySelectorAll('[data-open-kind]');
14655
- for (var k = 0; k < opens.length; k++) {
14656
- (function (b) {
14657
- b.addEventListener('click', function () { navigateTo(b.getAttribute('data-open-kind'), b.getAttribute('data-open-id')); });
14658
- })(opens[k]);
14659
- }
14660
- var typesOpen = document.getElementById('openTypesBtn');
14661
- if (typesOpen && typesOpen.addEventListener && panel.innerHTML.indexOf('openTypesBtn') >= 0) {
14662
- typesOpen.addEventListener('click', function () { navigateTo('types', null); });
13935
+ var navs = panel.querySelectorAll('[data-kind]');
13936
+ for (var i = 0; i < navs.length; i++) {
13937
+ (function (n) {
13938
+ n.addEventListener('click', function () { select(n.getAttribute('data-kind'), n.getAttribute('data-id'), true); });
13939
+ n.addEventListener('mouseenter', function () {
13940
+ var node = nodeForRef(n.getAttribute('data-kind'), n.getAttribute('data-id'));
13941
+ if (node.length) node.addClass('hoverhl');
13942
+ });
13943
+ n.addEventListener('mouseleave', function () { cy.nodes().removeClass('hoverhl'); });
13944
+ })(navs[i]);
13945
+ }
13946
+ var opens = panel.querySelectorAll('[data-open-kind]');
13947
+ for (var k = 0; k < opens.length; k++) {
13948
+ (function (b) {
13949
+ b.addEventListener('click', function () { navigateTo(b.getAttribute('data-open-kind'), b.getAttribute('data-open-id')); });
13950
+ })(opens[k]);
13951
+ }
13952
+ var typesOpen = document.getElementById('openTypesBtn');
13953
+ if (typesOpen && typesOpen.addEventListener && panel.innerHTML.indexOf('openTypesBtn') >= 0) {
13954
+ typesOpen.addEventListener('click', function () { navigateTo('types', null); });
13955
+ }
13956
+ var oapis = panel.querySelectorAll('[data-openapi-tag]');
13957
+ for (var oi = 0; oi < oapis.length; oi++) {
13958
+ (function (b) {
13959
+ b.addEventListener('click', function () {
13960
+ var tag = b.getAttribute('data-openapi-tag') || '';
13961
+ if (typeof opts !== 'undefined' && opts && opts.onOpenApi) { opts.onOpenApi(tag); return; }
13962
+ var href = openApiSiblingHref();
13963
+ if (href) window.open(href + (tag ? '#/' + tag : ''), '_blank', 'noopener');
13964
+ });
13965
+ })(oapis[oi]);
13966
+ }
13967
+ var ospecs = panel.querySelectorAll('[data-openspec-kind]');
13968
+ for (var si = 0; si < ospecs.length; si++) {
13969
+ (function (b) {
13970
+ b.addEventListener('click', function () {
13971
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
13972
+ opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
13973
+ }
13974
+ });
13975
+ })(ospecs[si]);
13976
+ }
13977
+ var flows = panel.querySelectorAll('[data-flow-comp]');
13978
+ for (var j = 0; j < flows.length; j++) {
13979
+ (function (b) {
13980
+ b.addEventListener('click', function (ev) {
13981
+ if (ev && ev.stopPropagation) ev.stopPropagation();
13982
+ openFlow(b.getAttribute('data-flow-comp'), b.getAttribute('data-flow-method'), b.getAttribute('data-flow-mode') || 'flow');
13983
+ });
13984
+ })(flows[j]);
13985
+ }
13986
+ }
13987
+
13988
+ renderPanel();
13989
+ // Stage G: a deep link may focus a component and/or open a method's narrative
13990
+ // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
13991
+ // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
13992
+ // works whether the seeded view is the component itself or its parent subsystem
13993
+ // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
13994
+ // and focus the component here).
13995
+ (function () {
13996
+ if (typeof opts === 'undefined' || !opts) return;
13997
+ var f = opts.initialFlow, s = opts.initialSelect;
13998
+ var focusComp = (f && f.comp) || (s && s.comp);
13999
+ if (focusComp && compById[focusComp]) {
14000
+ try { select('component', focusComp, true); } catch (e) { /* ignore */ }
14001
+ }
14002
+ if (f && f.comp && f.method && compById[f.comp]) {
14003
+ try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
14004
+ }
14005
+ })();
14006
+ })();
14007
+ </script>
14008
+ </body>
14009
+ </html>
14010
+ `;
14011
+ }
14012
+ });
14013
+
14014
+ // src/core/rules/repository.ts
14015
+ function addRule(rule) {
14016
+ ruleSet.push(rule);
14017
+ }
14018
+ function listRules() {
14019
+ return ruleSet;
14020
+ }
14021
+ function registerBuiltinRules() {
14022
+ ruleSet = [];
14023
+ for (const rule of SDD_RULES) addRule(rule);
14024
+ }
14025
+ function registerPackRules(packRules) {
14026
+ for (const rule of packRules) addRule(rule);
14027
+ }
14028
+ function ruleSequence() {
14029
+ const base = ruleSet.filter((r) => r !== lintAllowsRule);
14030
+ return ruleSet.includes(lintAllowsRule) ? [...base, lintAllowsRule] : base;
14031
+ }
14032
+ function specScopedRules() {
14033
+ return ruleSequence().filter((r) => r.scope === "spec");
14034
+ }
14035
+ function knownIssueCodes() {
14036
+ return listRules().flatMap((r) => r.codes);
14037
+ }
14038
+ var ruleSet;
14039
+ var init_repository = __esm({
14040
+ "src/core/rules/repository.ts"() {
14041
+ "use strict";
14042
+ init_rules();
14043
+ init_lint_allows();
14044
+ ruleSet = [];
14045
+ }
14046
+ });
14047
+
14048
+ // src/utils/filenames.ts
14049
+ function safeFilenamePart(value) {
14050
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
14051
+ }
14052
+ var init_filenames = __esm({
14053
+ "src/utils/filenames.ts"() {
14054
+ "use strict";
14055
+ }
14056
+ });
14057
+
14058
+ // src/core/openapi.ts
14059
+ function schemaFor(typeRef, closureIds) {
14060
+ const trimmed = typeRef.trim().replace(/^promise\s*<(.+)>$/i, "$1").trim();
14061
+ const arrayMatch = /^(.+)\[\]$/.exec(trimmed);
14062
+ if (arrayMatch) {
14063
+ return { type: "array", items: schemaFor(arrayMatch[1], closureIds) };
14064
+ }
14065
+ const lower = trimmed.toLowerCase();
14066
+ if (lower in PRIMITIVES) {
14067
+ const p = PRIMITIVES[lower];
14068
+ return p.type ? { ...p } : {};
14069
+ }
14070
+ const local = trimmed.split(/::|\./).pop().toLowerCase().replace(/[^a-z0-9_-]/g, "");
14071
+ const hit = [...closureIds].find((id) => id.toLowerCase() === local);
14072
+ if (hit) return { $ref: `#/components/schemas/${hit}` };
14073
+ return { type: "object", description: `Unresolved type: ${trimmed}` };
14074
+ }
14075
+ function operationFor(method2, closureIds) {
14076
+ const endpoint = method2.endpoint;
14077
+ const httpVerb = endpoint && endpoint.transport === "HTTP" ? endpoint.method.toLowerCase() : "post";
14078
+ const bodyVerbs = /* @__PURE__ */ new Set(["post", "put", "patch"]);
14079
+ const params = method2.params ?? [];
14080
+ const op = {
14081
+ operationId: method2.name,
14082
+ summary: method2.description,
14083
+ responses: {
14084
+ "200": {
14085
+ description: method2.returns || "Success",
14086
+ ...method2.returns && method2.returns.toLowerCase() !== "void" ? { content: { "application/json": { schema: schemaFor(method2.returns, closureIds) } } } : {}
14087
+ }
14088
+ }
14089
+ };
14090
+ if (method2.guarantees?.length) op["x-wairon-guarantees"] = method2.guarantees;
14091
+ if (method2.effect) op["x-wairon-effect"] = method2.effect;
14092
+ if (method2.ext && Object.keys(method2.ext).length) op["x-wairon-ext"] = method2.ext;
14093
+ if (params.length) {
14094
+ if (bodyVerbs.has(httpVerb)) {
14095
+ op.requestBody = {
14096
+ required: true,
14097
+ content: {
14098
+ "application/json": {
14099
+ schema: {
14100
+ type: "object",
14101
+ properties: Object.fromEntries(params.map((p) => [p.name, schemaFor(p.type, closureIds)])),
14102
+ required: params.filter((p) => !p.optional).map((p) => p.name)
14103
+ }
14104
+ }
14105
+ }
14106
+ };
14107
+ } else {
14108
+ op.parameters = params.map((p) => ({
14109
+ name: p.name,
14110
+ in: "query",
14111
+ required: !p.optional,
14112
+ ...p.description ? { description: p.description } : {},
14113
+ schema: schemaFor(p.type, closureIds)
14114
+ }));
14115
+ }
14116
+ }
14117
+ return op;
14118
+ }
14119
+ function securitySchemeObject(auth) {
14120
+ if (auth.scheme === "none") return null;
14121
+ const desc = auth.description ? { description: auth.description } : {};
14122
+ switch (auth.scheme) {
14123
+ case "apiKey":
14124
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
14125
+ case "bearer":
14126
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
14127
+ case "basic":
14128
+ return { type: "http", scheme: "basic", ...desc };
14129
+ case "oauth2": {
14130
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
14131
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
14132
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
14133
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
14134
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
14135
+ }
14136
+ case "openIdConnect":
14137
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
14138
+ case "custom":
14139
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
14140
+ default:
14141
+ return null;
14142
+ }
14143
+ }
14144
+ function schemeBaseName(scheme) {
14145
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
14146
+ }
14147
+ function buildSecurity(entries) {
14148
+ const schemes = {};
14149
+ const nameByContent = /* @__PURE__ */ new Map();
14150
+ const securityByEntry = /* @__PURE__ */ new Map();
14151
+ for (const entry of entries) {
14152
+ if (!entry.auth) continue;
14153
+ const obj = securitySchemeObject(entry.auth);
14154
+ if (!obj) continue;
14155
+ const content = JSON.stringify(obj);
14156
+ let name = nameByContent.get(content);
14157
+ if (!name) {
14158
+ name = schemeBaseName(entry.auth.scheme);
14159
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
14160
+ schemes[name] = obj;
14161
+ nameByContent.set(content, name);
14162
+ }
14163
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
14164
+ securityByEntry.set(entry.id, { [name]: scopeNames });
14165
+ }
14166
+ return { schemes, securityByEntry };
14167
+ }
14168
+ function httpEntriesOf(snapshot) {
14169
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
14170
+ }
14171
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
14172
+ const { schemes, securityByEntry } = buildSecurity(entries);
14173
+ const paths = {};
14174
+ for (const entry of entries) {
14175
+ const security = securityByEntry.get(entry.id);
14176
+ for (const method2 of entry.methods) {
14177
+ const endpoint = method2.endpoint;
14178
+ if (!endpoint || endpoint.transport !== "HTTP") continue;
14179
+ const p = endpoint.path.startsWith("/") ? endpoint.path : `/${endpoint.path}`;
14180
+ paths[p] = paths[p] ?? {};
14181
+ paths[p][endpoint.method.toLowerCase()] = {
14182
+ tags: [entry.id],
14183
+ ...operationFor(method2, closureIds),
14184
+ ...security ? { security: [security] } : {}
14185
+ };
14186
+ }
14187
+ }
14188
+ const schemas = {};
14189
+ for (const t of snapshot.types) {
14190
+ schemas[t.id] = {
14191
+ type: "object",
14192
+ title: t.name,
14193
+ properties: Object.fromEntries(t.fields.map((f) => [f.name, schemaFor(f.type, closureIds)])),
14194
+ required: t.fields.filter((f) => !f.optional).map((f) => f.name)
14195
+ };
14196
+ }
14197
+ const components = {};
14198
+ if (Object.keys(schemas).length) components.schemas = schemas;
14199
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
14200
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
14201
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
14202
+ return {
14203
+ openapi: "3.1.0",
14204
+ info: {
14205
+ title: opts.title ?? snapshot.projectName,
14206
+ version: snapshot.version ?? "0.0.0",
14207
+ ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
14208
+ "x-wairon-origin": snapshot.origin,
14209
+ "x-wairon-generated-at": snapshot.generatedAt
14210
+ },
14211
+ ...servers ? { servers } : {},
14212
+ paths,
14213
+ ...Object.keys(components).length ? { components } : {}
14214
+ };
14215
+ }
14216
+ function toOpenApiSet(snapshot) {
14217
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
14218
+ const byPortal = /* @__PURE__ */ new Map();
14219
+ const order = [];
14220
+ for (const entry of httpEntriesOf(snapshot)) {
14221
+ if (!byPortal.has(entry.component)) {
14222
+ byPortal.set(entry.component, []);
14223
+ order.push(entry.component);
14224
+ }
14225
+ byPortal.get(entry.component).push(entry);
14226
+ }
14227
+ return order.map((portalId) => {
14228
+ const entries = byPortal.get(portalId);
14229
+ const name = entries[0]?.name ?? portalId;
14230
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
14231
+ });
14232
+ }
14233
+ function isOpenApiDocument(body) {
14234
+ try {
14235
+ const parsed = yaml2.load(body);
14236
+ return !!parsed && typeof parsed === "object" && typeof parsed.openapi === "string";
14237
+ } catch {
14238
+ return false;
14239
+ }
14240
+ }
14241
+ function typeRefFromSchema(schema) {
14242
+ if (!schema) return "json";
14243
+ const ref = schema.$ref;
14244
+ if (typeof ref === "string") return ref.split("/").pop() ?? "json";
14245
+ if (schema.type === "array") {
14246
+ return `${typeRefFromSchema(schema.items)}[]`;
14247
+ }
14248
+ const t = schema.type;
14249
+ if (t === "integer") return "int";
14250
+ if (typeof t === "string" && t !== "object") return t;
14251
+ return "json";
14252
+ }
14253
+ function authFromSecurityScheme(scheme) {
14254
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
14255
+ if (scheme.type === "apiKey") {
14256
+ return {
14257
+ scheme: "apiKey",
14258
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
14259
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
14260
+ ...desc
14261
+ };
14262
+ }
14263
+ if (scheme.type === "http") {
14264
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
14265
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
14266
+ }
14267
+ if (scheme.type === "oauth2") {
14268
+ const flows = scheme.flows ?? {};
14269
+ const flowKey = Object.keys(flows)[0];
14270
+ const f = flows[flowKey] ?? {};
14271
+ return {
14272
+ scheme: "oauth2",
14273
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
14274
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
14275
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
14276
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
14277
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
14278
+ ...desc
14279
+ };
14280
+ }
14281
+ if (scheme.type === "openIdConnect") {
14282
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
14283
+ }
14284
+ return void 0;
14285
+ }
14286
+ function fromOpenApi(document, projectName) {
14287
+ let parsed;
14288
+ try {
14289
+ parsed = yaml2.load(document);
14290
+ } catch (e) {
14291
+ throw new Error(`Invalid surface document: not parseable as JSON/YAML (${e instanceof Error ? e.message : String(e)})`);
14292
+ }
14293
+ if (!parsed || typeof parsed !== "object" || typeof parsed.openapi !== "string" || typeof parsed.paths !== "object") {
14294
+ throw new Error('Invalid surface document: missing OpenAPI "openapi"/"paths" structure.');
14295
+ }
14296
+ const info = parsed.info ?? {};
14297
+ const methods = [];
14298
+ for (const [rawPath, ops] of Object.entries(parsed.paths)) {
14299
+ for (const [verb, opRaw] of Object.entries(ops ?? {})) {
14300
+ if (!["get", "post", "put", "delete", "patch", "options", "head"].includes(verb)) continue;
14301
+ const op = opRaw ?? {};
14302
+ const name = typeof op.operationId === "string" && /^[a-zA-Z0-9_]+$/.test(op.operationId) ? op.operationId : `${verb}_${rawPath.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
14303
+ const params = [];
14304
+ for (const p of op.parameters ?? []) {
14305
+ if (typeof p.name !== "string") continue;
14306
+ params.push({
14307
+ name: p.name,
14308
+ type: typeRefFromSchema(p.schema),
14309
+ ...p.required === true ? {} : { optional: true },
14310
+ ...typeof p.description === "string" ? { description: p.description } : {}
14311
+ });
14312
+ }
14313
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
14314
+ if (bodySchema) {
14315
+ const props = bodySchema.properties ?? {};
14316
+ const required = new Set(bodySchema.required ?? []);
14317
+ if (Object.keys(props).length) {
14318
+ for (const [pname, pschema] of Object.entries(props)) {
14319
+ params.push({ name: pname, type: typeRefFromSchema(pschema), ...required.has(pname) ? {} : { optional: true } });
14320
+ }
14321
+ } else {
14322
+ params.push({ name: "body", type: typeRefFromSchema(bodySchema) });
14323
+ }
14324
+ }
14325
+ const okResponse = op.responses?.["200"] ?? op.responses?.["201"];
14326
+ const responseSchema = okResponse?.content?.["application/json"]?.schema;
14327
+ const returns = responseSchema ? typeRefFromSchema(responseSchema) : "void";
14328
+ const rawGuarantees = op["x-wairon-guarantees"];
14329
+ const guarantees = Array.isArray(rawGuarantees) ? rawGuarantees.filter((g) => typeof g === "string" && g.length > 0) : [];
14330
+ const rawEffect = op["x-wairon-effect"];
14331
+ const effect = rawEffect === "read" || rawEffect === "write" ? rawEffect : void 0;
14332
+ const rawExt = op["x-wairon-ext"];
14333
+ const ext = rawExt && typeof rawExt === "object" && !Array.isArray(rawExt) ? rawExt : void 0;
14334
+ methods.push({
14335
+ name,
14336
+ description: typeof op.summary === "string" ? op.summary : typeof op.description === "string" ? op.description : name,
14337
+ signature: `${name}(${params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${returns}`,
14338
+ returns,
14339
+ params,
14340
+ endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath },
14341
+ ...guarantees.length ? { guarantees } : {},
14342
+ ...effect ? { effect } : {},
14343
+ ...ext ? { ext } : {}
14344
+ });
14345
+ }
14346
+ }
14347
+ const types = [];
14348
+ const schemas = parsed.components?.schemas ?? {};
14349
+ for (const [id, schema] of Object.entries(schemas)) {
14350
+ const props = schema.properties ?? {};
14351
+ const required = new Set(schema.required ?? []);
14352
+ types.push({
14353
+ id,
14354
+ name: typeof schema.title === "string" ? schema.title : id,
14355
+ kind: "value-object",
14356
+ fields: Object.entries(props).map(([fname, fschema]) => ({
14357
+ name: fname,
14358
+ type: typeRefFromSchema(fschema),
14359
+ ...required.has(fname) ? {} : { optional: true }
14360
+ }))
14361
+ });
14362
+ }
14363
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
14364
+ const firstScheme = Object.values(securitySchemes)[0];
14365
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
14366
+ const entry = {
14367
+ id: `${projectName}-api`,
14368
+ name: typeof info.title === "string" ? info.title : projectName,
14369
+ audience: "external",
14370
+ type: "REST",
14371
+ component: `${projectName}-api`,
14372
+ methods,
14373
+ details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
14374
+ ...typeof info.version === "string" ? { version: info.version } : {},
14375
+ ...importedAuth ? { auth: importedAuth } : {}
14376
+ };
14377
+ return SurfaceSnapshotSchema.parse({
14378
+ projectName,
14379
+ origin: "authored",
14380
+ ...typeof info.version === "string" ? { version: info.version } : {},
14381
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14382
+ interfaces: [entry],
14383
+ types
14384
+ });
14385
+ }
14386
+ var yaml2, PRIMITIVES;
14387
+ var init_openapi = __esm({
14388
+ "src/core/openapi.ts"() {
14389
+ "use strict";
14390
+ yaml2 = __toESM(require("js-yaml"));
14391
+ init_models();
14392
+ PRIMITIVES = {
14393
+ string: { type: "string" },
14394
+ number: { type: "number" },
14395
+ float: { type: "number" },
14396
+ decimal: { type: "number" },
14397
+ int: { type: "integer" },
14398
+ integer: { type: "integer" },
14399
+ boolean: { type: "boolean" },
14400
+ bool: { type: "boolean" },
14401
+ date: { type: "string", format: "date-time" },
14402
+ datetime: { type: "string", format: "date-time" },
14403
+ uuid: { type: "string", format: "uuid" },
14404
+ json: { type: "object" },
14405
+ object: { type: "object" },
14406
+ any: {},
14407
+ unknown: {},
14408
+ void: {}
14409
+ };
14410
+ }
14411
+ });
14412
+
14413
+ // src/core/surfaces.ts
14414
+ function surfacesDir(rootDir) {
14415
+ return path16.join(rootDir, ".wai", SURFACES_DIRNAME);
14416
+ }
14417
+ function audienceRank(audience) {
14418
+ const idx = SURFACE_AUDIENCES.indexOf(audience ?? "instance");
14419
+ return idx === -1 ? SURFACE_AUDIENCES.indexOf("instance") : idx;
14420
+ }
14421
+ function stateIdString() {
14422
+ const s = computeStateId();
14423
+ return `${s.algorithm}:${s.digest}`;
14424
+ }
14425
+ function computeTypeClosure(entries, types) {
14426
+ const included = /* @__PURE__ */ new Map();
14427
+ const queue = [];
14428
+ const enqueueRef = (ref) => {
14429
+ if (BUILTIN_TYPES.has(ref.toLowerCase())) return;
14430
+ for (const spec of types) {
14431
+ const qualifiedId2 = spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
14432
+ if (matchTypeRef(ref, qualifiedId2) && !included.has(spec.id)) {
14433
+ included.set(spec.id, spec);
14434
+ queue.push(spec.id);
14435
+ }
14663
14436
  }
14664
- var oapis = panel.querySelectorAll('[data-openapi-tag]');
14665
- for (var oi = 0; oi < oapis.length; oi++) {
14666
- (function (b) {
14667
- b.addEventListener('click', function () {
14668
- var tag = b.getAttribute('data-openapi-tag') || '';
14669
- if (typeof opts !== 'undefined' && opts && opts.onOpenApi) { opts.onOpenApi(tag); return; }
14670
- var href = openApiSiblingHref();
14671
- if (href) window.open(href + (tag ? '#/' + tag : ''), '_blank', 'noopener');
14672
- });
14673
- })(oapis[oi]);
14437
+ };
14438
+ for (const entry of entries) {
14439
+ for (const m of entry.methods) {
14440
+ for (const ref of methodTypeRefs(m)) enqueueRef(ref);
14674
14441
  }
14675
- var ospecs = panel.querySelectorAll('[data-openspec-kind]');
14676
- for (var si = 0; si < ospecs.length; si++) {
14677
- (function (b) {
14678
- b.addEventListener('click', function () {
14679
- if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
14680
- opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
14681
- }
14682
- });
14683
- })(ospecs[si]);
14442
+ }
14443
+ while (queue.length) {
14444
+ const spec = included.get(queue.shift());
14445
+ for (const field of spec.fields) {
14446
+ for (const ref of extractTypeIdentifiers(field.type)) enqueueRef(ref);
14684
14447
  }
14685
- var flows = panel.querySelectorAll('[data-flow-comp]');
14686
- for (var j = 0; j < flows.length; j++) {
14687
- (function (b) {
14688
- b.addEventListener('click', function (ev) {
14689
- if (ev && ev.stopPropagation) ev.stopPropagation();
14690
- openFlow(b.getAttribute('data-flow-comp'), b.getAttribute('data-flow-method'), b.getAttribute('data-flow-mode') || 'flow');
14691
- });
14692
- })(flows[j]);
14448
+ }
14449
+ return [...included.values()].map((t) => ({
14450
+ id: t.id,
14451
+ name: t.name,
14452
+ kind: t.kind,
14453
+ fields: t.fields.map((f) => ({
14454
+ name: f.name,
14455
+ type: f.type,
14456
+ ...f.description ? { description: f.description } : {},
14457
+ ...f.optional ? { optional: true } : {}
14458
+ }))
14459
+ }));
14460
+ }
14461
+ function projectOwnSurface(maxAudience) {
14462
+ const system = loadSystemSpec();
14463
+ if (!system) {
14464
+ throw new Error("Cannot project a surface: the L0 system spec is missing.");
14465
+ }
14466
+ const subsystems = loadSubsystemSpecs();
14467
+ const components = loadComponentSpecs();
14468
+ const interfaces = loadInterfaceSpecs();
14469
+ const types = loadTypeSpecs();
14470
+ const floor = audienceRank(maxAudience);
14471
+ const rawEntries = system.publicInterfaces ?? [];
14472
+ const entries = [];
14473
+ for (const raw of rawEntries) {
14474
+ const audience = raw.audience ?? "instance";
14475
+ if (audienceRank(audience) < floor) continue;
14476
+ if (!raw.component) continue;
14477
+ const comp = components.find((c) => c.id === raw.component);
14478
+ if (!comp) continue;
14479
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!raw.interface || i.id === raw.interface));
14480
+ const methods = compInterfaces.flatMap((i) => i.methods);
14481
+ const subsystemType = subsystems.find((s) => s.id === comp.subsystem)?.publicInterfaces.find((pi) => pi.component === comp.id)?.type;
14482
+ entries.push({
14483
+ id: raw.id ?? raw.interface ?? comp.id,
14484
+ name: raw.name ?? comp.name,
14485
+ audience,
14486
+ type: raw.type ?? subsystemType ?? "Custom",
14487
+ component: comp.id,
14488
+ methods,
14489
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
14490
+ // Project the backing Portal's auth + basePath so the codec can emit
14491
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
14492
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
14493
+ ...comp.basePath ? { basePath: comp.basePath } : {},
14494
+ details: raw.details ?? "",
14495
+ ...raw.version ? { version: raw.version } : {},
14496
+ ...raw.stability ? { stability: raw.stability } : {}
14497
+ });
14498
+ }
14499
+ return SurfaceSnapshotSchema.parse({
14500
+ projectName: system.name,
14501
+ origin: "generated",
14502
+ stateId: stateIdString(),
14503
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14504
+ interfaces: entries,
14505
+ types: computeTypeClosure(entries, types)
14506
+ });
14507
+ }
14508
+ function projectChildSurface() {
14509
+ return projectOwnSurface("project");
14510
+ }
14511
+ function localName(id) {
14512
+ return id.split("::").pop();
14513
+ }
14514
+ function projectSubsystemSurface(subsystemId) {
14515
+ const system = loadSystemSpec();
14516
+ if (!system) {
14517
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
14518
+ }
14519
+ const subsystems = loadSubsystemSpecs();
14520
+ const target = subsystems.find((s) => s.id === subsystemId);
14521
+ if (!target) {
14522
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
14523
+ }
14524
+ const components = loadComponentSpecs();
14525
+ const interfaces = loadInterfaceSpecs();
14526
+ const types = loadTypeSpecs();
14527
+ const entries = [];
14528
+ const unprojectable = [];
14529
+ for (const pub of target.publicInterfaces ?? []) {
14530
+ if (!pub.component) continue;
14531
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
14532
+ if (!comp) continue;
14533
+ if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
14534
+ unprojectable.push({ component: pub.component, componentType: comp.componentType });
14535
+ continue;
14693
14536
  }
14537
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
14538
+ const methods = compInterfaces.flatMap((i) => i.methods);
14539
+ entries.push({
14540
+ id: localName(pub.interface ?? comp.id),
14541
+ name: comp.name,
14542
+ // Family ceiling: a sibling surface is consumable by the system family only.
14543
+ audience: "project",
14544
+ type: pub.type ?? "Custom",
14545
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
14546
+ // refs by their final segment.
14547
+ component: localName(comp.id),
14548
+ methods,
14549
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
14550
+ // Project the backing component's auth + basePath so the codec can emit
14551
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
14552
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
14553
+ ...comp.basePath ? { basePath: comp.basePath } : {},
14554
+ details: pub.details ?? ""
14555
+ });
14694
14556
  }
14695
-
14696
- renderPanel();
14697
- // Stage G: a deep link may focus a component and/or open a method's narrative
14698
- // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
14699
- // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
14700
- // works whether the seeded view is the component itself or its parent subsystem
14701
- // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
14702
- // and focus the component here).
14703
- (function () {
14704
- if (typeof opts === 'undefined' || !opts) return;
14705
- var f = opts.initialFlow, s = opts.initialSelect;
14706
- var focusComp = (f && f.comp) || (s && s.comp);
14707
- if (focusComp && compById[focusComp]) {
14708
- try { select('component', focusComp, true); } catch (e) { /* ignore */ }
14557
+ for (const skipped of unprojectable) {
14558
+ console.error(
14559
+ `[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, a Gateway, or an Observer (for events).`
14560
+ );
14561
+ }
14562
+ return SurfaceSnapshotSchema.parse({
14563
+ projectName: `${system.name}::${subsystemId}`,
14564
+ origin: "generated",
14565
+ stateId: stateIdString(),
14566
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14567
+ interfaces: entries,
14568
+ types: computeTypeClosure(entries, types)
14569
+ });
14570
+ }
14571
+ function listSnapshots(rootDir = getProjectRoot()) {
14572
+ const dir = surfacesDir(rootDir);
14573
+ if (!fs12.existsSync(dir)) return [];
14574
+ const out = [];
14575
+ for (const file of fs12.readdirSync(dir)) {
14576
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
14577
+ try {
14578
+ out.push(SurfaceSnapshotSchema.parse(readYamlFile(path16.join(dir, file))));
14579
+ } catch {
14709
14580
  }
14710
- if (f && f.comp && f.method && compById[f.comp]) {
14711
- try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
14581
+ }
14582
+ return out;
14583
+ }
14584
+ function snapshotFilename(projectName) {
14585
+ return `${safeFilenamePart(projectName)}.yaml`;
14586
+ }
14587
+ function writeSnapshotIfChanged(snapshot, rootDir) {
14588
+ const dir = surfacesDir(rootDir);
14589
+ fs12.mkdirSync(dir, { recursive: true });
14590
+ const p = path16.join(dir, snapshotFilename(snapshot.projectName));
14591
+ const next = SurfaceSnapshotSchema.parse(snapshot);
14592
+ if (fs12.existsSync(p)) {
14593
+ try {
14594
+ const existing = SurfaceSnapshotSchema.parse(readYamlFile(p));
14595
+ if (surfaceContentKey(existing) === surfaceContentKey(next)) {
14596
+ return { path: p, changed: false };
14597
+ }
14598
+ } catch {
14712
14599
  }
14713
- })();
14714
- })();
14715
- </script>
14716
- </body>
14717
- </html>
14718
- `;
14719
14600
  }
14720
- });
14721
-
14722
- // src/core/rules/repository.ts
14723
- function addRule(rule) {
14724
- ruleSet.push(rule);
14601
+ writeYamlFile(p, next);
14602
+ return { path: p, changed: true };
14725
14603
  }
14726
- function registerBuiltinRules() {
14727
- ruleSet = [];
14728
- for (const rule of SDD_RULES) addRule(rule);
14604
+ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
14605
+ return writeSnapshotIfChanged(snapshot, rootDir).path;
14729
14606
  }
14730
- function registerPackRules(packRules) {
14731
- for (const rule of packRules) addRule(rule);
14607
+ function loadSurfaceSnapshots() {
14608
+ return listSnapshots();
14732
14609
  }
14733
- function ruleSequence() {
14734
- const base = ruleSet.filter((r) => r !== lintAllowsRule);
14735
- return ruleSet.includes(lintAllowsRule) ? [...base, lintAllowsRule] : base;
14610
+ function listMountSnapshots(mounts) {
14611
+ const bound = path16.resolve(getProjectRoot());
14612
+ const out = [];
14613
+ for (const namespace of mounts) {
14614
+ const dir = resolveSubprojectForNamespace(namespace);
14615
+ if (!dir || path16.resolve(dir) === bound) continue;
14616
+ const snapshots = listSnapshots(dir);
14617
+ if (snapshots.length > 0) out.push({ namespace, snapshots });
14618
+ }
14619
+ return out;
14736
14620
  }
14737
- function specScopedRules() {
14738
- return ruleSequence().filter((r) => r.scope === "spec");
14621
+ function loadMountSurfaceSnapshots(mounts) {
14622
+ return listMountSnapshots(mounts);
14739
14623
  }
14740
- var ruleSet;
14741
- var init_repository = __esm({
14742
- "src/core/rules/repository.ts"() {
14624
+ function selectPortalSpec(renderedSet, portalId) {
14625
+ const hit = renderedSet.find((spec) => spec.portalId === portalId);
14626
+ if (!hit) {
14627
+ const known = renderedSet.map((s) => s.portalId).join(", ");
14628
+ throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
14629
+ }
14630
+ return [hit];
14631
+ }
14632
+ function perPortalPath(resolvedOut, portalId) {
14633
+ const ext = path16.extname(resolvedOut);
14634
+ const stem2 = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
14635
+ return `${stem2}.${safeFilenamePart(portalId)}${ext}`;
14636
+ }
14637
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
14638
+ const resolved = path16.resolve(outPath);
14639
+ fs12.mkdirSync(path16.dirname(resolved), { recursive: true });
14640
+ if (!renderedSet || renderedSet.length === 0) {
14641
+ writeYamlFile(resolved, snapshot);
14642
+ return [resolved];
14643
+ }
14644
+ if (renderedSet.length === 1) {
14645
+ fs12.writeFileSync(resolved, renderedSet[0].document);
14646
+ return [resolved];
14647
+ }
14648
+ return renderedSet.map((spec) => {
14649
+ const target = perPortalPath(resolved, spec.portalId);
14650
+ fs12.writeFileSync(target, spec.document);
14651
+ return target;
14652
+ });
14653
+ }
14654
+ function exportResult(snapshot, renderedSet, writtenPaths) {
14655
+ const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
14656
+ return {
14657
+ snapshot,
14658
+ ...rendered !== void 0 ? { rendered } : {},
14659
+ ...renderedSet ? { renderedSet } : {},
14660
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
14661
+ ...writtenPaths.length ? { writtenPaths } : {}
14662
+ };
14663
+ }
14664
+ function exportSurface(maxAudience, format, outPath, portalId) {
14665
+ const snapshot = projectOwnSurface(maxAudience);
14666
+ let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
14667
+ if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
14668
+ const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
14669
+ return exportResult(snapshot, renderedSet, writtenPaths);
14670
+ }
14671
+ function importSurface(sourcePath, origin) {
14672
+ const resolved = path16.resolve(sourcePath);
14673
+ if (!fs12.existsSync(resolved)) {
14674
+ throw new Error(`Surface document not found: ${resolved}`);
14675
+ }
14676
+ const body = fs12.readFileSync(resolved, "utf8");
14677
+ let snapshot;
14678
+ if (isOpenApiDocument(body)) {
14679
+ const projectName = path16.basename(resolved).replace(/\.(json|ya?ml)$/i, "");
14680
+ snapshot = fromOpenApi(body, projectName);
14681
+ snapshot = { ...snapshot, origin };
14682
+ } else {
14683
+ snapshot = SurfaceSnapshotSchema.parse(readYamlFile(resolved));
14684
+ snapshot = { ...snapshot, origin };
14685
+ }
14686
+ saveSnapshot(snapshot);
14687
+ return snapshot;
14688
+ }
14689
+ function pinFamilySurfaces() {
14690
+ const parent = resolveChainingParent();
14691
+ if (!parent) return null;
14692
+ const childRoot = getProjectRoot();
14693
+ const projected = runWithProjectRoot(parent.parentRoot, () => {
14694
+ invalidateSpecCache();
14695
+ const siblings = loadSubsystemSpecs().filter((s) => !s.id.includes("::") && s.id !== parent.subsystemId);
14696
+ return [projectChildSurface(), ...siblings.map((s) => projectSubsystemSurface(s.id))];
14697
+ });
14698
+ const before = new Map(listSnapshots(childRoot).map((s) => [s.projectName, surfaceContentKey(s)]));
14699
+ const changed = [];
14700
+ for (const snapshot of projected) {
14701
+ const stored = saveSnapshot(snapshot, childRoot);
14702
+ if (before.get(snapshot.projectName) !== surfaceContentKey(SurfaceSnapshotSchema.parse(snapshot))) {
14703
+ changed.push(stored);
14704
+ }
14705
+ }
14706
+ return changed;
14707
+ }
14708
+ function computeParentStateId(parentRoot) {
14709
+ return computeStateIdAt(parentRoot);
14710
+ }
14711
+ function listExternalInterfaces() {
14712
+ const snapshots = listSnapshots();
14713
+ const chainingParent = resolveChainingParent();
14714
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
14715
+ return snapshots.map((snapshot) => {
14716
+ const generated = snapshot.origin === "generated";
14717
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
14718
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
14719
+ return {
14720
+ projectName: snapshot.projectName,
14721
+ origin: snapshot.origin,
14722
+ sourceKind,
14723
+ generatedAt: snapshot.generatedAt,
14724
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
14725
+ ...snapshot.version ? { version: snapshot.version } : {},
14726
+ freshness,
14727
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
14728
+ };
14729
+ });
14730
+ }
14731
+ function surfaceContentKey(snapshot) {
14732
+ const { stateId, generatedAt, origin, ...content } = snapshot;
14733
+ return JSON.stringify(content);
14734
+ }
14735
+ var fs12, path16, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
14736
+ var init_surfaces = __esm({
14737
+ "src/core/surfaces.ts"() {
14743
14738
  "use strict";
14744
- init_rules();
14745
- init_lint_allows();
14746
- ruleSet = [];
14739
+ fs12 = __toESM(require("fs"));
14740
+ path16 = __toESM(require("path"));
14741
+ init_fs();
14742
+ init_yaml();
14743
+ init_filenames();
14744
+ init_models();
14745
+ init_specs2();
14746
+ init_statehash();
14747
+ init_type_analysis();
14748
+ init_openapi();
14749
+ SURFACES_DIRNAME = "surfaces";
14750
+ CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
14747
14751
  }
14748
14752
  });
14749
14753
 
@@ -14850,7 +14854,7 @@ var init_approval = __esm({
14850
14854
  // src/core/validation.ts
14851
14855
  var validation_exports = {};
14852
14856
  __export(validation_exports, {
14853
- listRules: () => listRules,
14857
+ listRules: () => listRules2,
14854
14858
  validateAsComplete: () => validateAsComplete,
14855
14859
  validateProjectConfig: () => validateProjectConfig,
14856
14860
  validateRegistry: () => validateRegistry,
@@ -14863,6 +14867,32 @@ function projectPackSelections() {
14863
14867
  return [];
14864
14868
  }
14865
14869
  }
14870
+ function issueKey(issue2) {
14871
+ return `${issue2.code}|${issue2.specId ?? ""}`;
14872
+ }
14873
+ function worstByKey(issues) {
14874
+ const worst = /* @__PURE__ */ new Map();
14875
+ for (const issue2 of issues) {
14876
+ const key = issueKey(issue2);
14877
+ const seen = worst.get(key);
14878
+ if (!seen || SEVERITY_RANK[issue2.severity] > SEVERITY_RANK[seen]) worst.set(key, issue2.severity);
14879
+ }
14880
+ return worst;
14881
+ }
14882
+ function stricterSeverities(parent, child) {
14883
+ const fromParent = parent?.sddRuleSeverity ?? {};
14884
+ const fromChild = child?.sddRuleSeverity ?? {};
14885
+ const codes = /* @__PURE__ */ new Set([...Object.keys(fromParent), ...Object.keys(fromChild)]);
14886
+ if (codes.size === 0) return parent ?? child;
14887
+ const defaults = new Map(knownIssueCodes().map((rc) => [rc.code, rc.defaultSeverity]));
14888
+ const merged = {};
14889
+ for (const code of codes) {
14890
+ const p = fromParent[code] ?? defaults.get(code);
14891
+ const c = fromChild[code] ?? defaults.get(code);
14892
+ merged[code] = p === void 0 || c === void 0 ? p ?? c : SEVERITY_RANK[p] >= SEVERITY_RANK[c] ? p : c;
14893
+ }
14894
+ return { ...parent ?? child, sddRuleSeverity: merged };
14895
+ }
14866
14896
  function issue(severity, code, message, agentId, specId) {
14867
14897
  return { severity, code, message, agentId, specId };
14868
14898
  }
@@ -14944,7 +14974,7 @@ function validateProjectConfig(config) {
14944
14974
  issues
14945
14975
  };
14946
14976
  }
14947
- function listRules() {
14977
+ function listRules2() {
14948
14978
  const extensions = loadProjectExtensions();
14949
14979
  registerBuiltinRules();
14950
14980
  registerPackRules(extensions.rules);
@@ -14956,7 +14986,8 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
14956
14986
  let recursive = true;
14957
14987
  let extensions;
14958
14988
  let treatAllAsComplete = false;
14959
- if (rulesOrOptions && ("scopeSubsystem" in rulesOrOptions || "recursive" in rulesOrOptions || "rules" in rulesOrOptions || "projectType" in rulesOrOptions || "extensions" in rulesOrOptions || "treatAllAsComplete" in rulesOrOptions)) {
14989
+ let crossTree;
14990
+ if (rulesOrOptions && ("scopeSubsystem" in rulesOrOptions || "recursive" in rulesOrOptions || "rules" in rulesOrOptions || "projectType" in rulesOrOptions || "extensions" in rulesOrOptions || "treatAllAsComplete" in rulesOrOptions || "crossTree" in rulesOrOptions)) {
14960
14991
  const opts = rulesOrOptions;
14961
14992
  rules = opts.rules;
14962
14993
  projectType = opts.projectType ?? "backend";
@@ -14964,6 +14995,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
14964
14995
  recursive = opts.recursive ?? true;
14965
14996
  extensions = opts.extensions;
14966
14997
  treatAllAsComplete = opts.treatAllAsComplete ?? false;
14998
+ crossTree = opts.crossTree;
14967
14999
  }
14968
15000
  extensions ??= loadProjectExtensions();
14969
15001
  scanAllSpecs({ recursive });
@@ -15015,6 +15047,9 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
15015
15047
  for (const err of extensions.errors) {
15016
15048
  issues.push(issue("error", "EXTENSION_LOAD_ERROR", err));
15017
15049
  }
15050
+ const mountSurfaceSnapshots = loadMountSurfaceSnapshots(
15051
+ subsystems.filter((s) => s.projectPath).map((s) => s.id)
15052
+ );
15018
15053
  const ctx = buildRuleContext({
15019
15054
  system,
15020
15055
  subsystems,
@@ -15030,6 +15065,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
15030
15065
  // By-name selections only: a legacy path ref pins nothing to check.
15031
15066
  packSelections: projectPackSelections(),
15032
15067
  surfaceSnapshots,
15068
+ mountSurfaceSnapshots,
15033
15069
  codeModel,
15034
15070
  issues
15035
15071
  });
@@ -15038,56 +15074,27 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
15038
15074
  for (const rule of ruleSequence()) {
15039
15075
  rule.check(ctx);
15040
15076
  }
15041
- const hasCrossTreeSuspects = issues.some(
15042
- (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
15043
- );
15044
- const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
15045
- if (chainingParent) {
15046
- let unverified = 0;
15047
- let downgraded = 0;
15048
- for (let at = 0; at < issues.length; at++) {
15049
- const iss = issues[at];
15050
- if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
15051
- issues[at] = {
15052
- severity: "warning",
15053
- code: "UNVERIFIED_EXTERNAL_REF",
15054
- crossTreeContext: true,
15055
- // --ci waives it (parent root is authoritative)
15056
- specId: iss.specId,
15057
- ...iss.agentId ? { agentId: iss.agentId } : {},
15058
- ...iss.draftContext ? { draftContext: true } : {},
15059
- message: `Unverified external reference (${iss.code}): ${iss.message} No vendored surface snapshot covers this reference, so it cannot be verified from this chained subproject standalone \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect what this project can consume via \`wairon surface externals\` / sdd_list_external_interfaces.`
15060
- };
15061
- unverified++;
15062
- continue;
15063
- }
15064
- if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
15065
- if (iss.severity === "error") {
15066
- iss.severity = "warning";
15067
- downgraded++;
15068
- }
15069
- iss.crossTreeContext = true;
15070
- }
15071
- }
15072
- if (unverified > 0 || downgraded > 0) {
15073
- const notes = [];
15074
- if (unverified > 0) {
15075
- notes.push(
15076
- `${unverified} cross-tree reference(s) have no vendored surface snapshot covering them and were reported as UNVERIFIED_EXTERNAL_REF warnings \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect via \`wairon surface externals\` / sdd_list_external_interfaces.`
15077
- );
15078
- }
15079
- if (downgraded > 0) {
15080
- notes.push(
15081
- `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
15082
- );
15083
- }
15084
- issues.unshift({
15085
- severity: "warning",
15086
- code: "CHAINED_SUBPROJECT_CONTEXT",
15087
- crossTreeContext: true,
15088
- message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${notes.join(" ")} Full cross-tree verification runs from the parent root.`
15089
- });
15090
- }
15077
+ const unresolved = (i) => RESOLUTION_FAILURE_CODES.has(i.code) && !i.surfaceResolved;
15078
+ const chainingParent = crossTree !== "off" && issues.some(unresolved) ? findChainingParent(getProjectRoot()) : null;
15079
+ const resolution = chainingParent ? resolveThroughParent(getProjectRoot(), treatAllAsComplete, rules) : null;
15080
+ if (resolution) {
15081
+ const judgedByParent = worstByKey(resolution.issues);
15082
+ const kept = issues.filter((i) => {
15083
+ const judged = judgedByParent.get(issueKey(i));
15084
+ if (judged !== void 0) return SEVERITY_RANK[judged] < SEVERITY_RANK[i.severity];
15085
+ return !unresolved(i);
15086
+ });
15087
+ const keptByChild = worstByKey(kept);
15088
+ const added = resolution.issues.filter((i) => {
15089
+ const own = keptByChild.get(issueKey(i));
15090
+ return own === void 0 || SEVERITY_RANK[own] <= SEVERITY_RANK[i.severity];
15091
+ });
15092
+ const merged = dedupeIssues([...kept, ...added]);
15093
+ return {
15094
+ valid: merged.every((i) => i.severity !== "error"),
15095
+ issues: merged,
15096
+ resolvedThrough: { root: resolution.root, scope: resolution.scope }
15097
+ };
15091
15098
  }
15092
15099
  return {
15093
15100
  valid: issues.every((i) => i.severity !== "error"),
@@ -15099,6 +15106,74 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
15099
15106
  });
15100
15107
  }
15101
15108
  }
15109
+ function resolveThroughParent(boundRoot, treatAllAsComplete, childRules) {
15110
+ const reach = getRequestParentReach();
15111
+ if (reach && !reach.parentReach) return null;
15112
+ const ceiling = reach?.topRoot ? path18.resolve(reach.topRoot) : void 0;
15113
+ const chain = [];
15114
+ let top = path18.resolve(boundRoot);
15115
+ while (top !== ceiling) {
15116
+ const hop = findChainingParent(top);
15117
+ if (!hop) break;
15118
+ const next = path18.resolve(hop.parentRoot);
15119
+ if (ceiling && !isWithinOrEqual(ceiling, next)) break;
15120
+ chain.unshift(hop.subsystemId);
15121
+ top = next;
15122
+ }
15123
+ if (chain.length === 0) return null;
15124
+ const scope = chain.join("::");
15125
+ const inner = runWithProjectRoot(top, () => {
15126
+ invalidateSpecCache();
15127
+ let governing = { rules: stricterSeverities(void 0, childRules) };
15128
+ try {
15129
+ const config = loadProjectConfig();
15130
+ governing = { rules: stricterSeverities(config.rules, childRules), projectType: config.projectType };
15131
+ } catch {
15132
+ }
15133
+ return validateSddTree({
15134
+ ...governing,
15135
+ scopeSubsystem: scope,
15136
+ recursive: true,
15137
+ crossTree: "off",
15138
+ treatAllAsComplete
15139
+ });
15140
+ });
15141
+ if (inner.issues.some((i) => i.code === "MISSING_SYSTEM_SPEC" || i.code === "SUBSYSTEM_NOT_FOUND")) return null;
15142
+ const local = scope.split("::").pop();
15143
+ const issues = inner.issues.map((i) => ({
15144
+ ...i,
15145
+ message: stripNamespace(i.message, scope),
15146
+ ...i.specId !== void 0 ? { specId: i.specId === scope ? local : stripNamespace(i.specId, scope) } : {}
15147
+ }));
15148
+ return { root: top, scope, issues };
15149
+ }
15150
+ function stripNamespace(text2, scope) {
15151
+ const prefix = `${scope}::`;
15152
+ let out = "";
15153
+ let from = 0;
15154
+ for (let at = text2.indexOf(prefix); at !== -1; at = text2.indexOf(prefix, from)) {
15155
+ const atBoundary = at === 0 || !isIdChar(text2[at - 1]);
15156
+ out += text2.slice(from, at) + (atBoundary ? "" : prefix);
15157
+ from = at + prefix.length;
15158
+ }
15159
+ return out + text2.slice(from);
15160
+ }
15161
+ function isIdChar(ch) {
15162
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "-" || ch === "." || ch === ":";
15163
+ }
15164
+ function isWithinOrEqual(dir, target) {
15165
+ const rel2 = path18.relative(dir, target);
15166
+ return rel2 === "" || !rel2.startsWith("..") && !path18.isAbsolute(rel2);
15167
+ }
15168
+ function dedupeIssues(list2) {
15169
+ const seen = /* @__PURE__ */ new Set();
15170
+ return list2.filter((i) => {
15171
+ const key = `${i.code}|${i.specId ?? ""}|${i.message}`;
15172
+ if (seen.has(key)) return false;
15173
+ seen.add(key);
15174
+ return true;
15175
+ });
15176
+ }
15102
15177
  function settledStatusBearing(loaded) {
15103
15178
  let settled;
15104
15179
  try {
@@ -15126,7 +15201,7 @@ function settledStatusBearing(loaded) {
15126
15201
  function validateAsComplete(options) {
15127
15202
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
15128
15203
  }
15129
- var path18, SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
15204
+ var path18, RESOLUTION_FAILURE_CODES, SEVERITY_RANK;
15130
15205
  var init_validation = __esm({
15131
15206
  "src/core/validation.ts"() {
15132
15207
  "use strict";
@@ -15142,26 +15217,15 @@ var init_validation = __esm({
15142
15217
  init_fs();
15143
15218
  path18 = __toESM(require("path"));
15144
15219
  init_approval();
15145
- SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
15220
+ RESOLUTION_FAILURE_CODES = /* @__PURE__ */ new Set([
15146
15221
  "UNDEFINED_TYPE_REFERENCE",
15147
15222
  "INVALID_DEPENDENCY_REFERENCE",
15148
15223
  "INVALID_TARGET_COMPONENT_REFERENCE",
15149
15224
  "INVALID_SUBSYSTEM_REFERENCE",
15150
- "UNDECLARED_DEPENDENCY_CALL",
15151
15225
  "INVALID_TRUSTED_LINK",
15152
- "CROSS_SUBSYSTEM_NON_ADAPTER",
15153
15226
  "CROSS_TREE_REF_UNRESOLVED"
15154
15227
  ]);
15155
- SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
15156
- "MISSING_SOURCE_FILE",
15157
- "SOURCE_PATH_ESCAPES_ROOT",
15158
- "MISSING_SOURCE_PATH",
15159
- "UNREALIZED_METHOD",
15160
- "CONFORMANCE_ANALYSIS_SKIPPED",
15161
- "CONFORMANCE_DEGRADED",
15162
- "UNDECLARED_DEPENDENCY",
15163
- "UNREALIZED_DEPENDENCY"
15164
- ]);
15228
+ SEVERITY_RANK = { off: 0, warning: 1, error: 2 };
15165
15229
  }
15166
15230
  });
15167
15231
 
@@ -15796,6 +15860,20 @@ function qualifyId(id, prefix, rootSubsystems) {
15796
15860
  }
15797
15861
  return prefix ? `${prefix}::${id}` : id;
15798
15862
  }
15863
+ function qualifyDeclaredId(id, prefix, mountRealization = false) {
15864
+ if (!id || !prefix) return id;
15865
+ if (id.startsWith("::") || id.startsWith("super::")) {
15866
+ return qualifyId(id, prefix, NO_ROOT_SUBSYSTEMS);
15867
+ }
15868
+ if (mountRealization && id === prefix.split("::").pop()) {
15869
+ return prefix;
15870
+ }
15871
+ return `${prefix}::${id}`;
15872
+ }
15873
+ function qualifySubsystemRef(id, prefix, rootSubsystems) {
15874
+ if (prefix && !id.includes("::") && id === prefix.split("::").pop()) return prefix;
15875
+ return qualifyId(id, prefix, rootSubsystems);
15876
+ }
15799
15877
  function splitNamespace(qualifiedId2) {
15800
15878
  if (!qualifiedId2.includes("::")) {
15801
15879
  return { prefix: "", localId: qualifiedId2 };
@@ -15991,6 +16069,19 @@ function stripNamespaceFromImplementation(spec, prefix) {
15991
16069
  }))
15992
16070
  };
15993
16071
  }
16072
+ function childRelativeFilePaths(spec, authoringRoot, childRoot) {
16073
+ const reexpress = (p) => {
16074
+ if (path19.isAbsolute(p)) return p;
16075
+ const rel2 = path19.relative(childRoot, path19.resolve(authoringRoot, p));
16076
+ const inside = rel2 !== "" && rel2 !== ".." && !rel2.startsWith(`..${path19.sep}`) && !path19.isAbsolute(rel2);
16077
+ return inside ? rel2.replace(/\\/g, "/") : p;
16078
+ };
16079
+ return {
16080
+ ...spec,
16081
+ ...spec.sourcePath ? { sourcePath: reexpress(spec.sourcePath) } : {},
16082
+ ...spec.simPath ? { simPath: reexpress(spec.simPath) } : {}
16083
+ };
16084
+ }
15994
16085
  function stripNamespaceFromType(spec, prefix) {
15995
16086
  return {
15996
16087
  ...spec,
@@ -16183,7 +16274,14 @@ function buildProjectGraph(level) {
16183
16274
  return buildGraphModel(level);
16184
16275
  }
16185
16276
  function resolveChainingParent() {
16186
- return findChainingParent(getProjectRoot());
16277
+ const reach = getRequestParentReach();
16278
+ if (reach && !reach.parentReach) return null;
16279
+ const parent = findChainingParent(getProjectRoot());
16280
+ if (parent && reach?.topRoot) {
16281
+ const fromTop = path19.relative(path19.resolve(reach.topRoot), path19.resolve(parent.parentRoot));
16282
+ if (fromTop === ".." || fromTop.startsWith(`..${path19.sep}`) || path19.isAbsolute(fromTop)) return null;
16283
+ }
16284
+ return parent;
16187
16285
  }
16188
16286
  function computeGateStateId() {
16189
16287
  let gate = {};
@@ -16248,7 +16346,7 @@ function findLegacySpecFiles() {
16248
16346
  function updateSpec(kind, id, delta, hooks) {
16249
16347
  return current().updateSpec(kind, id, delta, hooks);
16250
16348
  }
16251
- var fs13, path19, SIGNATURE_TTL_MS, SpecWorkspace, workspaces;
16349
+ var fs13, path19, NO_ROOT_SUBSYSTEMS, SIGNATURE_TTL_MS, SpecWorkspace, workspaces;
16252
16350
  var init_specs2 = __esm({
16253
16351
  "src/core/specs.ts"() {
16254
16352
  "use strict";
@@ -16263,6 +16361,7 @@ var init_specs2 = __esm({
16263
16361
  init_models();
16264
16362
  init_narrative_labels();
16265
16363
  init_diagram();
16364
+ NO_ROOT_SUBSYSTEMS = /* @__PURE__ */ new Set();
16266
16365
  SIGNATURE_TTL_MS = 2e3;
16267
16366
  SpecWorkspace = class {
16268
16367
  constructor(rootDir) {
@@ -16309,6 +16408,18 @@ var init_specs2 = __esm({
16309
16408
  this.lastSignatureCheckMs = Date.now();
16310
16409
  return this.cachedIndex;
16311
16410
  }
16411
+ /**
16412
+ * The id a scan-time loader issue is anchored to: the refused spec's own id
16413
+ * when the file got far enough to carry one, else its name on disk (the
16414
+ * containing directory in the nested layout, where every file is `.index` /
16415
+ * `.interface`), qualified into the mount's namespace exactly as the loader
16416
+ * qualifies the spec itself — so scoping a validate to a mount keeps it.
16417
+ */
16418
+ loaderIssueSpecId(file, rawId, namespacePrefix) {
16419
+ const stem2 = path19.basename(file, ".yaml");
16420
+ const local = rawId ?? (stem2.startsWith(".") ? path19.basename(path19.dirname(file)) : stem2);
16421
+ return namespacePrefix ? qualifyId(local, namespacePrefix, this.rootSubsystems) : local;
16422
+ }
16312
16423
  scanSpecsForProject(projectDir, namespacePrefix, visitedDirs, maxDepth, currentDepth) {
16313
16424
  const index = emptyIndex();
16314
16425
  const projectPaths = aiPathsAt(projectDir);
@@ -16322,6 +16433,7 @@ var init_specs2 = __esm({
16322
16433
  const normFile = path19.normalize(file);
16323
16434
  if (normFile === systemYaml) continue;
16324
16435
  let detectedType = "spec";
16436
+ let rawId;
16325
16437
  try {
16326
16438
  const raw = readYamlFile(file);
16327
16439
  if (raw === null || typeof raw !== "object") {
@@ -16329,10 +16441,12 @@ var init_specs2 = __esm({
16329
16441
  severity: "error",
16330
16442
  code: "INVALID_YAML",
16331
16443
  message: `Spec file "${file}" is not a valid YAML object or is empty.`,
16332
- specId: path19.basename(file, ".yaml")
16444
+ specId: this.loaderIssueSpecId(file, void 0, namespacePrefix)
16333
16445
  });
16334
16446
  continue;
16335
16447
  }
16448
+ const idField = raw.id;
16449
+ if (typeof idField === "string" && idField) rawId = idField;
16336
16450
  if ("parentSystem" in raw) {
16337
16451
  detectedType = "subsystem";
16338
16452
  const parsed = SubsystemSpecSchema.parse(raw);
@@ -16384,21 +16498,20 @@ var init_specs2 = __esm({
16384
16498
  severity: "error",
16385
16499
  code: "UNKNOWN_SPEC_TYPE",
16386
16500
  message: `Spec file "${file}" does not match any recognized L1-L4 schema structure.`,
16387
- specId: path19.basename(file, ".yaml")
16501
+ specId: this.loaderIssueSpecId(file, rawId, namespacePrefix)
16388
16502
  });
16389
16503
  }
16390
16504
  } catch (e) {
16391
- const filename = path19.basename(file, ".yaml");
16392
16505
  this.loaderIssues.push({
16393
16506
  severity: "error",
16394
16507
  code: "SCHEMA_VALIDATION_ERROR",
16395
16508
  message: `Failed to parse ${detectedType} spec "${file}": ${e.message || String(e)}`,
16396
- specId: filename
16509
+ specId: this.loaderIssueSpecId(file, rawId, namespacePrefix)
16397
16510
  });
16398
16511
  }
16399
16512
  }
16400
16513
  index.subsystems = index.subsystems.map((sub) => {
16401
- const qualifiedSubId = namespacePrefix ? qualifyId(sub.id, namespacePrefix, this.rootSubsystems) : sub.id;
16514
+ const qualifiedSubId = namespacePrefix ? qualifyDeclaredId(sub.id, namespacePrefix, true) : sub.id;
16402
16515
  const componentPrefix = sub.projectPath ? qualifiedSubId : namespacePrefix;
16403
16516
  return {
16404
16517
  ...sub,
@@ -16417,14 +16530,14 @@ var init_specs2 = __esm({
16417
16530
  const originalSubsystemPaths = index.paths.subsystem;
16418
16531
  index.paths.subsystem = {};
16419
16532
  for (const [k, v] of Object.entries(originalSubsystemPaths)) {
16420
- const qualifiedK = namespacePrefix ? qualifyId(k, namespacePrefix, this.rootSubsystems) : k;
16533
+ const qualifiedK = namespacePrefix ? qualifyDeclaredId(k, namespacePrefix, true) : k;
16421
16534
  index.paths.subsystem[qualifiedK] = v;
16422
16535
  }
16423
16536
  if (namespacePrefix) {
16424
16537
  index.components = index.components.map((comp) => ({
16425
16538
  ...comp,
16426
- id: qualifyId(comp.id, namespacePrefix, this.rootSubsystems),
16427
- subsystem: qualifyId(comp.subsystem, namespacePrefix, this.rootSubsystems),
16539
+ id: qualifyDeclaredId(comp.id, namespacePrefix),
16540
+ subsystem: qualifySubsystemRef(comp.subsystem, namespacePrefix, this.rootSubsystems),
16428
16541
  owns: comp.owns.map((o) => qualifyId(o, namespacePrefix, this.rootSubsystems)),
16429
16542
  dependsOn: comp.dependsOn.map((d) => qualifyId(d, namespacePrefix, this.rootSubsystems)),
16430
16543
  dispatch: comp.dispatch?.map((b) => ({
@@ -16434,12 +16547,12 @@ var init_specs2 = __esm({
16434
16547
  }));
16435
16548
  index.interfaces = index.interfaces.map((intf) => ({
16436
16549
  ...intf,
16437
- id: qualifyId(intf.id, namespacePrefix, this.rootSubsystems),
16550
+ id: qualifyDeclaredId(intf.id, namespacePrefix),
16438
16551
  component: qualifyId(intf.component, namespacePrefix, this.rootSubsystems)
16439
16552
  }));
16440
16553
  index.implementations = index.implementations.map((impl) => ({
16441
16554
  ...impl,
16442
- id: qualifyId(impl.id, namespacePrefix, this.rootSubsystems),
16555
+ id: qualifyDeclaredId(impl.id, namespacePrefix),
16443
16556
  contract: qualifyId(impl.contract, namespacePrefix, this.rootSubsystems),
16444
16557
  methods: impl.methods.map((m) => ({
16445
16558
  ...m,
@@ -16451,13 +16564,13 @@ var init_specs2 = __esm({
16451
16564
  }));
16452
16565
  index.types = index.types.map((t) => ({
16453
16566
  ...t,
16454
- id: qualifyId(t.id, namespacePrefix, this.rootSubsystems),
16455
- subsystem: t.subsystem ? qualifyId(t.subsystem, namespacePrefix, this.rootSubsystems) : void 0,
16567
+ id: qualifyDeclaredId(t.id, namespacePrefix),
16568
+ subsystem: t.subsystem ? qualifySubsystemRef(t.subsystem, namespacePrefix, this.rootSubsystems) : void 0,
16456
16569
  group: t.group ? qualifyId(t.group, namespacePrefix, this.rootSubsystems) : void 0
16457
16570
  }));
16458
16571
  index.groups = index.groups.map((g) => ({
16459
16572
  ...g,
16460
- id: qualifyId(g.id, namespacePrefix, this.rootSubsystems)
16573
+ id: qualifyDeclaredId(g.id, namespacePrefix)
16461
16574
  }));
16462
16575
  const originalPaths = index.paths;
16463
16576
  index.paths = {
@@ -16469,19 +16582,19 @@ var init_specs2 = __esm({
16469
16582
  group: {}
16470
16583
  };
16471
16584
  for (const [k, v] of Object.entries(originalPaths.component)) {
16472
- index.paths.component[qualifyId(k, namespacePrefix, this.rootSubsystems)] = v;
16585
+ index.paths.component[qualifyDeclaredId(k, namespacePrefix)] = v;
16473
16586
  }
16474
16587
  for (const [k, v] of Object.entries(originalPaths.interface)) {
16475
- index.paths.interface[qualifyId(k, namespacePrefix, this.rootSubsystems)] = v;
16588
+ index.paths.interface[qualifyDeclaredId(k, namespacePrefix)] = v;
16476
16589
  }
16477
16590
  for (const [k, v] of Object.entries(originalPaths.implementation)) {
16478
- index.paths.implementation[qualifyId(k, namespacePrefix, this.rootSubsystems)] = v;
16591
+ index.paths.implementation[qualifyDeclaredId(k, namespacePrefix)] = v;
16479
16592
  }
16480
16593
  for (const [k, v] of Object.entries(originalPaths.type)) {
16481
- index.paths.type[qualifyId(k, namespacePrefix, this.rootSubsystems)] = v;
16594
+ index.paths.type[qualifyDeclaredId(k, namespacePrefix)] = v;
16482
16595
  }
16483
16596
  for (const [k, v] of Object.entries(originalPaths.group)) {
16484
- index.paths.group[qualifyId(k, namespacePrefix, this.rootSubsystems)] = v;
16597
+ index.paths.group[qualifyDeclaredId(k, namespacePrefix)] = v;
16485
16598
  }
16486
16599
  }
16487
16600
  if (currentDepth < maxDepth) {
@@ -16875,7 +16988,11 @@ var init_specs2 = __esm({
16875
16988
  }
16876
16989
  prepareImplementationForWrite(spec) {
16877
16990
  const prefix = this.writePrefixFor(spec.id);
16878
- return prefix ? stripNamespaceFromImplementation(spec, prefix) : spec;
16991
+ if (!prefix) return spec;
16992
+ const stripped = stripNamespaceFromImplementation(spec, prefix);
16993
+ const mount = this.getSubprojectPrefix(spec.id);
16994
+ const childRoot = mount ? this.resolveSubprojectForNamespace(mount) : null;
16995
+ return childRoot ? childRelativeFilePaths(stripped, this.rootDir, childRoot) : stripped;
16879
16996
  }
16880
16997
  prepareTypeForWrite(spec) {
16881
16998
  const prefix = this.writePrefixFor(spec.id);
@@ -18174,6 +18291,7 @@ function externalizeSubsystem(subsystemId, projectPath) {
18174
18291
  });
18175
18292
  ensureDir(path20.dirname(childFooDir));
18176
18293
  fs14.renameSync(fooDir, childFooDir);
18294
+ rebaseImplementationPaths(childFooDir, parentRoot, childDir);
18177
18295
  patchSubsystemIndex(path20.join(childFooDir, ".index.yaml"), (s) => {
18178
18296
  s.parentSystem = childSystemName;
18179
18297
  delete s.projectPath;
@@ -18224,6 +18342,7 @@ function internalizeSubsystem(subsystemId) {
18224
18342
  fs14.rmSync(fooDir, { recursive: true, force: true });
18225
18343
  ensureDir(path20.dirname(fooDir));
18226
18344
  fs14.renameSync(childFooDir, fooDir);
18345
+ rebaseImplementationPaths(fooDir, childDir, parentRoot);
18227
18346
  patchSubsystemIndex(path20.join(fooDir, ".index.yaml"), (s) => {
18228
18347
  s.parentSystem = parentSystemName;
18229
18348
  delete s.projectPath;
@@ -18324,6 +18443,28 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18324
18443
  if (changed) writeYamlFile(file, raw);
18325
18444
  }
18326
18445
  }
18446
+ function rebaseImplementationPaths(specsDir, fromRoot, toRoot) {
18447
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
18448
+ let raw;
18449
+ try {
18450
+ raw = readYamlFile(file);
18451
+ } catch {
18452
+ continue;
18453
+ }
18454
+ if (!raw || typeof raw !== "object" || !("contract" in raw)) continue;
18455
+ let changed = false;
18456
+ for (const key of ["sourcePath", "simPath"]) {
18457
+ const p = raw[key];
18458
+ if (typeof p !== "string" || p === "" || path20.isAbsolute(p)) continue;
18459
+ const next = toPosixPath(path20.relative(toRoot, path20.resolve(fromRoot, p)));
18460
+ if (next !== p) {
18461
+ raw[key] = next;
18462
+ changed = true;
18463
+ }
18464
+ }
18465
+ if (changed) writeYamlFile(file, raw);
18466
+ }
18467
+ }
18327
18468
  function patchSubsystemIndex(indexPath, mutate) {
18328
18469
  if (!fs14.existsSync(indexPath)) return;
18329
18470
  const raw = readYamlFile(indexPath);
@@ -23578,7 +23719,7 @@ NOTICE:
23578
23719
  name: import_zod10.z.string().describe("Human-readable implementation name"),
23579
23720
  description: import_zod10.z.string().describe("Implementation details"),
23580
23721
  contract: import_zod10.z.string().describe("The L3 Interface contract ID this implements"),
23581
- sourcePath: import_zod10.z.string().optional().describe("Optional: target source code file path relative to project root"),
23722
+ sourcePath: import_zod10.z.string().optional().describe("Optional: target source code file path relative to project root \u2014 for a chained subproject's implementation (qualified id), relative to that subproject's root; a path given relative to this root that lands inside the subproject is re-expressed for you"),
23582
23723
  simPath: import_zod10.z.string().optional().describe("Optional: the committed integration-sim harness file (project-relative; N:1 sharing allowed). The validator proves it exists and its import graph wires the REAL modules (this component + each direct dependency; technology adapters may stay faked) \u2014 running it is CI's job. Declaring the first simPath in a subsystem activates MISSING_INTEGRATION_SIM for its other complete non-leaf implementations"),
23583
23724
  technologies: import_zod10.z.array(import_zod10.z.string()).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"]) \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.`),
23584
23725
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
@@ -23767,7 +23908,8 @@ NOTICE:
23767
23908
  return json({
23768
23909
  valid: result.valid,
23769
23910
  errors: result.issues.filter((i) => i.severity === "error"),
23770
- warnings: result.issues.filter((i) => i.severity === "warning")
23911
+ warnings: result.issues.filter((i) => i.severity === "warning"),
23912
+ ...result.resolvedThrough ? { resolvedThrough: result.resolvedThrough } : {}
23771
23913
  });
23772
23914
  } catch (e) {
23773
23915
  return errText(String(e));
@@ -25189,19 +25331,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
25189
25331
  const backupDir = path28.join(root, BACKUP_DIR, timestampSlug());
25190
25332
  let backedUp = false;
25191
25333
  for (const rootRel of roots) {
25192
- const relative14 = rootRel === "." ? "" : rootRel;
25193
- const stagedSpecDir = path28.join(stagingDir, relative14, ".wai");
25334
+ const relative15 = rootRel === "." ? "" : rootRel;
25335
+ const stagedSpecDir = path28.join(stagingDir, relative15, ".wai");
25194
25336
  if (!fs18.existsSync(stagedSpecDir)) continue;
25195
- const liveSpecDir = liveSpecDirFor(path28.join(root, relative14));
25337
+ const liveSpecDir = liveSpecDirFor(path28.join(root, relative15));
25196
25338
  if (backupExisting && liveSpecDir && fs18.existsSync(liveSpecDir)) {
25197
- const backupTarget = path28.join(backupDir, relative14, path28.basename(liveSpecDir));
25339
+ const backupTarget = path28.join(backupDir, relative15, path28.basename(liveSpecDir));
25198
25340
  fs18.mkdirSync(path28.dirname(backupTarget), { recursive: true });
25199
25341
  fs18.renameSync(liveSpecDir, backupTarget);
25200
25342
  backedUp = true;
25201
25343
  } else if (liveSpecDir && fs18.existsSync(liveSpecDir)) {
25202
25344
  fs18.rmSync(liveSpecDir, { recursive: true, force: true });
25203
25345
  }
25204
- const target = path28.join(root, relative14, ".wai");
25346
+ const target = path28.join(root, relative15, ".wai");
25205
25347
  fs18.mkdirSync(path28.dirname(target), { recursive: true });
25206
25348
  if (fs18.existsSync(target)) fs18.rmSync(target, { recursive: true, force: true });
25207
25349
  fs18.renameSync(stagedSpecDir, target);
@@ -26166,9 +26308,9 @@ init_loader();
26166
26308
  init_validation();
26167
26309
  function isCiDraftWaivable(issue2) {
26168
26310
  if (issue2.severity !== "warning") return false;
26311
+ if (issue2.code === "DRAFT_SUBSYSTEM_WARNING") return true;
26169
26312
  if (issue2.code === "DRAFT_COMPONENT_WARNING") return true;
26170
26313
  if (issue2.code === "UNUSED_COMPONENT") return issue2.draftContext === true;
26171
- if (issue2.crossTreeContext === true) return true;
26172
26314
  return false;
26173
26315
  }
26174
26316
  async function runValidate(options = {}) {
@@ -26233,6 +26375,11 @@ async function runValidate(options = {}) {
26233
26375
  scopeSubsystem: options.subsystem,
26234
26376
  recursive: options.recursive ?? true
26235
26377
  });
26378
+ if (sddResult.resolvedThrough) {
26379
+ logger.info(
26380
+ `Chained subproject \u2014 verified through the parent project at ${sddResult.resolvedThrough.root} (mount "${sddResult.resolvedThrough.scope}").`
26381
+ );
26382
+ }
26236
26383
  if (sddResult.issues.length === 0) {
26237
26384
  logger.success("Spec tree is valid and component type boundaries are enforced.");
26238
26385
  } else {
@@ -26427,14 +26574,14 @@ var http = __toESM(require("http"));
26427
26574
  var https = __toESM(require("https"));
26428
26575
  init_defaults();
26429
26576
  function downloadFile(url, dest) {
26430
- return new Promise((resolve29, reject) => {
26577
+ return new Promise((resolve30, reject) => {
26431
26578
  const file = fs22.createWriteStream(dest);
26432
26579
  const get4 = url.startsWith("https://") ? https.get : http.get;
26433
26580
  get4(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
26434
26581
  if (res.statusCode === 301 || res.statusCode === 302) {
26435
26582
  file.close();
26436
26583
  res.destroy();
26437
- downloadFile(res.headers.location, dest).then(resolve29).catch(reject);
26584
+ downloadFile(res.headers.location, dest).then(resolve30).catch(reject);
26438
26585
  return;
26439
26586
  }
26440
26587
  if (res.statusCode !== 200) {
@@ -26446,7 +26593,7 @@ function downloadFile(url, dest) {
26446
26593
  res.pipe(file);
26447
26594
  file.on("finish", () => {
26448
26595
  res.destroy();
26449
- file.close(() => resolve29());
26596
+ file.close(() => resolve30());
26450
26597
  });
26451
26598
  file.on("error", (err) => {
26452
26599
  res.destroy();
@@ -26623,7 +26770,7 @@ function releaseChannelLabel(tag) {
26623
26770
  return version.slice(dash + 1).replace(/\.\d+$/, "") || "stable";
26624
26771
  }
26625
26772
  function fetchReleases(repo) {
26626
- return new Promise((resolve29, reject) => {
26773
+ return new Promise((resolve30, reject) => {
26627
26774
  const url = `https://api.github.com/repos/${repo}/releases?per_page=100`;
26628
26775
  const options = {
26629
26776
  headers: {
@@ -26643,7 +26790,7 @@ function fetchReleases(repo) {
26643
26790
  return;
26644
26791
  }
26645
26792
  try {
26646
- resolve29(JSON.parse(data));
26793
+ resolve30(JSON.parse(data));
26647
26794
  } catch {
26648
26795
  reject(new Error("Failed to parse GitHub API response"));
26649
26796
  }
@@ -26818,7 +26965,7 @@ async function filteredCheckbox(config) {
26818
26965
  32,
26819
26966
  Math.max(...items.map((i) => i.label.length))
26820
26967
  );
26821
- return new Promise((resolve29) => {
26968
+ return new Promise((resolve30) => {
26822
26969
  const checked = /* @__PURE__ */ new Set();
26823
26970
  let cursor = 0;
26824
26971
  let filterIdx = 0;
@@ -26885,7 +27032,7 @@ async function filteredCheckbox(config) {
26885
27032
  );
26886
27033
  teardown();
26887
27034
  const result = items.filter((_, idx) => checked.has(idx)).map((i) => i.value);
26888
- resolve29(result);
27035
+ resolve30(result);
26889
27036
  }
26890
27037
  function abort() {
26891
27038
  process.stdout.write("\n");
@@ -27635,7 +27782,7 @@ init_logger();
27635
27782
  init_validation();
27636
27783
  init_extensions();
27637
27784
  init_loader();
27638
- async function listRules2() {
27785
+ async function listRules3() {
27639
27786
  let overrides = {};
27640
27787
  if (isProjectInitialized()) {
27641
27788
  try {
@@ -27650,7 +27797,7 @@ async function listRules2() {
27650
27797
  };
27651
27798
  const ext = loadProjectExtensions();
27652
27799
  const packRuleNames = new Set(ext.rules.map((r) => r.name));
27653
- const active = listRules();
27800
+ const active = listRules2();
27654
27801
  const builtin = active.filter((r) => !packRuleNames.has(r.name));
27655
27802
  const packRules = active.filter((r) => packRuleNames.has(r.name));
27656
27803
  const groups = [
@@ -35085,7 +35232,7 @@ async function awaitApproval(cfg, credential, requestId, timeoutSeconds) {
35085
35232
  let current2 = req;
35086
35233
  while (current2.status === "pending" && Date.now() < deadline) {
35087
35234
  const remaining = deadline - Date.now();
35088
- await new Promise((resolve29) => setTimeout(resolve29, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
35235
+ await new Promise((resolve30) => setTimeout(resolve30, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
35089
35236
  expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
35090
35237
  current2 = getApprovalRequestById(cfg.dataDir, requestId) ?? current2;
35091
35238
  }
@@ -36312,7 +36459,7 @@ var ShareSnapshotIndex = class {
36312
36459
  function putSnapshot(dataDir, snapshot) {
36313
36460
  return new ShareSnapshotRegistry(new ShareSnapshotStore(dataDir)).put(snapshot);
36314
36461
  }
36315
- function getSnapshot2(dataDir, snapshotId) {
36462
+ function getSnapshot(dataDir, snapshotId) {
36316
36463
  return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
36317
36464
  }
36318
36465
  function getSnapshotArtifact(dataDir, snapshotId, kind, portalId) {
@@ -39810,7 +39957,8 @@ var PROJECT_RECORD_TOOLS = /* @__PURE__ */ new Set([
39810
39957
  "sdd_host_initialize_project",
39811
39958
  "sdd_host_get_approval_status",
39812
39959
  "sdd_host_await_approval",
39813
- ...PROJECT_OPS_TOOLS
39960
+ ...PROJECT_OPS_TOOLS,
39961
+ ...LANDSCAPE_DISCOVERY_TOOLS
39814
39962
  ]);
39815
39963
  function jsonRpcRequests(body) {
39816
39964
  const arr = Array.isArray(body) ? body : [body];
@@ -39838,8 +39986,16 @@ var WRITE_TOOL_PREFIXES = [
39838
39986
  "sdd_move_"
39839
39987
  ];
39840
39988
  var READ_TOOL_PREFIXES = ["sdd_get_", "sdd_validate_"];
39989
+ var READ_TOOL_NAMES = /* @__PURE__ */ new Set([
39990
+ "sdd_list_external_interfaces",
39991
+ "listAgents",
39992
+ "getAgent",
39993
+ "listDomains",
39994
+ "validateTopology",
39995
+ "getProjectConfig"
39996
+ ]);
39841
39997
  function requiredDataPlaneCapability(toolName) {
39842
- if (READ_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) return "project:read";
39998
+ if (READ_TOOL_NAMES.has(toolName) || READ_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) return "project:read";
39843
39999
  if (WRITE_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) return "project:write";
39844
40000
  return "project:write";
39845
40001
  }
@@ -40043,7 +40199,9 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
40043
40199
  });
40044
40200
  return;
40045
40201
  }
40046
- await runWithProjectRoot(binding.rootPath, async () => {
40202
+ const topRoot = existingProjectRoot(cfg.dataDir, binding.projectId) ?? binding.rootPath;
40203
+ const parentReach = principal.projects.includes("*") || principal.projects.includes(binding.projectId);
40204
+ await runWithProjectBinding(binding.rootPath, { topRoot, parentReach }, async () => {
40047
40205
  const projectId = binding.projectId;
40048
40206
  const subproject = binding.subproject;
40049
40207
  const confinementError = subprojectConfinementError(projectId, subproject, body);
@@ -40134,7 +40292,7 @@ function resolveSharedView(cfg, token, meta) {
40134
40292
  record(cfg.dataDir, link?.id ?? "", meta, check.outcome);
40135
40293
  return { found: false, outcome: check.outcome };
40136
40294
  }
40137
- const snapshot = getSnapshot2(cfg.dataDir, check.link.snapshotId);
40295
+ const snapshot = getSnapshot(cfg.dataDir, check.link.snapshotId);
40138
40296
  record(cfg.dataDir, check.link.id, meta, "served");
40139
40297
  return {
40140
40298
  found: true,
@@ -41343,11 +41501,11 @@ async function runServe(options = {}) {
41343
41501
  logger.info(` data dir: ${import_chalk17.default.gray(cfg.dataDir)}`);
41344
41502
  logger.blank();
41345
41503
  logger.info("Press Ctrl+C to stop.");
41346
- await new Promise((resolve29) => {
41504
+ await new Promise((resolve30) => {
41347
41505
  const shutdown = () => {
41348
41506
  logger.info("Shutting down\u2026");
41349
41507
  handle.close();
41350
- resolve29();
41508
+ resolve30();
41351
41509
  };
41352
41510
  process.on("SIGINT", shutdown);
41353
41511
  process.on("SIGTERM", shutdown);
@@ -41412,11 +41570,11 @@ async function runDev(options = {}) {
41412
41570
  logger.blank();
41413
41571
  logger.info("An agent edits specs; refresh the page to see the live graph. Press Ctrl+C to stop.");
41414
41572
  if (options.open) openBrowser(url);
41415
- await new Promise((resolve29) => {
41573
+ await new Promise((resolve30) => {
41416
41574
  const shutdown = () => {
41417
41575
  logger.info("Shutting down\u2026");
41418
41576
  handle.close();
41419
- resolve29();
41577
+ resolve30();
41420
41578
  };
41421
41579
  process.on("SIGINT", shutdown);
41422
41580
  process.on("SIGTERM", shutdown);
@@ -41967,16 +42125,6 @@ async function runSurface(action, options = {}) {
41967
42125
  }
41968
42126
  return;
41969
42127
  }
41970
- case "generate-children": {
41971
- const written = generateChildSnapshots();
41972
- if (!written.length) {
41973
- logger.info("Delivered surfaces are already up to date \u2014 nothing rewritten.");
41974
- return;
41975
- }
41976
- logger.success(`Updated ${written.length} delivered surface(s):`);
41977
- for (const p of written) logger.info(` ${p}`);
41978
- return;
41979
- }
41980
42128
  case "externals": {
41981
42129
  const entries = listExternalInterfaces();
41982
42130
  if (!entries.length) {
@@ -41991,8 +42139,22 @@ async function runSurface(action, options = {}) {
41991
42139
  }
41992
42140
  return;
41993
42141
  }
42142
+ case "pin": {
42143
+ const written = pinFamilySurfaces();
42144
+ if (written === null) {
42145
+ logger.info("This project is not a chained subproject \u2014 there is no parent family to pin.");
42146
+ return;
42147
+ }
42148
+ if (!written.length) {
42149
+ logger.info("Pinned family surfaces are already up to date \u2014 nothing rewritten.");
42150
+ return;
42151
+ }
42152
+ logger.success(`Pinned ${written.length} family surface(s) from the parent:`);
42153
+ for (const p of written) logger.info(` ${p}`);
42154
+ return;
42155
+ }
41994
42156
  default:
41995
- throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children, externals).`);
42157
+ throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, externals, pin).`);
41996
42158
  }
41997
42159
  }
41998
42160
 
@@ -42732,12 +42894,6 @@ async function runLock2(options) {
42732
42894
  logger.info("Cancelled. Nothing was changed.");
42733
42895
  return;
42734
42896
  }
42735
- const childPaths = generateChildSnapshots();
42736
- if (childPaths.length > 0) {
42737
- logger.blank();
42738
- logger.success(`Updated ${childPaths.length} delivered surface(s) in the chained children:`);
42739
- for (const p of childPaths) logger.info(` ${p}`);
42740
- }
42741
42897
  logger.blank();
42742
42898
  await runGenerate({ domain: options.subsystem });
42743
42899
  logger.blank();
@@ -42793,7 +42949,7 @@ program.command("doctor").description("Health check: flags stale generated guide
42793
42949
  await runDoctor({ fix: opts.fix });
42794
42950
  });
42795
42951
  async function runRules() {
42796
- await listRules2();
42952
+ await listRules3();
42797
42953
  }
42798
42954
  async function runPatterns() {
42799
42955
  await listPatterns();
@@ -43000,7 +43156,7 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
43000
43156
  program.command("produce <target>").description("project the local project's specs to a producer target (notion | miro)").option("--page <id>", "parent page/board id in the target").option("--token <token>", "integration token (else env, else interactive prompt)").action(async (target, opts) => {
43001
43157
  await runProduce(target, { page: opts.page, token: opts.token });
43002
43158
  });
43003
- program.command("surface <action>").description("public surface exchange: export | import | list | generate-children | externals").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
43159
+ program.command("surface <action>").description("public surface exchange: export | import | list | externals | pin").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
43004
43160
  await runSurface(action, {
43005
43161
  audience: opts.audience,
43006
43162
  format: opts.format,