@wairon/cli 5.1.1-dev.106 → 5.1.1-dev.107

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/index.js CHANGED
@@ -3547,6 +3547,7 @@ __export(src_exports, {
3547
3547
  SurfaceSnapshotSchema: () => SurfaceSnapshotSchema,
3548
3548
  SurfaceTypeDefSchema: () => SurfaceTypeDefSchema,
3549
3549
  SurfaceTypeExportSchema: () => SurfaceTypeExportSchema,
3550
+ SurfaceUnresolvedExportSchema: () => SurfaceUnresolvedExportSchema,
3550
3551
  SwitchCaseSchema: () => SwitchCaseSchema,
3551
3552
  SystemPublicInterfaceSchema: () => SystemPublicInterfaceSchema,
3552
3553
  SystemSpecSchema: () => SystemSpecSchema,
@@ -3580,12 +3581,14 @@ __export(src_exports, {
3580
3581
  approvalKeyIn: () => approvalKeyIn,
3581
3582
  approvalRecord: () => approvalRecord,
3582
3583
  approvalVerdict: () => approvalVerdict,
3584
+ approvedStatuses: () => approvedStatuses,
3583
3585
  assertContainedProjectPath: () => assertContainedProjectPath,
3584
3586
  assertProjectInitialized: () => assertProjectInitialized,
3585
3587
  attachRetiredMounts: () => attachRetiredMounts,
3586
3588
  backendCommand: () => backendCommand,
3587
3589
  backfillChainedSubprojectConfigs: () => backfillChainedSubprojectConfigs,
3588
3590
  backupBeside: () => backupBeside,
3591
+ bindDeclaredExternal: () => bindDeclaredExternal,
3589
3592
  bodySitesOf: () => bodySitesOf,
3590
3593
  buildCanvasDataModel: () => buildCanvasDataModel,
3591
3594
  buildProjectGraph: () => buildProjectGraph,
@@ -3597,6 +3600,7 @@ __export(src_exports, {
3597
3600
  canonicalTypeRef: () => canonicalTypeRef,
3598
3601
  canonicalTypeText: () => canonicalTypeText,
3599
3602
  captureApprovedSpecs: () => captureApprovedSpecs,
3603
+ carriedFactChanges: () => carriedFactChanges,
3600
3604
  carryRetiredReachForms: () => carryRetiredReachForms,
3601
3605
  checkDeclarativePack: () => checkDeclarativePack,
3602
3606
  clearLoaderIssues: () => clearLoaderIssues,
@@ -3608,6 +3612,7 @@ __export(src_exports, {
3608
3612
  composeRuleSequence: () => composeRuleSequence,
3609
3613
  computeGateStateId: () => computeGateStateId,
3610
3614
  computeOwnDesignId: () => computeOwnDesignStateId,
3615
+ computeOwnDesignIdAsRecorded: () => computeOwnDesignStateIdAsRecorded,
3611
3616
  computeOwnStateId: () => computeOwnStateId,
3612
3617
  computeStateId: () => computeStateId,
3613
3618
  computeStateIdAt: () => computeStateIdAt,
@@ -3711,6 +3716,7 @@ __export(src_exports, {
3711
3716
  injectGuide: () => injectGuide,
3712
3717
  installPackFromDirectory: () => installPackFromDirectory,
3713
3718
  interfaceCanonicalTypes: () => interfaceCanonicalTypes,
3719
+ interfaceDesignView: () => interfaceDesignView,
3714
3720
  interfaceGenericParameters: () => interfaceGenericParameters,
3715
3721
  internalizeMember: () => internalizeMember,
3716
3722
  isDraftSubsystem: () => isDraftSubsystem,
@@ -3730,6 +3736,7 @@ __export(src_exports, {
3730
3736
  languageOfSourcePath: () => languageOfSourcePath,
3731
3737
  lineEndingFor: () => lineEndingFor,
3732
3738
  listDirectChainedSubprojects: () => listDirectChainedSubprojects,
3739
+ listExternalConsumers: () => listConsumers,
3733
3740
  listExternals: () => listExternals2,
3734
3741
  listFiles: () => listFiles,
3735
3742
  listFilesRecursive: () => listFilesRecursive,
@@ -3831,6 +3838,7 @@ __export(src_exports, {
3831
3838
  removeSnapshot: () => removeSnapshot2,
3832
3839
  renameAlias: () => renameAlias,
3833
3840
  renameComponent: () => renameComponent,
3841
+ renameField: () => renameField,
3834
3842
  renameId: () => renameProjectId,
3835
3843
  renameMethod: () => renameMethod,
3836
3844
  renameType: () => renameType,
@@ -3845,6 +3853,7 @@ __export(src_exports, {
3845
3853
  resolveChainingParent: () => resolveChainingParent,
3846
3854
  resolveDomains: () => resolveDomains2,
3847
3855
  resolveExpectedOutputPaths: () => resolveExpectedOutputPaths,
3856
+ resolveExternalCandidate: () => resolveCandidate,
3848
3857
  resolveExternals: () => resolveDeclared,
3849
3858
  resolveImport: () => resolveImport,
3850
3859
  resolveInstalledPack: () => resolveInstalledPack,
@@ -3873,6 +3882,7 @@ __export(src_exports, {
3873
3882
  setExecutionTier: () => setExecutionTier,
3874
3883
  setId: () => setProjectId,
3875
3884
  setLogLevel: () => setLogLevel,
3885
+ setNetwork: () => setNetwork,
3876
3886
  setProjectRoot: () => setProjectRoot,
3877
3887
  setProjectType: () => setProjectType,
3878
3888
  settledSpecPaths: () => settledSpecPaths,
@@ -3886,6 +3896,7 @@ __export(src_exports, {
3886
3896
  stepGraph: () => stepGraph,
3887
3897
  storedMethodSignature: () => storedMethodSignature,
3888
3898
  storedTypeMethod: () => storedTypeMethod,
3899
+ subsystemDesignView: () => subsystemDesignView,
3889
3900
  syncContextFiles: () => syncContextFiles,
3890
3901
  technologyName: () => technologyName,
3891
3902
  technologyTokens: () => technologyTokens,
@@ -3899,6 +3910,7 @@ __export(src_exports, {
3899
3910
  typeDialectFor: () => typeDialectFor,
3900
3911
  typeGenericParameters: () => typeGenericParameters,
3901
3912
  typeIsMany: () => typeIsMany,
3913
+ typeMappingFor: () => typeMappingFor,
3902
3914
  typeMatchesRef: () => typeMatchesRef,
3903
3915
  typeNamedRefs: () => typeNamedRefs,
3904
3916
  typeProblemEnumProposal: () => typeProblemEnumProposal,
@@ -4273,7 +4285,7 @@ function defaultTargetConfig(type) {
4273
4285
  enabled: true
4274
4286
  };
4275
4287
  }
4276
- var WAIRON_VERSION = "5.1.1-dev.106";
4288
+ var WAIRON_VERSION = "5.1.1-dev.107";
4277
4289
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
4278
4290
  var ARCHITECT_AGENT_ID = "agent-architect";
4279
4291
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -5390,12 +5402,12 @@ function declaredPackNames(config) {
5390
5402
  function declaredProfileIds(config) {
5391
5403
  return [...new Set(config.profileSelection?.profileIds ?? [])];
5392
5404
  }
5393
- function withPack(config, entry, remove2 = false) {
5405
+ function withPack(config, entry, remove3 = false) {
5394
5406
  const packs = config.extensions?.packs ?? [];
5395
5407
  const same = (e) => typeof entry === "string" ? e === entry : typeof e !== "string" && e.name === entry.name;
5396
5408
  const others = packs.filter((e) => !same(e));
5397
5409
  let next;
5398
- if (remove2) next = others;
5410
+ if (remove3) next = others;
5399
5411
  else if (typeof entry === "string") next = packs.includes(entry) ? packs : [...packs, entry];
5400
5412
  else next = [...others, entry];
5401
5413
  return { ...config, extensions: { useGlobalPacks: false, ...config.extensions, packs: next } };
@@ -6045,8 +6057,8 @@ var DocumentReconciler = class {
6045
6057
  }
6046
6058
  mapping(map, target, at, replace2) {
6047
6059
  const written = (pair) => String(YAML.isScalar(pair.key) ? pair.key.value : pair.key);
6048
- const renamed = this.renamedKeys(map, target, written);
6049
- const keyOf = (pair) => renamed.get(pair) ?? written(pair);
6060
+ const renamed2 = this.renamedKeys(map, target, written);
6061
+ const keyOf = (pair) => renamed2.get(pair) ?? written(pair);
6050
6062
  const kept = map.items.filter((pair) => target[keyOf(pair)] !== void 0);
6051
6063
  if (kept.length === 0) {
6052
6064
  replace2(target);
@@ -6056,7 +6068,7 @@ var DocumentReconciler = class {
6056
6068
  const key2 = keyOf(pair);
6057
6069
  const span = this.pairSpan(pair);
6058
6070
  const keyRange = pair.key.range;
6059
- if (renamed.has(pair) && keyRange) this.splices.push({ start: keyRange[0], end: keyRange[1], text: key2, rank: 0 });
6071
+ if (renamed2.has(pair) && keyRange) this.splices.push({ start: keyRange[0], end: keyRange[1], text: key2, rank: 0 });
6060
6072
  if (target[key2] === void 0) {
6061
6073
  this.splices.push({ start: span.start, end: span.end, text: "", rank: 0 });
6062
6074
  continue;
@@ -6294,6 +6306,13 @@ function packsOf(config) {
6294
6306
  function withPacks(config, packs) {
6295
6307
  return { ...config, extensions: { ...config.extensions, packs, useGlobalPacks: effectiveUseGlobalPacks(config) } };
6296
6308
  }
6309
+ function networkReads(stored, value) {
6310
+ if (value === null || stored === void 0) return value === null && stored === void 0;
6311
+ return (stored.description ?? "") === (value.description ?? "");
6312
+ }
6313
+ function networkValue(network) {
6314
+ return network.description ? { description: network.description } : true;
6315
+ }
6297
6316
  function registryOver(store, root) {
6298
6317
  const current2 = () => {
6299
6318
  const config = store.read();
@@ -6499,6 +6518,18 @@ function registryOver(store, root) {
6499
6518
  store.write(partConfig(partOf, config));
6500
6519
  return true;
6501
6520
  },
6521
+ setNetwork(network) {
6522
+ const config = current2();
6523
+ if (config.partOf !== void 0) {
6524
+ throw new WaironError(
6525
+ `Refusing to declare a network at ${root}: it is a part of "${config.partOf.project}", and a part declares no network \u2014 the project that declares one encloses it. Declare it on that project.`
6526
+ );
6527
+ }
6528
+ if (networkReads(config.network, network)) return false;
6529
+ const { network: _dropped, ...rest } = config;
6530
+ save(network === null ? rest : { ...rest, network: networkValue(network) });
6531
+ return true;
6532
+ },
6502
6533
  removeMember(alias) {
6503
6534
  const config = current2();
6504
6535
  const members = config.members;
@@ -6738,6 +6769,9 @@ function projectConfigRepositoryOver(adapter, rootDir) {
6738
6769
  setPartOf(partOf) {
6739
6770
  return registry.setPartOf(partOf);
6740
6771
  },
6772
+ setNetwork(network) {
6773
+ return registry.setNetwork(network);
6774
+ },
6741
6775
  removeMember(alias) {
6742
6776
  return registry.removeMember(alias);
6743
6777
  },
@@ -6832,6 +6866,9 @@ var projectConfigRepository = {
6832
6866
  setPartOf(partOf) {
6833
6867
  return bound().setPartOf(partOf);
6834
6868
  },
6869
+ setNetwork(network) {
6870
+ return bound().setNetwork(network);
6871
+ },
6835
6872
  removeMember(alias) {
6836
6873
  return bound().removeMember(alias);
6837
6874
  },
@@ -7303,10 +7340,13 @@ var ComponentSpecSchema = import_zod5.z.object({
7303
7340
  /** Other L2 component ids this component collaborates with (facades / standalone blocks). */
7304
7341
  dependsOn: import_zod5.z.array(import_zod5.z.string()).default([]),
7305
7342
  /**
7306
- * Portal-only, and expected on every Portal: how callers reach its verbs (see
7307
- * TransportSchema). Replaces the retired portalType, which the loader reads
7308
- * compatibly for one release (HTTP_API as HTTP). Which process serves which
7309
- * Portal is implementation: there are no listener mounts.
7343
+ * On a Portal (and expected on every Portal): how callers reach its verbs
7344
+ * (see TransportSchema). Replaces the retired portalType, which the loader
7345
+ * reads compatibly for one release (HTTP_API as HTTP). Which process serves
7346
+ * which Portal is implementation: there are no listener mounts. On an
7347
+ * Adapter, optional: the transport it calls its target Portal over —
7348
+ * inferred from the target when absent, and ADAPTER_TRANSPORT_MISMATCH when
7349
+ * stated and different from the target's. On no other block.
7310
7350
  */
7311
7351
  transport: TransportSchema.optional(),
7312
7352
  /**
@@ -7971,7 +8011,13 @@ var TypeFieldSchema = import_zod5.z.object({
7971
8011
  * For foreign keys, the referenced type/table ID (e.g. "billing.Invoice")
7972
8012
  * and optionally field (e.g. "billing.Invoice.id").
7973
8013
  */
7974
- references: import_zod5.z.string().optional()
8014
+ references: import_zod5.z.string().optional(),
8015
+ /**
8016
+ * The field's rename trace: every name it held before, oldest first.
8017
+ * Written only by the rename-field tool, never rewritten; the design export
8018
+ * shows it as `formerly`, so a consumer holding the old name can follow it.
8019
+ */
8020
+ previousNames: PreviousNamesSchema.optional()
7975
8021
  });
7976
8022
  var TypeMethodSchema = import_zod5.z.object({
7977
8023
  name: import_zod5.z.string(),
@@ -8121,14 +8167,17 @@ function typeSourceFiles(type) {
8121
8167
  var DESIGN_VIEW_FIELDS = {
8122
8168
  implementation: {
8123
8169
  linkage: ["sourcePath", "simPath", "router", "injectedParams", "conformance", "createdAt", "updatedAt"],
8124
- design: ["id", "name", "description", "contract", "technologies", "methods", "detail", "previousIds", "lint", "ext", "status"]
8170
+ readiness: ["status"],
8171
+ design: ["id", "name", "description", "contract", "technologies", "methods", "detail", "previousIds", "lint", "ext"]
8125
8172
  },
8126
8173
  implementationMethod: {
8127
8174
  linkage: ["sourcePath", "symbol", "exportedVia", "conformance"],
8175
+ readiness: [],
8128
8176
  design: ["name", "narrative", "detail", "intent", "calls", "ext"]
8129
8177
  },
8130
8178
  type: {
8131
8179
  linkage: ["sourcePath", "symbol", "createdAt", "updatedAt"],
8180
+ readiness: [],
8132
8181
  design: [
8133
8182
  "kind",
8134
8183
  "id",
@@ -8154,10 +8203,12 @@ var DESIGN_VIEW_FIELDS = {
8154
8203
  },
8155
8204
  typeMethod: {
8156
8205
  linkage: ["sourcePath", "symbol"],
8206
+ readiness: [],
8157
8207
  design: ["name", "signature", "params", "returns", "description"]
8158
8208
  },
8159
8209
  component: {
8160
8210
  linkage: ["externalLinks", "createdAt", "updatedAt"],
8211
+ readiness: ["status"],
8161
8212
  design: [
8162
8213
  "id",
8163
8214
  "name",
@@ -8180,10 +8231,33 @@ var DESIGN_VIEW_FIELDS = {
8180
8231
  "variant",
8181
8232
  "previousIds",
8182
8233
  "lint",
8183
- "ext",
8184
- "status"
8234
+ "ext"
8235
+ ]
8236
+ },
8237
+ subsystem: {
8238
+ linkage: ["createdAt", "updatedAt"],
8239
+ readiness: ["status"],
8240
+ design: [
8241
+ "id",
8242
+ "name",
8243
+ "description",
8244
+ "parentSystem",
8245
+ "publicInterfaces",
8246
+ "lifecycle",
8247
+ "profile",
8248
+ "projectPath",
8249
+ "targetLanguage",
8250
+ "trustedLinks",
8251
+ "designDepth",
8252
+ "lint",
8253
+ "ext"
8185
8254
  ]
8186
8255
  },
8256
+ interface: {
8257
+ linkage: ["createdAt", "updatedAt"],
8258
+ readiness: ["status"],
8259
+ design: ["id", "name", "description", "component", "methods", "previousIds", "lint", "ext", "implements"]
8260
+ },
8187
8261
  /**
8188
8262
  * A retired listener mount as a stored file may still hold it until doctor
8189
8263
  * --fix migrates it: its `via` was linkage (the router entry, now the
@@ -8191,9 +8265,13 @@ var DESIGN_VIEW_FIELDS = {
8191
8265
  */
8192
8266
  retiredMount: {
8193
8267
  linkage: ["via"],
8268
+ readiness: [],
8194
8269
  design: ["portal", "prefixes"]
8195
8270
  }
8196
8271
  };
8272
+ function outOfApproval(kind) {
8273
+ return [...DESIGN_VIEW_FIELDS[kind].linkage, ...DESIGN_VIEW_FIELDS[kind].readiness];
8274
+ }
8197
8275
  function withoutFields(value, fields) {
8198
8276
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
8199
8277
  const out = {};
@@ -8206,20 +8284,26 @@ function eachOf(items, project3) {
8206
8284
  return Array.isArray(items) ? items.map(project3) : items;
8207
8285
  }
8208
8286
  function implementationDesignView(impl) {
8209
- const view3 = withoutFields(impl, DESIGN_VIEW_FIELDS.implementation.linkage);
8287
+ const view3 = withoutFields(impl, outOfApproval("implementation"));
8210
8288
  if ("methods" in view3) view3.methods = eachOf(view3.methods, (m) => withoutFields(m, DESIGN_VIEW_FIELDS.implementationMethod.linkage));
8211
8289
  return view3;
8212
8290
  }
8213
8291
  function typeDesignView(type) {
8214
- const view3 = withoutFields(type, DESIGN_VIEW_FIELDS.type.linkage);
8292
+ const view3 = withoutFields(type, outOfApproval("type"));
8215
8293
  if ("methods" in view3) view3.methods = eachOf(view3.methods, (m) => withoutFields(m, DESIGN_VIEW_FIELDS.typeMethod.linkage));
8216
8294
  return view3;
8217
8295
  }
8218
8296
  function componentDesignView(component) {
8219
- const view3 = withoutFields(component, DESIGN_VIEW_FIELDS.component.linkage);
8297
+ const view3 = withoutFields(component, outOfApproval("component"));
8220
8298
  if ("mounts" in view3) view3.mounts = eachOf(view3.mounts, (m) => withoutFields(m, DESIGN_VIEW_FIELDS.retiredMount.linkage));
8221
8299
  return view3;
8222
8300
  }
8301
+ function subsystemDesignView(subsystem) {
8302
+ return withoutFields(subsystem, outOfApproval("subsystem"));
8303
+ }
8304
+ function interfaceDesignView(contract) {
8305
+ return withoutFields(contract, outOfApproval("interface"));
8306
+ }
8223
8307
  function componentEntryFor(component, method) {
8224
8308
  if (component.componentType !== "Portal") return void 0;
8225
8309
  const own2 = component.invokedBy;
@@ -8421,6 +8505,14 @@ var SurfaceContractEntrySchema = import_zod5.z.object({
8421
8505
  /** The entry's export role, call (the default) or implement. */
8422
8506
  role: import_zod5.z.string().optional()
8423
8507
  });
8508
+ var SurfaceUnresolvedExportSchema = import_zod5.z.object({
8509
+ /** The public name the entry declares. */
8510
+ id: import_zod5.z.string(),
8511
+ /** Where it re-exports from: the `from` the entry names. */
8512
+ source: import_zod5.z.string(),
8513
+ /** The name the source's rename trace carries the item to, when it records one. */
8514
+ renamedTo: import_zod5.z.string().optional()
8515
+ });
8424
8516
  var SurfaceSnapshotSchema = import_zod5.z.object({
8425
8517
  /** Producing project/system name — the snapshot's resolution identity. */
8426
8518
  projectName: import_zod5.z.string(),
@@ -8448,7 +8540,14 @@ var SurfaceSnapshotSchema = import_zod5.z.object({
8448
8540
  * language judges a native-ABI library call against. Absent when the
8449
8541
  * producer declares none, or on snapshots written before it was carried.
8450
8542
  */
8451
- targetLanguage: import_zod5.z.string().optional()
8543
+ targetLanguage: import_zod5.z.string().optional(),
8544
+ /**
8545
+ * The public names the producer's L0 declares that no longer resolve (a
8546
+ * named re-export whose source dropped or renamed the item), with the
8547
+ * source's rename trace when it has one. Provenance, outside every digest;
8548
+ * absent when every entry resolves.
8549
+ */
8550
+ unresolvedExports: import_zod5.z.array(SurfaceUnresolvedExportSchema).optional()
8452
8551
  });
8453
8552
  var ExternalLockEntrySchema = import_zod5.z.object({
8454
8553
  project: import_zod5.z.string(),
@@ -8483,7 +8582,7 @@ var ParentExcerptSchema = import_zod5.z.object({
8483
8582
  function relationHealth(status) {
8484
8583
  if (status.uses.some((u) => u.state === "changed" || u.state === "removed" || u.state === "renamed")) return "incompatible";
8485
8584
  if (!status.reachable || status.outOfReach || status.uses.some((u) => u.state === "unavailable" || u.state === "unlocked")) return "unavailable";
8486
- if (status.drifted) return "drifted";
8585
+ if (status.drifted || (status.staleFacts?.length ?? 0) > 0) return "drifted";
8487
8586
  return "ok";
8488
8587
  }
8489
8588
  var NamedOpenApiSpecSchema = import_zod5.z.object({
@@ -8524,7 +8623,8 @@ function sortKeys(v) {
8524
8623
 
8525
8624
  // src/core/statehash.ts
8526
8625
  var CONTENT_ALGORITHM = "sha256";
8527
- var DESIGN_ALGORITHM = "sha256-design";
8626
+ var DESIGN_ALGORITHM = "sha256-design-2";
8627
+ var EARLIER_DESIGN_ALGORITHM = "sha256-design";
8528
8628
  function inIdentityOrder(specs) {
8529
8629
  return specs.map((spec) => ({ spec, content: canonicalize(spec) })).sort((a, b) => compareOrdinal(a.spec.id, b.spec.id) || compareOrdinal(a.content, b.content)).map((entry) => entry.spec);
8530
8630
  }
@@ -8533,9 +8633,9 @@ function digestTree(keep, reading = {}) {
8533
8633
  const same = (spec) => spec;
8534
8634
  const tree = {
8535
8635
  system: loadSystemSpec(),
8536
- subsystems: kind(loadSubsystemSpecs()),
8636
+ subsystems: kind(loadSubsystemSpecs().map(reading.subsystem ?? same)),
8537
8637
  components: kind(loadComponentSpecs().map(reading.component ?? same)),
8538
- interfaces: kind(loadInterfaceSpecs()),
8638
+ interfaces: kind(loadInterfaceSpecs().map(reading.interface ?? same)),
8539
8639
  implementations: kind(loadImplementationSpecs().map(reading.implementation ?? same)),
8540
8640
  types: kind(loadTypeSpecs().map(reading.type ?? same))
8541
8641
  };
@@ -8567,9 +8667,31 @@ function computeOwnDesignStateId() {
8567
8667
  implementation: implementationDesignView,
8568
8668
  component: componentDesignView,
8569
8669
  type: typeDesignView,
8670
+ subsystem: subsystemDesignView,
8671
+ interface: interfaceDesignView,
8570
8672
  algorithm: DESIGN_ALGORITHM
8571
8673
  });
8572
8674
  }
8675
+ function approvedStatusKey(kind, id) {
8676
+ return `${kind}:${id}`;
8677
+ }
8678
+ function computeOwnDesignStateIdAsRecorded(approvedStatus) {
8679
+ const withStatus = (kind, view3) => (spec) => {
8680
+ const status = spec.status;
8681
+ if (status === void 0) return view3(spec);
8682
+ const id = spec.id;
8683
+ const approved = typeof id === "string" ? approvedStatus.get(approvedStatusKey(kind, id)) : void 0;
8684
+ return { ...view3(spec), status: approved ?? status };
8685
+ };
8686
+ return digestTree(ownedByRoot(), {
8687
+ implementation: withStatus("implementation", implementationDesignView),
8688
+ component: withStatus("component", componentDesignView),
8689
+ type: withStatus("type", typeDesignView),
8690
+ subsystem: withStatus("subsystem", subsystemDesignView),
8691
+ interface: withStatus("interface", interfaceDesignView),
8692
+ algorithm: EARLIER_DESIGN_ALGORITHM
8693
+ });
8694
+ }
8573
8695
  function isInside(dir, file) {
8574
8696
  const rel2 = path7.relative(dir, file);
8575
8697
  return rel2 !== "" && !rel2.startsWith("..") && !path7.isAbsolute(rel2);
@@ -9815,6 +9937,54 @@ var DIALECTS = /* @__PURE__ */ new Map([
9815
9937
  function typeDialectFor(language) {
9816
9938
  return DIALECTS.get(language.toLowerCase()) ?? null;
9817
9939
  }
9940
+ var MAPPING_ONLY = (language) => `(mapping only: wairon's code analyzer does not read ${language} annotations yet, so code conformance does not compare these types \u2014 follow the table by hand)`;
9941
+ var MAPPING_ONLY_TABLES = /* @__PURE__ */ new Map([
9942
+ ["rust", [
9943
+ "string \u2192 String (&str for a borrowed parameter)",
9944
+ "int \u2192 i64 (a narrower iN/uN where the contract says so in its description)",
9945
+ "float \u2192 f64",
9946
+ "bool \u2192 bool",
9947
+ "bytes \u2192 Vec<u8> (&[u8] for a borrowed parameter)",
9948
+ "date \u2192 chrono::NaiveDate, datetime \u2192 chrono::DateTime<Utc>, duration \u2192 std::time::Duration",
9949
+ "any \u2192 serde_json::Value",
9950
+ "list<T> \u2192 Vec<T>",
9951
+ "set<T> \u2192 HashSet<T> (BTreeSet<T> where order matters)",
9952
+ "map<K, V> \u2192 HashMap<K, V> (BTreeMap<K, V> where order matters)",
9953
+ "T? \u2192 Option<T>",
9954
+ "A | B \u2192 an enum with one variant per named type",
9955
+ "async T \u2192 async fn returning T",
9956
+ "result<T, E> \u2192 Result<T, E> (result<void, E> \u2192 Result<(), E>)",
9957
+ "an enum E \u2192 enum E { A, B } (unit variants)",
9958
+ "a named scalar E holding P \u2192 a newtype struct E(P)",
9959
+ "a contract (interface) \u2192 a trait; a type method \u2192 an impl method on the struct"
9960
+ ]],
9961
+ ["python", [
9962
+ "string \u2192 str",
9963
+ "int \u2192 int",
9964
+ "float \u2192 float",
9965
+ "bool \u2192 bool",
9966
+ "bytes \u2192 bytes",
9967
+ "date \u2192 datetime.date, datetime \u2192 datetime.datetime, duration \u2192 datetime.timedelta",
9968
+ "any \u2192 typing.Any",
9969
+ "list<T> \u2192 list[T]",
9970
+ "set<T> \u2192 set[T]",
9971
+ "map<K, V> \u2192 dict[K, V]",
9972
+ "T? \u2192 T | None (Optional[T])",
9973
+ "A | B \u2192 A | B",
9974
+ "async T \u2192 async def returning T",
9975
+ "result<T, E> \u2192 T, raising E on failure (result<void, E> \u2192 None)",
9976
+ "an enum E \u2192 class E(enum.Enum)",
9977
+ 'a named scalar E holding P \u2192 E = NewType("E", P)',
9978
+ "a contract (interface) \u2192 a typing.Protocol; an entity or value-object \u2192 a @dataclass"
9979
+ ]]
9980
+ ]);
9981
+ function typeMappingFor(language) {
9982
+ const dialect = typeDialectFor(language);
9983
+ if (dialect) return dialect.mappingLines();
9984
+ const key2 = language.toLowerCase();
9985
+ const table = MAPPING_ONLY_TABLES.get(key2);
9986
+ return table ? [...table, MAPPING_ONLY(language)] : null;
9987
+ }
9818
9988
 
9819
9989
  // src/models/step-graph.ts
9820
9990
  function stepGraph(method) {
@@ -10285,6 +10455,45 @@ function contentDigest(snapshot) {
10285
10455
  };
10286
10456
  return sha256(canonicalize(contract));
10287
10457
  }
10458
+ function factValue(value) {
10459
+ return value === void 0 || value === null ? "" : canonicalize(value);
10460
+ }
10461
+ function carriedFactChanges(pinned, live) {
10462
+ const changes = [];
10463
+ const differs = (label7, a, b) => {
10464
+ if (factValue(a) !== factValue(b)) changes.push(label7);
10465
+ };
10466
+ differs("the producer's id", pinned.projectId, live.projectId);
10467
+ differs("targetLanguage", pinned.targetLanguage, live.targetLanguage);
10468
+ const liveEntries = new Map(live.interfaces.map((e) => [e.id, e]));
10469
+ for (const entry of pinned.interfaces) {
10470
+ const now = liveEntries.get(entry.id);
10471
+ if (!now) continue;
10472
+ differs(`export kind of ${entry.id}`, entry.type, now.type);
10473
+ differs(`transport of ${entry.id}`, entry.transport, now.transport);
10474
+ differs(`abi of ${entry.id}`, entry.abi, now.abi);
10475
+ differs(`role of ${entry.id}`, entry.role, now.role);
10476
+ differs(`stereotype of ${entry.id}`, entry.componentType, now.componentType);
10477
+ differs(`basePath of ${entry.id}`, entry.basePath, now.basePath);
10478
+ differs(`auth of ${entry.id}`, entry.auth?.scheme, now.auth?.scheme);
10479
+ differs(`rename trace of ${entry.id}`, entry.formerly, now.formerly);
10480
+ const liveMethods = new Map(now.methods.map((m) => [m.name, m]));
10481
+ for (const method of entry.methods) {
10482
+ const nowMethod = liveMethods.get(method.name);
10483
+ if (!nowMethod) continue;
10484
+ differs(`effect of ${entry.id}.${method.name}`, method.effect, nowMethod.effect);
10485
+ differs(`rename trace of ${entry.id}.${method.name}`, method.formerly, nowMethod.formerly);
10486
+ }
10487
+ }
10488
+ const liveTypes = new Map(live.types.map((t) => [t.id, t]));
10489
+ for (const def of pinned.types) {
10490
+ const now = liveTypes.get(def.id);
10491
+ if (now) differs(`rename trace of ${def.id}`, def.formerly, now.formerly);
10492
+ }
10493
+ const unresolved = (snapshot) => [...snapshot.unresolvedExports ?? []].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
10494
+ differs("the unresolved re-exports", unresolved(pinned), unresolved(live));
10495
+ return changes;
10496
+ }
10288
10497
  function memberDigest(snapshot, publicName, member) {
10289
10498
  if (member === "type") {
10290
10499
  const exported = snapshot.exportedTypes?.find((t) => t.id === publicName);
@@ -10318,6 +10527,38 @@ function exportTargetKey(e) {
10318
10527
 
10319
10528
  // src/models/project-family.ts
10320
10529
  var path11 = __toESM(require("path"));
10530
+ function directoryKey(dir) {
10531
+ const resolved = path11.resolve(dir);
10532
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
10533
+ }
10534
+ function relationOf(consumer, producer) {
10535
+ if (consumer.parent === producer.namespace) return "parent";
10536
+ if (producer.parent === consumer.namespace) return "member";
10537
+ if (consumer.parent !== void 0 && consumer.parent === producer.parent) return "sibling";
10538
+ return "family";
10539
+ }
10540
+ function familyExternal(alias, project3, consumer, producer) {
10541
+ return { alias, project: project3, sourceKind: "family", relation: relationOf(consumer, producer), producer: producer.namespace, directory: producer.directory, audience: "project", role: "external" };
10542
+ }
10543
+ function bindDeclaredExternal(declared, consumer, nodes) {
10544
+ const unresolved = (problem) => ({ alias: declared.alias, project: declared.project, sourceKind: "unresolved", audience: "instance", problem, role: "external" });
10545
+ if (declared.problem) return unresolved(declared.problem);
10546
+ if (declared.sourceHosted !== void 0) return { alias: declared.alias, project: declared.project, sourceKind: "hosted", hosted: declared.sourceHosted, audience: "instance", role: "external" };
10547
+ if (declared.sourceGit !== void 0) return { alias: declared.alias, project: declared.project, sourceKind: "git", audience: "instance", role: "external" };
10548
+ if (declared.sourcePath !== void 0) {
10549
+ const directory = path11.resolve(consumer.directory, declared.sourcePath);
10550
+ const found = nodes.find((n) => directoryKey(n.directory) === directoryKey(directory));
10551
+ if (!found) return { alias: declared.alias, project: declared.project, sourceKind: "path", directory, audience: "instance", role: "external" };
10552
+ if (found.id !== declared.project) {
10553
+ return unresolved(`the family project at source.path "${declared.sourcePath}" answers to ${found.id === void 0 ? "no id" : `"${found.id}"`}, not "${declared.project}"`);
10554
+ }
10555
+ return familyExternal(declared.alias, declared.project, consumer, found);
10556
+ }
10557
+ const holders = nodes.filter((n) => n.id === declared.project && n !== consumer);
10558
+ if (holders.length === 0) return unresolved(`no project of the family answers to "${declared.project}", and no source.path says where to find it`);
10559
+ if (holders.length > 1) return unresolved(`${holders.length} projects of the family answer to "${declared.project}" (${holders.map((n) => `"${n.namespace || "(the bound root)"}"`).join(", ")}), so the declaration could mean either`);
10560
+ return familyExternal(declared.alias, declared.project, consumer, holders[0]);
10561
+ }
10321
10562
  function walkMemberPath(byKey3, start, segments) {
10322
10563
  let node = start;
10323
10564
  for (const segment of segments.slice(0, -1)) {
@@ -10541,7 +10782,9 @@ var DesignFieldSchema = import_zod9.z.object({
10541
10782
  optional: import_zod9.z.boolean(),
10542
10783
  description: import_zod9.z.string().optional(),
10543
10784
  key: import_zod9.z.string().optional(),
10544
- references: import_zod9.z.string().optional()
10785
+ references: import_zod9.z.string().optional(),
10786
+ /** The field's rename trace, oldest first — present only when it was renamed. */
10787
+ formerly: import_zod9.z.array(import_zod9.z.string()).optional()
10545
10788
  }).passthrough();
10546
10789
  var DeclaredInvocationSchema2 = DeclaredInvocationSchema.passthrough();
10547
10790
  var DesignMethodSchema = import_zod9.z.object({
@@ -16902,7 +17145,10 @@ function typeSection(t, keys) {
16902
17145
  optional: f.optional === true,
16903
17146
  ...f.description !== void 0 ? { description: f.description } : {},
16904
17147
  ...f.key !== void 0 ? { key: f.key } : {},
16905
- ...f.references !== void 0 ? { references: keys.typeFieldRef(f.references, owner) } : {}
17148
+ ...f.references !== void 0 ? { references: keys.typeFieldRef(f.references, owner) } : {},
17149
+ // The field's rename trace, only when it was renamed: an export of a
17150
+ // never-renamed field reads exactly as before.
17151
+ ...f.previousNames && f.previousNames.length > 0 ? { formerly: [...f.previousNames] } : {}
16906
17152
  })),
16907
17153
  methods: t.methods.map((m) => ({
16908
17154
  key: `${key2}.${m.name}`,
@@ -17161,6 +17407,7 @@ function projectOwnSurface(maxAudience) {
17161
17407
  };
17162
17408
  const projectId = boundProjectId();
17163
17409
  const targetLanguage = system.targetLanguage?.trim().toLowerCase();
17410
+ const unresolvedExports = table.problems.filter((p) => p.kind === "invalid" && p.publicName !== void 0 && p.source !== void 0 && !table.entries.some((e) => e.publicName === p.publicName)).map((p) => ({ id: p.publicName, source: p.source, ...p.renamedTo ? { renamedTo: p.renamedTo } : {} }));
17164
17411
  return canonicalReferences(SurfaceSnapshotSchema.parse({
17165
17412
  projectName: system.name,
17166
17413
  ...projectId ? { projectId } : {},
@@ -17170,7 +17417,8 @@ function projectOwnSurface(maxAudience) {
17170
17417
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
17171
17418
  interfaces: entries,
17172
17419
  types: closure,
17173
- ...exportedTypes.length ? { exportedTypes: exportedTypes.map((e) => ({ id: e.publicName, type: closureIdOf(e), audience: e.audience ?? "instance" })) } : {}
17420
+ ...exportedTypes.length ? { exportedTypes: exportedTypes.map((e) => ({ id: e.publicName, type: closureIdOf(e), audience: e.audience ?? "instance" })) } : {},
17421
+ ...unresolvedExports.length ? { unresolvedExports } : {}
17174
17422
  }));
17175
17423
  }
17176
17424
  function canonicalReferences(snapshot) {
@@ -17480,7 +17728,7 @@ function pinBinding(binding, lock) {
17480
17728
  }
17481
17729
  const digest3 = contentDigest(snapshot);
17482
17730
  const pinned = externalsRepository.readSnapshot(external.alias);
17483
- const snapshotChanged = !pinned || contentDigest(pinned) !== digest3;
17731
+ const snapshotChanged = !pinned || contentDigest(pinned) !== digest3 || pinnedContentKey(pinned) !== pinnedContentKey(snapshot);
17484
17732
  if (snapshotChanged) externalsRepository.saveSnapshot(external.alias, snapshot);
17485
17733
  const { used, missing } = usedDigests(binding.usage, snapshot);
17486
17734
  const entry = {
@@ -17623,7 +17871,15 @@ function sameSignatureAfterRename(live, from, to, digest3) {
17623
17871
  function goneUse(live, publicName, member, digest3) {
17624
17872
  const to = renamedIn(live, publicName, member);
17625
17873
  const at = { publicName, ...member !== void 0 ? { member } : {} };
17626
- if (!to) return { ...at, state: "removed" };
17874
+ if (!to) {
17875
+ const broken = (live.unresolvedExports ?? []).find((u) => u.id === publicName);
17876
+ if (!broken) return { ...at, state: "removed" };
17877
+ return {
17878
+ ...at,
17879
+ state: "removed",
17880
+ detail: `the producer re-exports it from "${broken.source}", which ${broken.renamedTo ? `renamed it to "${broken.renamedTo}"` : "no longer exports it"}, so the producer's re-export no longer resolves \u2014 ${broken.renamedTo ? `the producer must re-export "${broken.renamedTo}", or depend on "${broken.source}" directly` : "the producer must fix its re-export"}`
17881
+ };
17882
+ }
17627
17883
  const renamedTo = renamedName(to);
17628
17884
  const signature = member === void 0 || digest3 === void 0 ? "" : sameSignatureAfterRename(live, { publicName, member }, to, digest3) ? " (its signature is unchanged)" : " (and its signature changed)";
17629
17885
  return { ...at, state: "renamed", renamedTo, detail: `renamed to "${renamedTo}"${signature} \u2014 follow the rename` };
@@ -17642,13 +17898,19 @@ function compareUses(entry, usage, live) {
17642
17898
  uses.push(now === null ? goneUse(live, publicName, member, digest3) : { publicName, member, state: now === digest3 ? "unchanged" : "changed" });
17643
17899
  }
17644
17900
  }
17901
+ const unlocked = (publicName, member) => {
17902
+ const present = member === void 0 ? liveHas(publicName) : memberDigest(live, publicName, member) !== null;
17903
+ if (!present) return goneUse(live, publicName, member, void 0);
17904
+ return { publicName, ...member !== void 0 ? { member } : {}, state: "unlocked", detail: entry === void 0 ? "used, and present in the live producer, but never pinned \u2014 pin it (`wairon externals pin`) to record what it is judged against" : "used now, but not in the lock \u2014 re-pin" };
17905
+ };
17645
17906
  for (const use of usage?.used ?? []) {
17646
17907
  if (!(use.publicName in locked)) {
17647
- uses.push({ publicName: use.publicName, state: "unlocked", detail: "used now, but not in the lock \u2014 re-pin" });
17908
+ const gone = use.members.map((m) => unlocked(use.publicName, m)).filter((u) => u.state !== "unlocked");
17909
+ uses.push(...gone.length ? gone : [unlocked(use.publicName)]);
17648
17910
  continue;
17649
17911
  }
17650
17912
  for (const member of use.members) {
17651
- if (!(member in locked[use.publicName])) uses.push({ publicName: use.publicName, member, state: "unlocked", detail: "used now, but not in the lock \u2014 re-pin" });
17913
+ if (!(member in locked[use.publicName])) uses.push(unlocked(use.publicName, member));
17652
17914
  }
17653
17915
  }
17654
17916
  for (const ref of usage?.unexported ?? []) {
@@ -17700,11 +17962,14 @@ function externalStatus(binding, lock) {
17700
17962
  }
17701
17963
  const drifted = entry !== void 0 ? contentDigest(live) !== entry.digest : void 0;
17702
17964
  const uses = compareUses(entry, binding.usage, live);
17965
+ const snapshot = entry !== void 0 ? externalsRepository.readSnapshot(external.alias) : null;
17966
+ const staleFacts = snapshot ? carriedFactChanges(snapshot, live) : [];
17703
17967
  return {
17704
17968
  ...base,
17705
17969
  reachable: true,
17706
17970
  stale: uses.some((u) => u.state === "changed" || u.state === "removed" || u.state === "renamed"),
17707
17971
  ...drifted !== void 0 ? { drifted } : {},
17972
+ ...staleFacts.length ? { staleFacts } : {},
17708
17973
  uses
17709
17974
  };
17710
17975
  }
@@ -17722,7 +17987,7 @@ function getExternalsStatus(offline) {
17722
17987
  function listExternals() {
17723
17988
  const bindings = resolveDeclared();
17724
17989
  const lock = externalsRepository.readLock();
17725
- return bindings.map(({ external }) => {
17990
+ const declared = bindings.map(({ external }) => {
17726
17991
  const entry = lock?.externals[external.alias];
17727
17992
  return {
17728
17993
  alias: external.alias,
@@ -17735,6 +18000,20 @@ function listExternals() {
17735
18000
  ...external.problem ? { problem: external.problem } : {}
17736
18001
  };
17737
18002
  });
18003
+ const aliases = new Set(bindings.map((b) => b.external.alias));
18004
+ const orphans = Object.entries(lock?.externals ?? {}).filter(([alias]) => !aliases.has(alias)).map(([alias, entry]) => ({
18005
+ alias,
18006
+ project: entry.project,
18007
+ sourceKind: "unresolved",
18008
+ audience: "instance",
18009
+ lock: entry,
18010
+ problem: `pinned but no longer declared \u2014 \`wairon externals remove ${alias}\` removes the pin`
18011
+ }));
18012
+ return [...declared, ...orphans];
18013
+ }
18014
+ function pinnedContentKey(snapshot) {
18015
+ const { stateId: _s, generatedAt: _g, origin: _o, ...content } = SurfaceSnapshotSchema.parse(JSON.parse(JSON.stringify(snapshot)));
18016
+ return canonicalize({ ...content, interfaces: content.interfaces.map((e) => ({ ...e, component: lastSegment2(e.component) })) });
17738
18017
  }
17739
18018
  function surfaceContentKey(snapshot) {
17740
18019
  const { stateId, generatedAt, origin, ...content } = snapshot;
@@ -17748,7 +18027,9 @@ function audienceRank2(audience) {
17748
18027
  const idx = levels.indexOf(audience ?? "instance");
17749
18028
  return idx === -1 ? levels.indexOf("instance") : idx;
17750
18029
  }
18030
+ var AUDIENCE_RANKING = SURFACE_AUDIENCES.join(" < ");
17751
18031
  var LOCATION_FORMS = "`../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>`";
18032
+ var NOT_A_LOCATION = /[\s{}[\]"'`<>|*?]/;
17752
18033
  function refused(alias, why2) {
17753
18034
  return { alias, written: false, declaration: null, refusal: `The external "${alias}" was not declared: ${why2}.` };
17754
18035
  }
@@ -17756,11 +18037,14 @@ function requestProblem(request, config, source, sourceProblem) {
17756
18037
  const { alias } = request;
17757
18038
  if (!EXTERNAL_ALIAS_RE.test(alias)) return `an alias is [a-z0-9-_]+, and "${alias}" is not \u2014 choose one specs can write as \`${alias.toLowerCase().replace(/[^a-z0-9_-]/g, "-")}::name\``;
17758
18039
  if (config?.members?.[alias] !== void 0) return `"${alias}" is already declared under \`members\` \u2014 one alias names one project, so choose another alias`;
17759
- if (config?.externals?.[alias] !== void 0) return `"${alias}" is already declared under \`externals\` \u2014 choose another alias, or edit that declaration`;
18040
+ if (config?.externals?.[alias] !== void 0) return `"${alias}" is already declared under \`externals\` \u2014 choose another alias, change its imports with \`wairon externals use ${alias} --add <names>\` (sdd_update_external), or remove it first with \`wairon externals remove ${alias}\``;
18041
+ if (request.source !== void 0 && NOT_A_LOCATION.test(request.source.trim())) {
18042
+ return `the source "${request.source}" is not a location (accepted: ${LOCATION_FORMS})`;
18043
+ }
17760
18044
  if (sourceProblem) return `${sourceProblem.replace(/^its /, "the ")} (accepted: ${LOCATION_FORMS})`;
17761
18045
  const git2 = source?.git !== void 0;
17762
18046
  if (!git2 && (request.ref !== void 0 || request.dir !== void 0)) {
17763
- return `\`ref\` and \`dir\` apply to a git source only \u2014 write the source as ${"`<git url>`"} or ${"`<git url>#<commit>`"}`;
18047
+ return "`ref` and `dir` apply to a git source only \u2014 write the source as `<git url>` or `<git url>#<commit>`";
17764
18048
  }
17765
18049
  if (git2 && request.ref !== void 0 && source?.ref !== void 0 && request.ref !== source.ref) {
17766
18050
  return `the source fixes the ref at "${source.ref}" (its \`#<commit>\`), but \`ref\` says "${request.ref}" \u2014 give one of them`;
@@ -17807,13 +18091,28 @@ function readProducer(binding) {
17807
18091
  const config = loadProjectConfig();
17808
18092
  if (!config) return { why: `${directory} holds no wairon project` };
17809
18093
  const floor = audienceRank2(external.audience);
17810
- const exported = resolveProjectExports().entries.filter((e) => audienceRank2(e.audience ?? "instance") >= floor).map((e) => e.publicName);
17811
- return { read: { id: effectiveProjectId(config), exported: [...new Set(exported)] } };
18094
+ const entries = resolveProjectExports().entries;
18095
+ const exported = entries.filter((e) => audienceRank2(e.audience ?? "instance") >= floor).map((e) => e.publicName);
18096
+ const audiences = /* @__PURE__ */ new Map();
18097
+ for (const e of entries) {
18098
+ const audience = e.audience ?? "instance";
18099
+ const held = audiences.get(e.publicName);
18100
+ if (held === void 0 || audienceRank2(audience) > audienceRank2(held)) audiences.set(e.publicName, audience);
18101
+ }
18102
+ return { read: { id: effectiveProjectId(config), exported: [...new Set(exported)], audiences, audience: external.audience } };
17812
18103
  });
17813
18104
  } catch (e) {
17814
18105
  return { why: e instanceof Error ? e.message : String(e) };
17815
18106
  }
17816
18107
  }
18108
+ function unexportedReason(name, read2) {
18109
+ const narrower = read2.audiences.get(name);
18110
+ if (narrower !== void 0) {
18111
+ const who = read2.audience === "project" ? "a project of its family" : "a path, git or hosted consumer outside its family";
18112
+ return `the producer exports "${name}" to \`${narrower}\` only, and this project is outside it \u2014 it reads the producer at \`${read2.audience}\` (${who}); audiences, narrowest first: ${AUDIENCE_RANKING} \u2014 widen the export's audience in the producer's L0, or consume it from inside its family`;
18113
+ }
18114
+ return `the producer does not export "${name}" to this project${read2.exported.length ? ` (closest: ${closest(name, read2.exported).map((n) => `"${n}"`).join(", ")})` : " (it exports nothing to this project)"}`;
18115
+ }
17817
18116
  function contradiction(request, binding, read2) {
17818
18117
  const declared = request.project ?? request.alias;
17819
18118
  const problem = binding?.external.problem;
@@ -17825,10 +18124,7 @@ function contradiction(request, binding, read2) {
17825
18124
  return `the producer there answers to "${read2.id}", not "${declared}" \u2014 declare \`project: ${read2.id}\`, or use "${read2.id}" as the alias`;
17826
18125
  }
17827
18126
  const missing = (request.use ?? []).filter((u) => u !== "*" && !read2.exported.includes(u));
17828
- if (missing.length > 0) {
17829
- return missing.map((u) => `the producer does not export "${u}" to this project${read2.exported.length ? ` (closest: ${closest(u, read2.exported).map((n) => `"${n}"`).join(", ")})` : " (it exports nothing to this project)"}`).join("; ");
17830
- }
17831
- return null;
18127
+ return missing.length > 0 ? missing.map((u) => unexportedReason(u, read2)).join("; ") : null;
17832
18128
  }
17833
18129
  function declare(request) {
17834
18130
  const { alias } = request;
@@ -17838,23 +18134,35 @@ function declare(request) {
17838
18134
  if (problem) return refused(alias, problem);
17839
18135
  const declaration2 = declarationOf(request, read2.source);
17840
18136
  const declared = request.project ?? alias;
17841
- if (request.dryRun) return { alias, written: false, declaration: declaration2, project: declared };
18137
+ let binding;
18138
+ let bindProblem = "it could not be bound";
17842
18139
  try {
17843
- declareExternal(alias, declaration2);
18140
+ binding = resolveCandidate(alias, declaration2);
17844
18141
  } catch (e) {
17845
- return refused(alias, (e instanceof Error ? e.message : String(e)).replace(/\.$/, ""));
18142
+ bindProblem = e instanceof Error ? e.message : String(e);
17846
18143
  }
17847
- const binding = resolveDeclared().find((b) => b.external.alias === alias);
17848
- const answered = binding ? readProducer(binding) : { why: "it is not among the resolved externals" };
18144
+ const answered = binding ? readProducer(binding) : { why: bindProblem };
17849
18145
  const producer = "read" in answered ? answered.read : null;
17850
18146
  const contradicted = contradiction(request, binding, producer);
17851
- if (contradicted) {
17852
- removeExternal(alias);
17853
- return refused(alias, contradicted);
18147
+ if (contradicted) return refused(alias, contradicted);
18148
+ const why2 = "why" in answered ? answered.why : "unknown";
18149
+ if (request.dryRun) {
18150
+ return {
18151
+ alias,
18152
+ written: false,
18153
+ declaration: declaration2,
18154
+ project: producer?.id ?? declared,
18155
+ ...producer ? {} : { unreachable: `its producer could not be read (${why2}), so nothing would be pinned` }
18156
+ };
18157
+ }
18158
+ try {
18159
+ declareExternal(alias, declaration2);
18160
+ } catch (e) {
18161
+ return refused(alias, (e instanceof Error ? e.message : String(e)).replace(/\.$/, ""));
17854
18162
  }
17855
18163
  let pin2;
17856
18164
  if (request.pin !== false && producer) pin2 = pinExternals([alias])[0];
17857
- const unreachable = producer ? void 0 : `its producer could not be read (${"why" in answered ? answered.why : "unknown"}), so nothing was pinned \u2014 it reads as unavailable, never a pass, until \`wairon externals pin ${alias}\` succeeds`;
18165
+ const unreachable = producer ? void 0 : `its producer could not be read (${why2}), so nothing was pinned \u2014 it reads as unavailable, never a pass, until \`wairon externals pin ${alias}\` succeeds`;
17858
18166
  return {
17859
18167
  alias,
17860
18168
  written: true,
@@ -17864,6 +18172,77 @@ function declare(request) {
17864
18172
  ...unreachable ? { unreachable } : {}
17865
18173
  };
17866
18174
  }
18175
+ function remove(alias, dryRun) {
18176
+ const config = loadProjectConfig();
18177
+ const declared = config?.externals?.[alias] !== void 0;
18178
+ const rows = !declared || dryRun ? listExternals() : [];
18179
+ const pinned = rows.some((r) => r.alias === alias && r.lock !== void 0);
18180
+ if (!declared && !pinned) {
18181
+ const known = rows.map((r) => `"${r.alias}"`);
18182
+ return {
18183
+ alias,
18184
+ removed: false,
18185
+ unpinned: false,
18186
+ refusal: `The external "${alias}" was not removed: it is neither declared nor pinned here \u2014 ${known.length ? `this project's externals are ${known.join(", ")}` : "this project declares no externals"}.`
18187
+ };
18188
+ }
18189
+ if (dryRun) return { alias, removed: false, unpinned: pinned };
18190
+ const removed = declared ? removeExternal(alias) : false;
18191
+ const unpinned = unpin(alias);
18192
+ return { alias, removed, unpinned };
18193
+ }
18194
+ function currentUse(declaration2) {
18195
+ const use = typeof declaration2 === "object" && declaration2 !== null ? declaration2.use : void 0;
18196
+ return Array.isArray(use) ? use.filter((u) => typeof u === "string") : [];
18197
+ }
18198
+ function useRefused(alias, use, why2) {
18199
+ return { alias, written: false, use, added: [], removed: [], refusal: `The \`use\` of "${alias}" was not changed: ${why2}.` };
18200
+ }
18201
+ function updateUse(request) {
18202
+ const { alias } = request;
18203
+ const config = loadProjectConfig();
18204
+ const existing = config?.externals?.[alias];
18205
+ const current2 = currentUse(existing);
18206
+ if (existing === void 0) {
18207
+ const declared = Object.keys(config?.externals ?? {});
18208
+ return useRefused(alias, current2, `"${alias}" is not a declared external \u2014 ${declared.length ? `this project declares ${declared.map((a) => `"${a}"`).join(", ")}` : "this project declares no externals"} (declare it with \`wairon externals add\`)`);
18209
+ }
18210
+ const add2 = [...new Set(request.add ?? [])];
18211
+ const drop = [...new Set(request.remove ?? [])];
18212
+ const bad = [...add2, ...drop].find((u) => !USE_ENTRY_RE.test(u));
18213
+ if (bad !== void 0) return useRefused(alias, current2, `the entry "${bad}" is neither \`*\` nor a public name ([a-z0-9-_]+)`);
18214
+ if (add2.length === 0 && drop.length === 0) return useRefused(alias, current2, "name at least one public name to add or to remove");
18215
+ const kept = current2.filter((u) => !drop.includes(u));
18216
+ const appended = kept.includes("*") ? [] : add2.filter((u) => !kept.includes(u));
18217
+ const next = [...kept, ...appended];
18218
+ const removedNames = current2.filter((u) => drop.includes(u));
18219
+ const named2 = appended.filter((u) => u !== "*");
18220
+ if (named2.length > 0) {
18221
+ const binding = resolveDeclared(true).find((b) => b.external.alias === alias);
18222
+ const answered = binding ? readProducer(binding) : null;
18223
+ if (answered && "read" in answered) {
18224
+ const missing = named2.filter((u) => !answered.read.exported.includes(u));
18225
+ if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered.read)).join("; "));
18226
+ }
18227
+ }
18228
+ const same = next.length === current2.length && next.every((u, i) => u === current2[i]);
18229
+ if (request.dryRun || same) return { alias, written: false, use: next, added: appended, removed: removedNames };
18230
+ if (removedNames.length > 0) {
18231
+ const original = typeof existing === "object" && existing !== null ? existing : {};
18232
+ const { use: _old, ...rest } = original;
18233
+ const restated = { ...rest, ...next.length ? { use: next } : {} };
18234
+ removeExternal(alias);
18235
+ try {
18236
+ declareExternal(alias, restated);
18237
+ } catch (e) {
18238
+ declareExternal(alias, original);
18239
+ return useRefused(alias, current2, (e instanceof Error ? e.message : String(e)).replace(/\.$/, ""));
18240
+ }
18241
+ return { alias, written: true, use: next, added: appended, removed: removedNames };
18242
+ }
18243
+ importNames(alias, appended);
18244
+ return { alias, written: true, use: next, added: appended, removed: removedNames };
18245
+ }
17867
18246
 
17868
18247
  // src/core/surface-portal.ts
17869
18248
  function exportSurface2(maxAudience, format, outPath, portalId) {
@@ -17902,6 +18281,12 @@ function declareExternal2(request) {
17902
18281
  function pinnedParent2() {
17903
18282
  return pinnedParent();
17904
18283
  }
18284
+ function removeExternal2(alias, dryRun) {
18285
+ return remove(alias, dryRun);
18286
+ }
18287
+ function updateExternalUse(request) {
18288
+ return updateUse(request);
18289
+ }
17905
18290
 
17906
18291
  // src/core/family-validation.ts
17907
18292
  var fs11 = __toESM(require("fs"));
@@ -18009,11 +18394,18 @@ function findingChanges(before, after) {
18009
18394
  }
18010
18395
 
18011
18396
  // src/core/rules/reach-model-projector.ts
18012
- function bindingLine(endpoint) {
18397
+ function underBase(basePath, path49) {
18398
+ const base = (basePath ?? "").trim().replace(/\/+$/, "");
18399
+ if (base === "" || base === "/") return path49;
18400
+ const head2 = base.startsWith("/") ? base : `/${base}`;
18401
+ if (path49 === head2 || path49.startsWith(`${head2}/`)) return path49;
18402
+ return `${head2}${path49.startsWith("/") ? "" : "/"}${path49}`;
18403
+ }
18404
+ function bindingLine(endpoint, basePath) {
18013
18405
  if (!endpoint) return void 0;
18014
18406
  switch (endpoint.transport) {
18015
18407
  case "HTTP":
18016
- return `${endpoint.method} ${endpoint.path}`;
18408
+ return `${endpoint.method} ${underBase(basePath, endpoint.path)}`;
18017
18409
  case "gRPC":
18018
18410
  return `${endpoint.service}/${endpoint.method}`;
18019
18411
  case "GraphQL":
@@ -18053,6 +18445,12 @@ function widestAudience(ctx, portal, interfaceId) {
18053
18445
  function byVerb(a, b) {
18054
18446
  return a.project.localeCompare(b.project) || a.portal.localeCompare(b.portal) || a.verb.localeCompare(b.verb);
18055
18447
  }
18448
+ function byPlacement(a, b) {
18449
+ return a.project.localeCompare(b.project) || a.component.localeCompare(b.component);
18450
+ }
18451
+ function byTopic(a, b) {
18452
+ return a.component.localeCompare(b.component) || a.topic.localeCompare(b.topic) || Number(a.emits) - Number(b.emits);
18453
+ }
18056
18454
  function byCall(a, b) {
18057
18455
  return a.fromProject.localeCompare(b.fromProject) || a.fromComponent.localeCompare(b.fromComponent) || a.toPortal.localeCompare(b.toPortal) || a.verb.localeCompare(b.verb) || a.evidence.localeCompare(b.evidence);
18058
18456
  }
@@ -18068,7 +18466,7 @@ function project(ctx, network) {
18068
18466
  for (const m of intf.methods) {
18069
18467
  const entry = componentEntryFor(comp, m.invokedBy);
18070
18468
  if (isOutsideEntry(entry)) outsideEntered = true;
18071
- const binding = comp.transport === "InProcess" ? void 0 : bindingLine(m.endpoint);
18469
+ const binding = comp.transport === "InProcess" ? void 0 : bindingLine(m.endpoint, comp.basePath);
18072
18470
  const audience = widestAudience(ctx, comp.id, intf.id);
18073
18471
  verbs.push({
18074
18472
  project: "",
@@ -18117,11 +18515,32 @@ function project(ctx, network) {
18117
18515
  }
18118
18516
  }
18119
18517
  }
18518
+ const placements = ctx.components.filter(isOwn2).map((c) => ({
18519
+ project: "",
18520
+ component: c.id,
18521
+ ...c.subsystem !== void 0 ? { subsystem: c.subsystem } : {},
18522
+ componentType: c.componentType
18523
+ }));
18524
+ const topics = [];
18525
+ const addEnd = (component, topic, emits) => {
18526
+ if (!topics.some((t) => t.component === component && t.topic === topic && t.emits === emits)) topics.push({ component, topic, emits });
18527
+ };
18528
+ for (const comp of ctx.components.filter(isOwn2)) {
18529
+ for (const e of comp.emits ?? []) addEnd(comp.id, e.topic, true);
18530
+ for (const e of comp.subscribesTo ?? []) addEnd(comp.id, e.topic, false);
18531
+ for (const intf of ctx.interfacesByComponent.get(comp.id) ?? []) {
18532
+ for (const m of intf.methods) {
18533
+ if (m.endpoint?.transport === "MessageBus") addEnd(comp.id, m.endpoint.topic, m.endpoint.direction === "publish");
18534
+ }
18535
+ }
18536
+ }
18120
18537
  return {
18121
18538
  scope: "own",
18122
18539
  networks: ownNetwork ? [{ ...ownNetwork, gateways: [...ownNetwork.gateways].sort() }] : [],
18123
18540
  verbs: verbs.sort(byVerb),
18124
- calls: calls.sort(byCall)
18541
+ calls: calls.sort(byCall),
18542
+ placements: placements.sort(byPlacement),
18543
+ topics: topics.sort(byTopic)
18125
18544
  };
18126
18545
  }
18127
18546
  function qualify(projectKey, local) {
@@ -18156,21 +18575,32 @@ function compose(models, family, networks) {
18156
18575
  };
18157
18576
  const verbs = [];
18158
18577
  const calls = [];
18578
+ const placements = [];
18579
+ const topics = [];
18159
18580
  for (const [key2, model] of models) {
18581
+ for (const p of model.placements ?? []) {
18582
+ placements.push({
18583
+ ...p,
18584
+ project: key2,
18585
+ component: qualify(key2, p.component),
18586
+ ...p.subsystem !== void 0 ? { subsystem: qualify(key2, p.subsystem) } : {}
18587
+ });
18588
+ }
18589
+ for (const t of model.topics ?? []) topics.push({ ...t, component: qualify(key2, t.component) });
18160
18590
  const net = innermost(key2);
18161
18591
  for (const v of model.verbs) {
18162
18592
  const { network: _own, ...rest } = v;
18163
- const placed = {
18593
+ const placed2 = {
18164
18594
  ...rest,
18165
18595
  project: key2,
18166
18596
  portal: qualify(key2, v.portal),
18167
18597
  ...v.subsystem !== void 0 ? { subsystem: qualify(key2, v.subsystem) } : {},
18168
18598
  ...net !== void 0 ? { network: net } : {}
18169
18599
  };
18170
- verbs.push(placed);
18171
- if (net !== void 0 && placed.gateway && isOutsideEntry(placed.entry)) {
18600
+ verbs.push(placed2);
18601
+ if (net !== void 0 && placed2.gateway && isOutsideEntry(placed2.entry)) {
18172
18602
  const gateways = boundaries.get(net).gateways;
18173
- if (!gateways.includes(placed.portal)) gateways.push(placed.portal);
18603
+ if (!gateways.includes(placed2.portal)) gateways.push(placed2.portal);
18174
18604
  }
18175
18605
  }
18176
18606
  for (const c of model.calls) {
@@ -18186,15 +18616,17 @@ function compose(models, family, networks) {
18186
18616
  }
18187
18617
  }
18188
18618
  const portals = new Set(verbs.map((v) => v.portal));
18619
+ const placed = new Map(placements.map((p) => [p.component, p]));
18189
18620
  for (const ref of family.references) {
18190
18621
  if (!["call", "calls", "register", "dispatch"].includes(ref.position) || ref.member === void 0) continue;
18191
18622
  if (!portals.has(ref.target)) continue;
18192
18623
  const net = innermost(ref.consumer);
18624
+ const caller = ref.caller ?? ref.specId;
18625
+ const subsystem = placed.get(caller)?.subsystem;
18193
18626
  calls.push({
18194
18627
  fromProject: ref.consumer,
18195
- // The component whose implementation writes the call, as the
18196
- // reference recorded it — never guessed from a matching dependsOn.
18197
- fromComponent: ref.caller ?? ref.specId,
18628
+ fromComponent: caller,
18629
+ ...subsystem !== void 0 ? { fromSubsystem: subsystem } : {},
18198
18630
  ...net !== void 0 ? { fromNetwork: net } : {},
18199
18631
  toPortal: ref.target,
18200
18632
  verb: ref.member,
@@ -18205,7 +18637,9 @@ function compose(models, family, networks) {
18205
18637
  scope: "family",
18206
18638
  networks: [...boundaries.values()].map((b) => ({ ...b, gateways: [...b.gateways].sort() })).sort((a, b) => depth(a.id) - depth(b.id) || a.id.localeCompare(b.id)),
18207
18639
  verbs: verbs.sort(byVerb),
18208
- calls: calls.sort(byCall)
18640
+ calls: calls.sort(byCall),
18641
+ placements: placements.sort(byPlacement),
18642
+ topics: topics.sort(byTopic)
18209
18643
  };
18210
18644
  }
18211
18645
 
@@ -18495,6 +18929,8 @@ function run(options) {
18495
18929
  issues.push(...judged.own.issues.map((i) => ({ ...i, project: ref.key })), ...judged.composed.findings);
18496
18930
  projects.push(verdictOf({ namespace: ref.key, directory: ref.directory }, judged.own));
18497
18931
  }
18932
+ const paired = pairTopicsAcrossFamily(issues, models);
18933
+ issues.splice(0, issues.length, ...paired);
18498
18934
  issues.push(...within2(root, ceiling, () => checkMembers(narrowed(family, selected))));
18499
18935
  issues.push(...within2(root, ceiling, () => checkPolicies(narrowed(family, selected))));
18500
18936
  issues.push(...within2(root, ceiling, () => checkReach(narrowed(family, selected), models)));
@@ -18523,7 +18959,11 @@ function howMoved(use) {
18523
18959
  const changed = use.detail?.includes("signature changed") ? " (and its signature changed)" : "";
18524
18960
  return `was renamed to "${use.renamedTo}"${changed}, per the producer's rename trace`;
18525
18961
  }
18526
- return "is gone from the producer's live export table";
18962
+ return `is gone from the producer's live export table${use.detail ? ` (${use.detail})` : ""}`;
18963
+ }
18964
+ function staleTail(status) {
18965
+ const facts = status.staleFacts ?? [];
18966
+ return facts.length ? ` Its pinned snapshot is stale: ${facts.join(", ")} changed in the producer, and the rules judge the pin (a library's abi, an entry's transport or role) \u2014 re-pinning refreshes it.` : "";
18527
18967
  }
18528
18968
  function judgeExternal(status, project3, config) {
18529
18969
  const out = [];
@@ -18538,11 +18978,11 @@ function judgeExternal(status, project3, config) {
18538
18978
  project3
18539
18979
  ));
18540
18980
  }
18541
- if (broken.length === 0 && status.drifted) {
18981
+ if (broken.length === 0 && (status.drifted || (status.staleFacts?.length ?? 0) > 0)) {
18542
18982
  out.push(finding(
18543
18983
  config,
18544
18984
  "EXTERNAL_DRIFTED",
18545
- `The producer of ${who} changed since ${named(project3)} pinned it, but nothing ${named(project3)} uses changed. Re-pin when convenient (\`wairon externals pin ${status.alias}\`).`,
18985
+ `The producer of ${who} changed since ${named(project3)} pinned it, but nothing ${named(project3)} uses changed.${staleTail(status)} Re-pin when convenient (\`wairon externals pin ${status.alias}\`).`,
18546
18986
  project3
18547
18987
  ));
18548
18988
  }
@@ -18597,18 +19037,18 @@ function adviseOn(status, family, config) {
18597
19037
  case "incompatible": {
18598
19038
  const broken = status.uses.filter(isBroken);
18599
19039
  const users = [...new Set(broken.flatMap((u) => specsUsing(family, status.alias, u.publicName)))].sort();
18600
- const renamed = broken.some((u) => u.state === "renamed");
19040
+ const renamed2 = broken.some((u) => u.state === "renamed");
18601
19041
  return advisory(
18602
19042
  config,
18603
19043
  "EXTERNAL_LIVE_INCOMPATIBLE",
18604
- `${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. Adapt the uses${renamed ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
19044
+ `${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
18605
19045
  );
18606
19046
  }
18607
19047
  case "drifted":
18608
19048
  return advisory(
18609
19049
  config,
18610
19050
  "EXTERNAL_DRIFTED",
18611
- `The live producer of ${who} moved since it was pinned, but nothing this project uses changed. Re-pin when convenient (${repin(status.alias)}).`
19051
+ `The live producer of ${who} moved since it was pinned, but nothing this project uses changed.${staleTail(status)} Re-pin when convenient (${repin(status.alias)}).`
18612
19052
  );
18613
19053
  case "unavailable": {
18614
19054
  const reason = uncomparedReason(status);
@@ -19138,7 +19578,51 @@ function reachModel(options) {
19138
19578
  models.set(node.namespace, within2(node.directory, ceiling, () => ownReachOf(options)));
19139
19579
  }
19140
19580
  const scoped = narrowed(family, selected);
19141
- return within2(root, ceiling, () => compose(models, scoped, networksOf(scoped)));
19581
+ return within2(root, ceiling, () => {
19582
+ const model = compose(models, scoped, networksOf(scoped));
19583
+ return { ...model, findings: judgedFindings(model, scoped) };
19584
+ });
19585
+ }
19586
+ function judgedFindings(model, family) {
19587
+ const nodeOf = new Map(family.nodes.map((n) => [n.namespace, n]));
19588
+ const configs = /* @__PURE__ */ new Map();
19589
+ const allows = /* @__PURE__ */ new Map();
19590
+ const out = [];
19591
+ for (const f of judge(model)) {
19592
+ const project3 = projectOfFinding(model, f);
19593
+ const node = nodeOf.get(project3);
19594
+ if (!configs.has(project3)) configs.set(project3, node ? runWithProjectRoot(node.directory, configOrNull) : null);
19595
+ if (!allows.has(project3)) allows.set(project3, node ? runWithProjectRoot(node.directory, lintAllows) : []);
19596
+ const local = f.specId !== void 0 ? localKey(project3, f.specId) : void 0;
19597
+ const tuned = finding(configs.get(project3) ?? null, f.code, f.message, project3, f.specId);
19598
+ if (!tuned) continue;
19599
+ const allowed = tuned.severity !== "error" && local !== void 0 && (allows.get(project3) ?? []).some((a) => a.specId === local && a.code === f.code && (a.at ?? void 0) === (f.at ?? void 0));
19600
+ if (allowed) continue;
19601
+ out.push({
19602
+ severity: tuned.severity,
19603
+ code: f.code,
19604
+ message: f.message,
19605
+ ...f.specId !== void 0 ? { specId: f.specId } : {},
19606
+ ...f.at !== void 0 ? { at: f.at } : {}
19607
+ });
19608
+ }
19609
+ return out;
19610
+ }
19611
+ function pairTopicsAcrossFamily(issues, models) {
19612
+ const ends = [...models.entries()].flatMap(([project3, m]) => (m.topics ?? []).map((t) => ({ ...t, project: project3 })));
19613
+ if (ends.length === 0) return [...issues];
19614
+ const pairedElsewhere = (project3, component, emits) => {
19615
+ const own2 = ends.filter((e) => e.project === project3);
19616
+ const mine = own2.filter((e) => e.component === component && e.emits === emits).map((e) => e.topic);
19617
+ const unpaired = mine.filter((t) => !own2.some((o) => o.topic === t && o.emits !== emits));
19618
+ if (unpaired.length === 0) return false;
19619
+ return unpaired.every((t) => ends.some((o) => o.project !== project3 && o.topic === t && o.emits !== emits));
19620
+ };
19621
+ return issues.filter((i) => {
19622
+ if (i.code !== "UNCONSUMED_TOPIC" && i.code !== "UNSOURCED_SUBSCRIPTION") return true;
19623
+ if (i.project === void 0 || i.specId === void 0) return true;
19624
+ return !pairedElsewhere(i.project, i.specId, i.code === "UNCONSUMED_TOPIC");
19625
+ });
19142
19626
  }
19143
19627
 
19144
19628
  // src/core/rules/index.ts
@@ -21096,8 +21580,8 @@ var publicSurfaceBindingRule = {
21096
21580
  ctx.addIssue("error", "PUBLIC_INTERFACE_INVALID_COMPONENT", `Subsystem "${sub.id}" public interface references component "${pi.component}" which does not exist.`, sub.id, isDraftCtx);
21097
21581
  continue;
21098
21582
  }
21099
- const isSubsystemOwner = backing.subsystem === sub.id || backing.subsystem.startsWith(sub.id + "::");
21100
- if (!isSubsystemOwner) {
21583
+ const isSubsystemOwner2 = backing.subsystem === sub.id || backing.subsystem.startsWith(sub.id + "::");
21584
+ if (!isSubsystemOwner2) {
21101
21585
  ctx.addIssue("error", "PUBLIC_INTERFACE_FOREIGN_COMPONENT", `Subsystem "${sub.id}" publishes component "${pi.component}", but it belongs to subsystem "${backing.subsystem}". A subsystem may only publish its own components.`, sub.id, isDraftCtx);
21102
21586
  }
21103
21587
  }
@@ -21136,19 +21620,23 @@ var publicSurfaceConsumersRule = {
21136
21620
  var implementsContractsRule = {
21137
21621
  name: "implements-contracts",
21138
21622
  judges: "design",
21139
- description: "An interface that declares `implements: alias::name` realizes another project's extension point, so it must declare every method of that contract with the same params and returns, read from the export the reference resolves to (a contained member's live table, or a declared external's pin). Whether the reference resolves and names an entry exported with role implement is project-boundaries' finding (EXTERNAL_NOT_EXPORTED); this rule judges the shape once it resolves. Extra methods are the implementer's own and are not reported.",
21623
+ description: "An interface that declares `implements: alias::name` realizes another project's extension point, so it must declare every method of that contract with the same params and returns, read from the export the reference resolves to (a contained member's live table, or a declared external's pin). Types are compared semantically, never by spelling: the producer's own bare name and the consumer's `alias::name` (or the bare name its `use` imports) are one type. Whether the reference resolves and names an entry exported with role implement is project-boundaries' finding (EXTERNAL_NOT_EXPORTED); this rule judges the shape once it resolves. Extra methods are the implementer's own and are not reported.",
21140
21624
  codes: [
21141
21625
  { code: "IMPLEMENTS_MISMATCH", defaultSeverity: "error", summary: "An interface implementing an extension point lacks one of its methods, or declares one with different params or returns" }
21142
21626
  ],
21143
21627
  check(ctx) {
21144
21628
  for (const intf of ctx.interfaces) {
21145
21629
  if (intf.implements === void 0 || !ctx.isSpecInScope(intf.id)) continue;
21146
- const contract = extensionPoint(ctx, intf.id, intf.implements);
21147
- if (!contract) continue;
21630
+ const point = extensionPoint(ctx, intf.id, intf.implements);
21631
+ if (!point) continue;
21632
+ const { methods: contract, publicNames } = point;
21633
+ const alias = intf.implements.split("::")[0];
21634
+ const expectedType = (t) => respelled(t, (n) => publicNames.get(lastName(n)) ?? lastName(n));
21635
+ const ownType = (t) => respelled(t, (n) => n.startsWith(`${alias}::`) ? lastName(n.slice(alias.length + 2)) : n);
21148
21636
  const draft = intf.status === "draft" || intf.status === "design" || ctx.isComponentDraft(intf.component);
21149
21637
  for (const expected of contract) {
21150
21638
  const own2 = intf.methods.find((m) => m.name === expected.name);
21151
- const differs = own2 ? difference(expected, own2) : "is not declared";
21639
+ const differs = own2 ? difference(expected, own2, expectedType, ownType) : "is not declared";
21152
21640
  if (!differs) continue;
21153
21641
  ctx.addIssue(
21154
21642
  "error",
@@ -21163,17 +21651,36 @@ var implementsContractsRule = {
21163
21651
  }
21164
21652
  }
21165
21653
  };
21166
- function difference(expected, own2) {
21167
- const shape = (m) => (m.params ?? []).map((p) => `${p.type}${p.optional ? "?" : ""}`).join(", ");
21168
- if (shape(expected) !== shape(own2)) return `params (${shape(own2)}) instead of (${shape(expected)})`;
21169
- if (expected.returns !== own2.returns) return `returns ${own2.returns} instead of ${expected.returns}`;
21654
+ function lastName(name) {
21655
+ return name.split("::").pop().split(".").pop();
21656
+ }
21657
+ function renamed(expr, rename) {
21658
+ const args = expr.args.map((a) => renamed(a, rename));
21659
+ return expr.form === "named" || expr.form === "applied" ? { ...expr, name: rename(expr.name), args } : { ...expr, args };
21660
+ }
21661
+ function respelled(text3, rename) {
21662
+ const keep = (name) => isTypeVocabulary(name) ? name : rename(name);
21663
+ const parsed = parseTypeExpression(text3, "returns");
21664
+ if (parsed.expression) return canonicalTypeText(renamed(parsed.expression, keep));
21665
+ return text3.replace(/[A-Za-z_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z_][A-Za-z0-9_-]*)*/g, keep);
21666
+ }
21667
+ function difference(expected, own2, expectedType, ownType) {
21668
+ const shape = (m, as) => (m.params ?? []).map((p) => `${as(p.type)}${p.optional ? "?" : ""}`).join(", ");
21669
+ const written = (m) => (m.params ?? []).map((p) => `${p.type}${p.optional ? "?" : ""}`).join(", ");
21670
+ if (shape(expected, expectedType) !== shape(own2, ownType)) return `params (${written(own2)}) instead of (${written(expected)})`;
21671
+ if (expectedType(expected.returns) !== ownType(own2.returns)) return `returns ${own2.returns} instead of ${expected.returns}`;
21170
21672
  return void 0;
21171
21673
  }
21172
21674
  function extensionPoint(ctx, specId, ref) {
21173
21675
  const resolution = ctx.resolveCrossProject(specId, "implements", ref);
21174
21676
  if (resolution && resolution.outcome !== "resolved") return void 0;
21175
21677
  const surface = ctx.resolveSurfaceRef(ref);
21176
- if (surface.kind === "resolved") return surface.entry.role === "implement" ? surface.entry.methods : void 0;
21678
+ if (surface.kind === "resolved") {
21679
+ if (surface.entry.role !== "implement") return void 0;
21680
+ const publicNames2 = /* @__PURE__ */ new Map();
21681
+ for (const t of surface.snapshot.exportedTypes ?? []) publicNames2.set(lastName(t.type), t.id);
21682
+ return { methods: surface.entry.methods, publicNames: publicNames2 };
21683
+ }
21177
21684
  const [alias, name] = ref.split("::");
21178
21685
  if (name === void 0) return void 0;
21179
21686
  const bound2 = ctx.projectFamily?.nodes.find((n) => n.namespace === "");
@@ -21182,8 +21689,13 @@ function extensionPoint(ctx, specId, ref) {
21182
21689
  const table = (ctx.exportTables ?? []).find((t) => t.level === "project" && t.owner === key2);
21183
21690
  const entry = table?.entries.find((e) => e.kind === "component" && e.publicName === name);
21184
21691
  if (!entry || entry.role !== "implement" || !entry.component) return void 0;
21185
- if (entry.interface) return ctx.interfaceMap.get(entry.interface)?.methods;
21186
- return ctx.interfaceMethodsOf(entry.component);
21692
+ const methods = entry.interface ? ctx.interfaceMap.get(entry.interface)?.methods : ctx.interfaceMethodsOf(entry.component);
21693
+ if (!methods) return void 0;
21694
+ const publicNames = /* @__PURE__ */ new Map();
21695
+ for (const e of table?.entries ?? []) {
21696
+ if (e.kind === "type" && e.typeDef) publicNames.set(lastName(e.typeDef), e.publicName);
21697
+ }
21698
+ return { methods, publicNames };
21187
21699
  }
21188
21700
 
21189
21701
  // src/core/rules/integrity/public-surface-bound-contract.ts
@@ -21215,13 +21727,14 @@ var publicSurfaceBoundContractRule = {
21215
21727
  var exportTablesRule = {
21216
21728
  name: "export-tables",
21217
21729
  judges: "design",
21218
- description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`. One public name binds one target: EXPORT_ID_DUPLICATE when two wildcards (or two explicit entries) bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out. No chain leads back to itself: EXPORT_CYCLE, an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union. Every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns (a subsystem-owned type at L1, a project-level type with no subsystem for an L0 `{ typeDef }` without `from`), under a public name of [a-z0-9-_]+ (EXPORT_INVALID). Every exported component can serve what its role promises (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports): an entry consumers call must be a Portal (a gateway being a Portal variant) or an Observer for events, and an entry consumers implement (role implement, an extension point) may also be an Adapter whose realization consumers supply. An L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. An entry's export kind is derived from its backing Portal's transport and no longer authored, so no rule compares an authored kind with the stereotype; the retired public-surface-declared-type rule and its codes are gone. The rule judges the facts the export resolver recorded in the scan, for the bound root's table and every contained member's, and resolves nothing itself.",
21730
+ description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`. One public name binds one target: EXPORT_ID_DUPLICATE when two wildcards (or two explicit entries) bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out. No chain leads back to itself: EXPORT_CYCLE, an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union. Every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns (a subsystem-owned type at L1, a project-level type with no subsystem for an L0 `{ typeDef }` without `from`), under a public name of [a-z0-9-_]+ (EXPORT_INVALID) \u2014 a named re-export whose source renamed the item names the new name. Every exported component can serve what its role promises (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports): an entry consumers call must be a Portal (a gateway being a Portal variant) or an Observer for events, and an entry consumers implement (role implement, an extension point) may also be an Adapter whose realization consumers supply. An L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. An own L0 entry exported to `department` is a notice (EXPORT_AUDIENCE_NARROW): outside a hosted instance only the project's family sees it, and a sibling checkout or git consumer \u2014 read at `instance` \u2014 is refused; whether its consumers are hosted cannot be told from the tree, so it never fails the gate. An entry's export kind is derived from its backing Portal's transport and no longer authored, so no rule compares an authored kind with the stereotype; the retired public-surface-declared-type rule and its codes are gone. The rule judges the facts the export resolver recorded in the scan, for the bound root's table and every contained member's, and resolves nothing itself.",
21219
21731
  codes: [
21220
21732
  { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
21221
21733
  { code: "EXPORT_CYCLE", defaultSeverity: "error", summary: "A re-export chain leads back to itself (a warning for a wildcard cycle, which resolves to its union)" },
21222
21734
  { code: "EXPORT_INVALID", defaultSeverity: "error", summary: "An export entry names a missing source or item, an item its source does not export or its level does not own, or a malformed public name" },
21223
21735
  { code: "EXPORT_UNCONSUMABLE", defaultSeverity: "error", summary: "An exported component that cannot serve its role: called but neither a Portal nor an Observer, or implemented but neither a Portal nor an Adapter" },
21224
- { code: "EXPORT_WIDENS_AUDIENCE", defaultSeverity: "error", summary: "An L0 entry declares a wider audience than the member or external export it re-exports" }
21736
+ { code: "EXPORT_WIDENS_AUDIENCE", defaultSeverity: "error", summary: "An L0 entry declares a wider audience than the member or external export it re-exports" },
21737
+ { code: "EXPORT_AUDIENCE_NARROW", defaultSeverity: "notice", summary: "An L0 entry is exported to `department`: outside a hosted instance only this project's family sees it, and a path or git consumer is refused" }
21225
21738
  ],
21226
21739
  check(ctx) {
21227
21740
  const tables = ctx.exportTables;
@@ -21254,6 +21767,19 @@ var exportTablesRule = {
21254
21767
  }
21255
21768
  }
21256
21769
  }
21770
+ const own2 = tables.find((t) => t.level === "project" && t.owner === ctx.system.name);
21771
+ for (const entry of own2?.entries ?? []) {
21772
+ if ((entry.audience ?? "instance") !== "department") continue;
21773
+ ctx.addIssue(
21774
+ "notice",
21775
+ "EXPORT_AUDIENCE_NARROW",
21776
+ `The project's L0 export table exports "${entry.publicName}" to \`department\`. A path, git or hosted consumer outside this project's family reads it at \`instance\` and is refused it (\`externals add\` says so); only this project's family sees it locally, and, on a hosted instance, the units of the owning department \u2014 which of those its consumers are cannot be told from this tree. Audiences, narrowest first: project < department < instance < partner < external. Widen it to \`instance\` if a sibling checkout or a git consumer uses it, or keep it (and allow this notice) when its consumers are hosted department units.`,
21777
+ own2.owner,
21778
+ false,
21779
+ void 0,
21780
+ { at: entry.publicName }
21781
+ );
21782
+ }
21257
21783
  }
21258
21784
  };
21259
21785
 
@@ -21278,10 +21804,11 @@ var externalDeclarationsRule = {
21278
21804
  if (external.sourceKind !== "unresolved") continue;
21279
21805
  if (pinned.get(external.alias)?.snapshot && !external.problem?.startsWith("the `use`") && !isMalformed(external.problem)) continue;
21280
21806
  reported.add(external.alias);
21807
+ const unpinnedOutside = external.problem?.startsWith("no project of the family answers") && !pinned.get(external.alias)?.snapshot;
21281
21808
  ctx.addIssue(
21282
21809
  "notice",
21283
21810
  "EXTERNAL_UNRESOLVED",
21284
- `This project's .wai/project.yaml declares the external "${external.alias}" (producer "${external.project}"), which does not resolve: ${external.problem}. Fix the alias or the producer id, add \`source: { path: <the producer's root> }\` to say where it lives, or pin it (\`wairon externals pin\`) so this project's gate has something to judge it against.`
21811
+ unpinnedOutside ? `This project's .wai/project.yaml declares the external "${external.alias}" (producer "${external.project}"), which this project's own gate cannot resolve: no project this root contains answers to "${external.project}", and nothing is pinned for it. A parent or sibling is never climbed to from a project's own gate \u2014 its pin is the one thing the gate judges it against. Pin it here (\`wairon externals pin ${external.alias}\`), or give it \`source: { path: <the producer's root> }\`.` : `This project's .wai/project.yaml declares the external "${external.alias}" (producer "${external.project}"), which does not resolve: ${external.problem}. Fix the alias or the producer id, add \`source: { path: <the producer's root> }\` to say where it lives, or pin it (\`wairon externals pin\`) so this project's gate has something to judge it against.`
21285
21812
  );
21286
21813
  }
21287
21814
  for (const problem of family.problems) {
@@ -21477,6 +22004,50 @@ function label(node, namespace) {
21477
22004
  const id = node.id ?? node.name ?? "(no id)";
21478
22005
  return node.namespace === "" ? `"${id}"` : `"${id}" (keyed "${node.namespace}")`;
21479
22006
  }
22007
+ function lastSegment3(id) {
22008
+ return id.split("::").pop().split(".").pop();
22009
+ }
22010
+ function distance2(a, b) {
22011
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
22012
+ for (let i = 1; i <= a.length; i++) {
22013
+ let prev = row[0];
22014
+ row[0] = i;
22015
+ for (let j = 1; j <= b.length; j++) {
22016
+ const next = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
22017
+ prev = row[j];
22018
+ row[j] = next;
22019
+ }
22020
+ }
22021
+ return row[b.length];
22022
+ }
22023
+ function didYouMean(ctx, ref) {
22024
+ const written = lastSegment3(ref.authored);
22025
+ const key2 = nameKey(written);
22026
+ let names = [];
22027
+ let renamedTo;
22028
+ const table = ref.producer !== void 0 ? (ctx.exportTables ?? []).find((t) => t.level === "project" && t.owner === ref.producer) : void 0;
22029
+ if (table) {
22030
+ names = table.entries.map((e) => e.publicName);
22031
+ const traced2 = (holder) => (holder?.previousIds ?? []).some((p) => nameKey(lastSegment3(p)) === key2);
22032
+ for (const e of table.entries) {
22033
+ const holder = e.kind === "type" ? ctx.types.find((t) => t.id === e.typeDef || t.id === `${ref.producer}::${e.typeDef}`) : e.interface !== void 0 ? ctx.interfaceMap.get(e.interface) : ctx.componentMap.get(e.component ?? "");
22034
+ if (traced2(holder)) {
22035
+ renamedTo = e.publicName;
22036
+ break;
22037
+ }
22038
+ }
22039
+ } else {
22040
+ const alias = ref.authored.includes("::") ? ref.authored.split("::")[0] : void 0;
22041
+ const pin2 = alias !== void 0 ? ctx.pinnedExternals.find((p) => p.alias === alias)?.snapshot : void 0;
22042
+ if (pin2) {
22043
+ names = [...pin2.interfaces.map((e) => e.id), ...(pin2.exportedTypes ?? []).map((t) => t.id)];
22044
+ renamedTo = pin2.interfaces.find((e) => (e.formerly ?? []).some((f) => nameKey(f) === key2))?.id ?? (pin2.exportedTypes ?? []).find((t) => (pin2.types.find((d) => d.id === t.type)?.formerly ?? []).some((f) => nameKey(f) === key2))?.id ?? pin2.unresolvedExports?.find((u) => nameKey(u.id) === key2)?.renamedTo;
22045
+ }
22046
+ }
22047
+ if (renamedTo !== void 0 && renamedTo !== written) return ` It was renamed to "${renamedTo}" (the producer's rename trace records "${written}") \u2014 did you mean "${renamedTo}"?`;
22048
+ const close2 = [...new Set(names)].filter((n) => n !== written).sort((a, b) => distance2(written, a) - distance2(written, b) || (a < b ? -1 : 1)).filter((n) => distance2(written, n) <= Math.max(2, Math.floor(written.length / 2))).slice(0, 3);
22049
+ return close2.length ? ` Did you mean ${close2.map((n) => `"${n}"`).join(" or ")}?` : "";
22050
+ }
21480
22051
  function declaration(producer) {
21481
22052
  const id = producer?.id;
21482
22053
  if (!id || !PROJECT_ID_RE.test(id)) return "an `externals` entry naming it (it has no usable id yet \u2014 declare one first)";
@@ -21517,7 +22088,7 @@ var projectBoundariesRule = {
21517
22088
  ctx.addIssue(
21518
22089
  "error",
21519
22090
  "EXTERNAL_NOT_EXPORTED",
21520
- `${where}, which reaches "${resolution.canonicalTarget}" at no public name the referrer may see: ${resolution.reason}. Another project may reach it only through a public name of its L0 table whose audience covers the referrer \u2014 export it from that project's L0 (a project-level type takes one own \`{ typeDef }\` entry) and re-pin, or reach it by a name the producer exports.`,
22091
+ `${where}, which reaches "${resolution.canonicalTarget}" at no public name the referrer may see: ${resolution.reason}.${didYouMean(ctx, ref)} Another project may reach it only through a public name of its L0 table whose audience covers the referrer \u2014 export it from that project's L0 (a project-level type takes one own \`{ typeDef }\` entry) and re-pin, or reach it by a name the producer exports.`,
21521
22092
  ref.specId,
21522
22093
  draft,
21523
22094
  resolution
@@ -22553,10 +23124,10 @@ var portalFieldsRule = {
22553
23124
  name: "portal-fields",
22554
23125
  judges: "design",
22555
23126
  scope: "spec",
22556
- description: "A Portal declares its transport (the retired portalType is read as it for one release); only an InProcess Portal may declare an abi. Non-Portal components carry no transport, abi, basePath, auth or Portal-level invokedBy entry (auth is inbound transport auth: it belongs on the Portal that exposes the surface; an entry on a non-Portal is entry-declarations' ENTRY_ON_NON_PORTAL). Intrinsic to one component: no tree required.",
23127
+ description: "A Portal declares its transport (the retired portalType is read as it for one release); only an InProcess Portal may declare an abi. An Adapter may declare a transport: the one it calls its target Portal over (adapter-transport judges it against the target). Every other non-Portal component carries no transport, and no non-Portal carries an abi, basePath, auth or Portal-level invokedBy entry (auth is inbound transport auth: it belongs on the Portal that exposes the surface; an entry on a non-Portal is entry-declarations' ENTRY_ON_NON_PORTAL). Intrinsic to one component: no tree required.",
22557
23128
  codes: [
22558
23129
  { code: "MISSING_PORTAL_TRANSPORT", defaultSeverity: "error", summary: "Portal without a transport" },
22559
- { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with transport, abi or basePath, or an abi on a Portal whose transport is not InProcess" },
23130
+ { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with an abi or basePath, or a transport on one that is not an Adapter, or an abi on a Portal whose transport is not InProcess" },
22560
23131
  { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
22561
23132
  ],
22562
23133
  check(ctx) {
@@ -22583,11 +23154,12 @@ var portalFieldsRule = {
22583
23154
  }
22584
23155
  continue;
22585
23156
  }
22586
- if (comp.transport !== void 0 || comp.abi !== void 0 || comp.basePath !== void 0) {
23157
+ const strayTransport = comp.transport !== void 0 && comp.componentType !== "Adapter";
23158
+ if (strayTransport || comp.abi !== void 0 || comp.basePath !== void 0) {
22587
23159
  ctx.addIssue(
22588
23160
  "error",
22589
23161
  "UNEXPECTED_PORTAL_FIELD",
22590
- `Component "${comp.id}" is a ${comp.componentType}, not a Portal, but has "transport", "abi" or "basePath" configured. They are Portal-only \u2014 drop them, or make this component a Portal.`,
23162
+ `Component "${comp.id}" is a ${comp.componentType}, not a Portal, but has "transport", "abi" or "basePath" configured. abi and basePath are Portal-only, and a transport belongs to a Portal (or to an Adapter, as the one it calls its target over) \u2014 drop them, or make this component a Portal.`,
22591
23163
  comp.id,
22592
23164
  isDraftCtx
22593
23165
  );
@@ -24531,6 +25103,48 @@ var networkBoundariesRule = {
24531
25103
  }
24532
25104
  };
24533
25105
 
25106
+ // src/core/rules/wiring/adapter-transport.ts
25107
+ var adapterTransportRule = {
25108
+ name: "adapter-transport",
25109
+ judges: "design",
25110
+ description: "An Adapter calls a verb on its target Portal's interface, so its transport is the target's: inferred from the target Portal when the Adapter states none (HTTP for an HTTP API, a database connection for a database Portal, ...). An Adapter MAY state its own transport; when it does and a Portal it calls (one it names in dependsOn, or the target of one of its call, register or dispatch steps or declared calls) has another transport, that is ADAPTER_TRANSPORT_MISMATCH, one finding per target \u2014 so changing a Portal's transport shows its impact on every Adapter that calls it. A Portal the tree cannot resolve (an external one) is not judged here. Reads the components and narratives of the tree.",
25111
+ codes: [
25112
+ { code: "ADAPTER_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "An Adapter states a transport other than the transport of a Portal it calls" }
25113
+ ],
25114
+ check(ctx) {
25115
+ for (const adapter of ctx.components.filter((c) => c.componentType === "Adapter" && c.transport !== void 0)) {
25116
+ const targets = /* @__PURE__ */ new Map();
25117
+ const consider = (id) => {
25118
+ if (!id) return;
25119
+ const target = ctx.componentMap.get(id);
25120
+ if (target?.componentType === "Portal" && target.transport !== void 0) targets.set(target.id, target);
25121
+ };
25122
+ for (const dep of adapter.dependsOn ?? []) consider(dep);
25123
+ for (const impl of ctx.implementations) {
25124
+ if (ctx.interfaceMap.get(impl.contract)?.component !== adapter.id) continue;
25125
+ for (const method of impl.methods) {
25126
+ for (const step of method.narrative) {
25127
+ if (step.type === "call" || step.type === "register" || step.type === "dispatch") consider(step.targetComponent);
25128
+ }
25129
+ for (const entry of method.calls ?? []) consider(parseDeclaredCall(entry)?.compId);
25130
+ }
25131
+ }
25132
+ for (const target of [...targets.values()].sort((a, b) => a.id.localeCompare(b.id))) {
25133
+ if (target.transport === adapter.transport) continue;
25134
+ ctx.addIssue(
25135
+ "error",
25136
+ "ADAPTER_TRANSPORT_MISMATCH",
25137
+ `Adapter "${adapter.id}" states transport "${adapter.transport}", but it calls Portal "${target.id}", whose transport is "${target.transport}": an Adapter calls its target's verbs over the target's transport. Follow the Portal's transport (set "${target.transport}"), or drop the Adapter's transport to take its target's.`,
25138
+ adapter.id,
25139
+ ctx.isComponentDraft(adapter.id),
25140
+ void 0,
25141
+ { at: target.id }
25142
+ );
25143
+ }
25144
+ }
25145
+ }
25146
+ };
25147
+
24534
25148
  // src/core/rules/narrative-graph-projector.ts
24535
25149
  var methodKey = (compId, methodName) => `${compId}#${methodName}`;
24536
25150
  function stepEdges(step, ctx, followRegisterEdges) {
@@ -24729,6 +25343,10 @@ function portalScopeHint(comp) {
24729
25343
  }
24730
25344
 
24731
25345
  // src/core/rules/wiring/invoked-by-description.ts
25346
+ function floorNote(caller) {
25347
+ const length = (caller ?? "").trim().length;
25348
+ return ` (the floor: at least 40 characters once trimmed, and more than the method's own name \u2014 this caller has ${length})`;
25349
+ }
24732
25350
  var invokedByDescriptionRule = {
24733
25351
  name: "invoked-by-description",
24734
25352
  judges: "design",
@@ -24747,7 +25365,7 @@ var invokedByDescriptionRule = {
24747
25365
  ctx.addIssue(
24748
25366
  "warning",
24749
25367
  "INVOKED_BY_UNDESCRIBED",
24750
- `Method "${m.name}" on component "${intf.component}" declares invokedBy (${m.invokedBy.kind}) but its "caller" prose is missing or placeholder-thin \u2014 state WHO invokes it and when, so the entrypoint claim stays reviewable.`,
25368
+ `Method "${m.name}" on component "${intf.component}" declares invokedBy (${m.invokedBy.kind}) but its "caller" prose is missing or placeholder-thin${floorNote(m.invokedBy.caller)} \u2014 state WHO invokes it and when, so the entrypoint claim stays reviewable.`,
24751
25369
  intf.id,
24752
25370
  isDraftCtx
24753
25371
  );
@@ -24759,7 +25377,7 @@ var invokedByDescriptionRule = {
24759
25377
  ctx.addIssue(
24760
25378
  "warning",
24761
25379
  "INVOKED_BY_UNDESCRIBED",
24762
- `Portal "${comp.id}" declares invokedBy (${comp.invokedBy.kind}) for every verb, but its "caller" prose is missing or placeholder-thin \u2014 state WHO reaches the Portal and how, so the entry claim stays reviewable.`,
25380
+ `Portal "${comp.id}" declares invokedBy (${comp.invokedBy.kind}) for every verb, but its "caller" prose is missing or placeholder-thin${floorNote(comp.invokedBy.caller)} \u2014 state WHO reaches the Portal and how, so the entry claim stays reviewable.`,
24763
25381
  comp.id,
24764
25382
  ctx.isComponentDraft(comp.id)
24765
25383
  );
@@ -25314,7 +25932,7 @@ var sourceFileLinkageRule = {
25314
25932
  ctx.addIssue(
25315
25933
  "warning",
25316
25934
  "CONFORMANCE_DEGRADED",
25317
- `${degradedTsFiles.length} TypeScript/JavaScript source file(s) were analyzed below exact grade \u2014 the TypeScript compiler could not be resolved from the analyzed project or the wairon installation. Structural findings carry their grade, but dependency conformance skips these files. Install "typescript" in the analyzed project to restore exact analysis.`
25935
+ `${degradedTsFiles.length} TypeScript/JavaScript source file(s) were analyzed below exact grade \u2014 no TypeScript compiler API could be loaded from the analyzed project or the wairon installation: either no \`typescript\` package is installed there, or the installed one ships no JavaScript compiler API (TypeScript 7's native compiler has none; this analyzer reads the TypeScript 5.x API). Structural findings carry their grade, but dependency conformance skips these files. To restore exact analysis, make a TypeScript 5.x package resolvable from the analyzed project (\`npm i -D typescript@5\`) \u2014 on TypeScript 7, check \`node -e "console.log(typeof require('typescript').createSourceFile)"\` prints function.`
25318
25936
  );
25319
25937
  }
25320
25938
  for (const impl of ctx.implementations) {
@@ -25535,6 +26153,9 @@ function closedCallSites(code, file, fn, stopAt, container) {
25535
26153
  }
25536
26154
  return out;
25537
26155
  }
26156
+ function isRemoteLink(caller, target) {
26157
+ return caller.componentType === "Adapter" && target.componentType === "Portal" && target.transport !== void 0 && target.transport !== "InProcess";
26158
+ }
25538
26159
  var anchorKey = (symbol, exportedVia) => exportedVia !== void 0 ? `${exportedVia}.${symbol}` : symbol;
25539
26160
  var bodyKey = (file, symbol, exportedVia) => `${file}|${anchorKey(symbol, exportedVia)}`;
25540
26161
  function resolveCallTarget(ctx, componentId, methodName) {
@@ -25665,6 +26286,7 @@ var callConformanceRule = {
25665
26286
  const missed = [];
25666
26287
  for (const claim of claimsOf(implMethod)) {
25667
26288
  if (!ctx.componentMap.has(claim.component)) continue;
26289
+ if (isRemoteLink(component, ctx.componentMap.get(claim.component))) continue;
25668
26290
  const target = resolveCallTarget(ctx, claim.component, claim.method);
25669
26291
  if (target.bodies.has(bodyKey(here, fnSymbol, implMethod.exportedVia))) continue;
25670
26292
  const sharedBody = targetForwards(code, target);
@@ -25822,9 +26444,11 @@ var hiddenStateRule = {
25822
26444
  var dependencyConformanceRule = {
25823
26445
  name: "dependency-conformance",
25824
26446
  judges: "code",
25825
- description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning). A type-only import never accuses, but it does realize a declared edge: a constructor-injected collaborator is reached through its type, and `import type` is what dependency injection writes down. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
26447
+ description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). A type-only import alone never accuses \u2014 type coupling is allowed \u2014 but a CALL of a modelled contract method through a collaborator whose declared type comes from another component's file is runtime collaboration whatever the import's form, and the same justification is owed (UNDECLARED_DEPENDENCY on that edge). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning); a type-only import realizes it, since `import type` is what dependency injection writes down. Two twins of design rules are judged on the same code: a Portal file calling a write- or lifecycle-effect contract method of a Repository, Index, Store or Registry is the persistence shortcut the design rule PORTAL_WRITE_SHORTCUT refuses, unless the Portal's own narrative already claims that call (the design rule's finding then) (PORTAL_WRITE_SHORTCUT_IN_CODE); and a file importing a technology's package \u2014 a bare specifier whose package name is one of the technology's declared tokens (its name, or its `matches` when it declares them), compared exactly, never guessed \u2014 while no component it realizes is that technology's home is the leak TECH_LEAKAGE refuses in the design (TECH_LEAKAGE_IN_CODE). Only exact-grade analyzed files participate; chained subprojects validate standalone.",
25826
26448
  codes: [
25827
- { code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
26449
+ { code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import, or a call through a type-only-imported collaborator, between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
26450
+ { code: "PORTAL_WRITE_SHORTCUT_IN_CODE", defaultSeverity: "error", summary: "A Portal's code calls a write- or lifecycle-effect method of a data component (Repository, Index, Store, Registry) directly \u2014 the code twin of PORTAL_WRITE_SHORTCUT: mutations route through an Orchestrator" },
26451
+ { code: "TECH_LEAKAGE_IN_CODE", defaultSeverity: "warning", summary: "A source file imports a technology's package although no component it realizes binds that technology \u2014 the code twin of TECH_LEAKAGE" },
25828
26452
  { code: "UNREALIZED_DEPENDENCY", defaultSeverity: "warning", summary: "A declared dependsOn/owns edge between components in different files is realized by no import between any of their files" }
25829
26453
  ],
25830
26454
  check(ctx) {
@@ -25940,6 +26564,149 @@ var dependencyConformanceRule = {
25940
26564
  );
25941
26565
  }
25942
26566
  }
26567
+ const DATA_COMPONENTS2 = /* @__PURE__ */ new Set(["Repository", "Index", "Store", "Registry"]);
26568
+ const calledMethodOf = (componentId, name) => {
26569
+ const methods = ctx.interfaceMethodsOf(componentId);
26570
+ const direct = methods.find((m) => m.name === name);
26571
+ if (direct) return { name: direct.name, effect: direct.effect };
26572
+ for (const impl of realization.implementationsOf(componentId)) {
26573
+ const bound2 = impl.methods.find((m) => m.symbol === name);
26574
+ if (bound2) {
26575
+ const contract = methods.find((m) => m.name === bound2.name);
26576
+ return { name: bound2.name, effect: contract?.effect };
26577
+ }
26578
+ }
26579
+ return void 0;
26580
+ };
26581
+ const claims = (portalId, target, method) => realization.implementationsOf(portalId).some((impl) => impl.methods.some((m) => m.narrative.some((step) => step.targetComponent === target && step.targetMethod === method) || (m.calls ?? []).includes(`${target}.${method}`)));
26582
+ const typedLandings = (site, file) => {
26583
+ const scope = pathKey(site.from ?? file);
26584
+ const facts = code.factsAt(scope);
26585
+ if (!facts || !site.member) return [];
26586
+ let typeNames = [];
26587
+ if (site.field) typeNames = fieldTypesOf(facts, site.field);
26588
+ else if (site.via && !importBindingOf(facts, site.via)) typeNames = localTypesOf(facts, site.via);
26589
+ const out = [];
26590
+ for (const typeName of typeNames) {
26591
+ const typeOnly = facts.typeOnlyBindings?.[typeName];
26592
+ const runtime = typeOnly === void 0 ? importBindingOf(facts, typeName) : void 0;
26593
+ const specifier = typeOnly ?? (runtime && !runtime.namespace ? runtime.from : void 0);
26594
+ if (!specifier) continue;
26595
+ const toPath = resolveImport(scope, specifier, mappedPaths, code.packages);
26596
+ if (toPath && toPath !== scope) out.push({ toPath, typeOnly: typeOnly !== void 0 });
26597
+ }
26598
+ return out;
26599
+ };
26600
+ const calledAt = (toPath, name) => realization.componentsAt(toPath).map((c) => ({ component: c, method: calledMethodOf(c.id, name) })).filter((c) => c.method !== void 0);
26601
+ const reportedEdges = /* @__PURE__ */ new Set();
26602
+ const reportedWrites = /* @__PURE__ */ new Set();
26603
+ for (const fromPath of mappedPaths) {
26604
+ const facts = code.factsAt(fromPath);
26605
+ const fromComponents = realization.componentsAt(fromPath);
26606
+ if (!facts || fromComponents.length === 0) continue;
26607
+ for (const portal of fromComponents.filter((c) => c.componentType === "Portal")) {
26608
+ for (const impl of realization.implementationsOf(portal.id)) {
26609
+ for (const method of impl.methods) {
26610
+ if (pathKey(methodSourceFile(method, impl.sourcePath) ?? "") !== fromPath) continue;
26611
+ const sites = closedCallSites(code, fromPath, method.symbol ?? method.name, void 0, method.exportedVia) ?? [];
26612
+ for (const site of sites) {
26613
+ if (!site.member || pathKey(site.from ?? fromPath) !== fromPath) continue;
26614
+ const landings = /* @__PURE__ */ new Set([...typedLandings(site, fromPath).map((l) => l.toPath), ...code.originOf(site, fromPath)]);
26615
+ for (const toPath of landings) {
26616
+ if (toPath === fromPath) continue;
26617
+ for (const { component: target, method: called } of calledAt(toPath, site.name)) {
26618
+ if (!DATA_COMPONENTS2.has(target.componentType)) continue;
26619
+ if (called.effect !== "write" && called.effect !== "lifecycle") continue;
26620
+ if (claims(portal.id, target.id, called.name)) continue;
26621
+ const at = `${fromPath} -> ${target.id}.${called.name}`;
26622
+ if (reportedWrites.has(at)) continue;
26623
+ reportedWrites.add(at);
26624
+ ctx.addIssue(
26625
+ "error",
26626
+ "PORTAL_WRITE_SHORTCUT_IN_CODE",
26627
+ `Portal "${portal.id}": its method "${method.name}" in "${fromPath}" calls ${called.effect}-effect method ${target.id}.${called.name} directly (\`${site.via ?? (site.field ? `this.${site.field}` : "<receiver>")}.${site.name}(\u2026)\`), which its design never narrates \u2014 the code twin of PORTAL_WRITE_SHORTCUT. A Portal may reach a data component for READS only: route the ${called.effect === "write" ? "write" : "lifecycle change"} through an Orchestrator that owns the workflow.`,
26628
+ impl.id,
26629
+ draftAt(fromPath) || draftAt(toPath),
26630
+ void 0,
26631
+ { at }
26632
+ );
26633
+ }
26634
+ }
26635
+ }
26636
+ }
26637
+ }
26638
+ }
26639
+ const runtime = graph2.importsOf(fromPath);
26640
+ for (const sites of Object.values(facts.functionCallSites ?? {})) {
26641
+ for (const site of sites) {
26642
+ for (const { toPath, typeOnly } of typedLandings(site, fromPath)) {
26643
+ if (!typeOnly || runtime.has(toPath)) continue;
26644
+ const called = calledAt(toPath, site.name);
26645
+ if (called.length === 0) continue;
26646
+ const toComponents = realization.componentsAt(toPath);
26647
+ if (edgeJustified(fromComponents, toComponents)) continue;
26648
+ const edge = `${fromPath} -> ${toPath}`;
26649
+ if (reportedEdges.has(edge)) continue;
26650
+ reportedEdges.add(edge);
26651
+ ctx.addIssue(
26652
+ "warning",
26653
+ "UNDECLARED_DEPENDENCY",
26654
+ `"${fromPath}" (realizing ${fromComponents.map((c) => c.id).join(", ")}) calls ${called.map((c) => `${c.component.id}.${c.method.name}`).join(", ")} through a collaborator typed by a type-only import of "${toPath}" (realizing ${toComponents.map((c) => c.id).join(", ")}), but no declared dependsOn/owns edge justifies it \u2014 a call through an injected collaborator is runtime collaboration whatever the import's form. Declare the collaboration on the component that uses it, or route the cross-subsystem hop through the target's published surface.`,
26655
+ realization.implementationsAt(fromPath)[0]?.id,
26656
+ draftAt(fromPath) || draftAt(toPath),
26657
+ void 0,
26658
+ { at: edge }
26659
+ );
26660
+ }
26661
+ }
26662
+ }
26663
+ }
26664
+ const homes = /* @__PURE__ */ new Map();
26665
+ for (const impl of ctx.implementations) {
26666
+ const technologies = impl.technologies ?? [];
26667
+ if (technologies.length === 0) continue;
26668
+ const contract = ctx.interfaceMap.get(impl.contract);
26669
+ const owner = contract ? ctx.componentMap.get(contract.component) : void 0;
26670
+ if (!owner) continue;
26671
+ const members = /* @__PURE__ */ new Set([owner.id, ...owner.owns ?? []]);
26672
+ const facade = ownerOf4.get(owner.id);
26673
+ if (facade) members.add(facade.id);
26674
+ for (const technology of technologies) {
26675
+ for (const token of technologyTokens(technology)) {
26676
+ const key2 = token.toLowerCase();
26677
+ const entry = homes.get(key2) ?? { label: technologyName(technology), home: /* @__PURE__ */ new Set() };
26678
+ for (const id of members) entry.home.add(id);
26679
+ homes.set(key2, entry);
26680
+ }
26681
+ }
26682
+ }
26683
+ if (homes.size > 0) {
26684
+ for (const file of mappedPaths) {
26685
+ const facts = code.factsAt(file);
26686
+ if (!facts) continue;
26687
+ const atFile = realization.componentsAt(file);
26688
+ const specifiers = /* @__PURE__ */ new Set([...facts.imports, ...Object.values(facts.typeOnlyBindings ?? {})]);
26689
+ const reported = /* @__PURE__ */ new Set();
26690
+ for (const specifier of specifiers) {
26691
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:")) continue;
26692
+ if (resolveImport(file, specifier, code.paths, code.packages)) continue;
26693
+ const parts = specifier.split("/");
26694
+ const pkg = (specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]).toLowerCase();
26695
+ const tech = homes.get(pkg);
26696
+ if (!tech || reported.has(pkg) || atFile.some((c) => tech.home.has(c.id))) continue;
26697
+ reported.add(pkg);
26698
+ ctx.addIssue(
26699
+ "warning",
26700
+ "TECH_LEAKAGE_IN_CODE",
26701
+ `"${file}" (realizing ${atFile.map((c) => c.id).join(", ")}) imports "${specifier}", the package of technology "${tech.label}", whose home is ${[...tech.home].join(", ")} \u2014 the code twin of TECH_LEAKAGE. Move the vendor call behind the component that binds it, or bind the technology where it is really used.`,
26702
+ realization.implementationsAt(file)[0]?.id,
26703
+ draftAt(file),
26704
+ void 0,
26705
+ { at: `${file} -> ${pkg}` }
26706
+ );
26707
+ }
26708
+ }
26709
+ }
25943
26710
  }
25944
26711
  };
25945
26712
 
@@ -26613,16 +27380,16 @@ var paramConformanceRule = {
26613
27380
  { at: method.name, covers: undeclared.map((found) => found.unit) }
26614
27381
  );
26615
27382
  }
26616
- const renamed = agreed(judgements, "renamed");
26617
- if (renamed.length > 0) {
27383
+ const renamed2 = agreed(judgements, "renamed");
27384
+ if (renamed2.length > 0) {
26618
27385
  ctx.addIssue(
26619
27386
  "warning",
26620
27387
  "PARAM_NAME_MISMATCH",
26621
- `Method "${method.name}" of contract "${implementation.contract}" and ${subject} agree on position and type but not on name for ${renamed.length} parameter(s) \u2014 ${renamed.map((f) => f.told).join("; ")}. The contract, the ERD and every brief carry one word and the code answers to another, which is a rename nobody recorded rather than a different argument \u2014 the declared type agreeing is what says so. Rename one side to the other.`,
27388
+ `Method "${method.name}" of contract "${implementation.contract}" and ${subject} agree on position and type but not on name for ${renamed2.length} parameter(s) \u2014 ${renamed2.map((f) => f.told).join("; ")}. The contract, the ERD and every brief carry one word and the code answers to another, which is a rename nobody recorded rather than a different argument \u2014 the declared type agreeing is what says so. Rename one side to the other.`,
26622
27389
  implementation.id,
26623
27390
  draftContext4,
26624
27391
  void 0,
26625
- { at: method.name, covers: renamed.map((found) => found.unit) }
27392
+ { at: method.name, covers: renamed2.map((found) => found.unit) }
26626
27393
  );
26627
27394
  }
26628
27395
  const disagreed = agreed(judgements, "optionality");
@@ -28348,6 +29115,7 @@ var SDD_RULES = [
28348
29115
  invokedByDescriptionRule,
28349
29116
  reachabilityRule,
28350
29117
  networkBoundariesRule,
29118
+ adapterTransportRule,
28351
29119
  unusedTypesRule,
28352
29120
  // The declaration (spec-scoped, refused at the write boundary) before the
28353
29121
  // round-trip consequences it enables.
@@ -28697,7 +29465,9 @@ function resolveTypeScript(projectRoot) {
28697
29465
  for (const base of bases) {
28698
29466
  try {
28699
29467
  const req = (0, import_module2.createRequire)(base);
28700
- ts = req("typescript");
29468
+ const candidate = req("typescript");
29469
+ if (typeof candidate?.createSourceFile !== "function") continue;
29470
+ ts = candidate;
28701
29471
  break;
28702
29472
  } catch {
28703
29473
  }
@@ -30703,19 +31473,19 @@ function measure(candidate) {
30703
31473
  const config = loadProjectConfig();
30704
31474
  if (!config) throw new Error("Not inside a wairon project: a pack impact is measured on a project \u2014 run `wairon init` first.");
30705
31475
  const entry = candidate.entry;
30706
- const remove2 = candidate.remove === true;
30707
- let next = entry !== void 0 ? withPack(config, entry, remove2) : config;
31476
+ const remove3 = candidate.remove === true;
31477
+ let next = entry !== void 0 ? withPack(config, entry, remove3) : config;
30708
31478
  if (candidate.projectType !== void 0) next = { ...next, projectType: candidate.projectType };
30709
31479
  const current2 = entry !== void 0 ? (config.extensions?.packs ?? []).find((e) => sameEntry(e, entry)) : void 0;
30710
31480
  let manifest = null;
30711
- if (candidate.manifest && !remove2 || entry === void 0) {
31481
+ if (candidate.manifest && !remove3 || entry === void 0) {
30712
31482
  manifest = candidate.manifest ?? null;
30713
31483
  } else {
30714
- const measured = remove2 ? current2 ?? entry : entry;
31484
+ const measured = remove3 ? current2 ?? entry : entry;
30715
31485
  manifest = manifestOf(measured);
30716
31486
  if (!manifest) throw new Error(`Cannot measure the pack: ${unresolvedReason(config, measured)}`);
30717
31487
  }
30718
- const previous = !remove2 && current2 !== void 0 ? manifestOf(current2) : null;
31488
+ const previous = !remove3 && current2 !== void 0 ? manifestOf(current2) : null;
30719
31489
  const baseline = defaultsBaseline();
30720
31490
  const doctrine2 = manifest ? doctrineChanges(manifest, baseline) : [];
30721
31491
  const previousDoctrine = previous ? doctrineChanges(previous, baseline) : void 0;
@@ -30731,11 +31501,11 @@ function measure(candidate) {
30731
31501
  return {
30732
31502
  pack: manifest?.name ?? (entry !== void 0 ? entryName(entry) : `projectType ${next.projectType}`),
30733
31503
  ...manifest?.version ? { version: manifest.version } : {},
30734
- ...!remove2 && current2 !== void 0 && replaces ? { replaces } : {},
30735
- direction: remove2 ? "remove" : "apply",
31504
+ ...!remove3 && current2 !== void 0 && replaces ? { replaces } : {},
31505
+ direction: remove3 ? "remove" : "apply",
30736
31506
  doctrine: doctrine2,
30737
- ...previousDoctrine && !remove2 ? { previousDoctrine } : {},
30738
- governing: governingProfiles(manifest, remove2 ? config : next),
31507
+ ...previousDoctrine && !remove3 ? { previousDoctrine } : {},
31508
+ governing: governingProfiles(manifest, remove3 ? config : next),
30739
31509
  findings: findingChanges(before, after),
30740
31510
  before: totals(config, before),
30741
31511
  after: totals(next, after)
@@ -30749,9 +31519,11 @@ function doctrine(manifest) {
30749
31519
 
30750
31520
  // src/core/rules/gate-identity.ts
30751
31521
  var crypto4 = __toESM(require("crypto"));
30752
- var GATE_ALGORITHM = "sha256+design+doctrine+inputs+members";
31522
+ var GATE_ALGORITHM = "sha256+design-2+doctrine+inputs+members";
31523
+ var EARLIER_DESIGN_GATE_ALGORITHM = "sha256+design+doctrine+inputs+members";
30753
31524
  var CONTENT_GATE_ALGORITHM = "sha256+content+doctrine+inputs+members";
30754
- var DESIGN_CONTENT_ALGORITHM = "sha256-design";
31525
+ var DESIGN_CONTENT_ALGORITHM = "sha256-design-2";
31526
+ var EARLIER_DESIGN_CONTENT_ALGORITHM = "sha256-design";
30755
31527
  var byKey2 = (items, key2) => [...items].sort((a, b) => compareOrdinal(key2(a), key2(b)));
30756
31528
  function ruleIdentities(rules) {
30757
31529
  return byKey2(
@@ -30841,7 +31613,8 @@ function computeGateIdentity(content, doctrine2, builtinRules, inputs, gate, mem
30841
31613
  members: byKey2(Object.entries(members), ([alias]) => alias).map(([alias, subject]) => ({ alias, subject }))
30842
31614
  };
30843
31615
  const digest3 = crypto4.createHash("sha256").update(canonicalize(payload)).digest("hex");
30844
- return { algorithm: content.algorithm === DESIGN_CONTENT_ALGORITHM ? GATE_ALGORITHM : CONTENT_GATE_ALGORITHM, digest: digest3 };
31616
+ const algorithm = content.algorithm === DESIGN_CONTENT_ALGORITHM ? GATE_ALGORITHM : content.algorithm === EARLIER_DESIGN_CONTENT_ALGORITHM ? EARLIER_DESIGN_GATE_ALGORITHM : CONTENT_GATE_ALGORITHM;
31617
+ return { algorithm, digest: digest3 };
30845
31618
  }
30846
31619
  function analyzerDigest(builtinRules, gate) {
30847
31620
  const codeCodes = codeJudgedCodes(builtinRules);
@@ -31140,7 +31913,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
31140
31913
  }
31141
31914
  }
31142
31915
  function ownReachModel(options) {
31143
- return runOwnersGate(options ?? {}, options?.projectType ?? "backend", true).reach ?? { scope: "own", networks: [], verbs: [], calls: [] };
31916
+ return runOwnersGate(options ?? {}, options?.projectType ?? "backend", true).reach ?? { scope: "own", networks: [], verbs: [], calls: [], placements: [], topics: [] };
31144
31917
  }
31145
31918
  function reachModel2(options) {
31146
31919
  return reachModel(options);
@@ -31393,7 +32166,16 @@ function computeGateStateId() {
31393
32166
  const record = approvalRecord(getProjectRoot());
31394
32167
  if (!record?.stateId || record.stateId.algorithm === current2.algorithm) return current2;
31395
32168
  const asRecorded = computeGateIdentity(computeOwnStateId(), extensions, builtin, inputs, gate, members);
31396
- return record.stateId.algorithm === asRecorded.algorithm ? { ...current2, asRecorded } : current2;
32169
+ if (record.stateId.algorithm === asRecorded.algorithm) return { ...current2, asRecorded };
32170
+ const asApproved = computeGateIdentity(
32171
+ computeOwnDesignStateIdAsRecorded(approvedStatuses(record)),
32172
+ extensions,
32173
+ builtin,
32174
+ inputs,
32175
+ gate,
32176
+ members
32177
+ );
32178
+ return record.stateId.algorithm === asApproved.algorithm ? { ...current2, asRecorded: asApproved } : current2;
31397
32179
  }
31398
32180
  var NEVER_SUBJECT = "never";
31399
32181
  function directMemberSubjects() {
@@ -31732,7 +32514,7 @@ function generateSequenceDiagram(componentId, methodName, options) {
31732
32514
  lines.push("sequenceDiagram");
31733
32515
  lines.push(" autonumber");
31734
32516
  const declared = /* @__PURE__ */ new Set();
31735
- const declare2 = (comp) => {
32517
+ const declare3 = (comp) => {
31736
32518
  const pid = ids.idFor(comp.id);
31737
32519
  if (!declared.has(pid)) {
31738
32520
  declared.add(pid);
@@ -31740,7 +32522,7 @@ function generateSequenceDiagram(componentId, methodName, options) {
31740
32522
  }
31741
32523
  return pid;
31742
32524
  };
31743
- const caller = declare2(entry);
32525
+ const caller = declare3(entry);
31744
32526
  const walk2 = (comp, method, depth, stack) => {
31745
32527
  const key2 = `${comp.id}#${method}`;
31746
32528
  if (stack.has(key2)) {
@@ -31826,12 +32608,12 @@ function generateSequenceDiagram(componentId, methodName, options) {
31826
32608
  lines.push(` Note over ${selfId}: ${escapeLabel(`dispatches via unknown "${step.targetComponent}"`)}`);
31827
32609
  break;
31828
32610
  }
31829
- const portalId = declare2(portal);
32611
+ const portalId = declare3(portal);
31830
32612
  lines.push(step.detach ? ` ${selfId}-)${portalId}: ${escapeLabel(`\u27E8${step.capability ?? "?"}\u27E9`)} \u2014 detached` : ` ${selfId}->>${portalId}: ${escapeLabel(`\u27E8${step.capability ?? "?"}\u27E9`)}`);
31831
32613
  const binding = portal.dispatch?.find((b) => b.capability === step.capability);
31832
32614
  const server = binding ? componentById.get(binding.component) : void 0;
31833
32615
  if (binding && server) {
31834
- const serverId = declare2(server);
32616
+ const serverId = declare3(server);
31835
32617
  const expandable = depth < maxDepth && !!findMethodImpl(server.id, binding.method) && server.id !== comp.id;
31836
32618
  if (expandable) {
31837
32619
  lines.push(` ${portalId}->>+${serverId}: ${escapeLabel(binding.method)}()`);
@@ -31850,7 +32632,7 @@ function generateSequenceDiagram(componentId, methodName, options) {
31850
32632
  lines.push(` Note over ${selfId}: ${escapeLabel(`registers callback on unknown "${step.targetComponent}"`)}`);
31851
32633
  break;
31852
32634
  }
31853
- const regTargetId = declare2(regTarget);
32635
+ const regTargetId = declare3(regTarget);
31854
32636
  lines.push(` ${selfId}--)${regTargetId}: ${escapeLabel(`register ${step.targetMethod}()`)}`);
31855
32637
  break;
31856
32638
  }
@@ -31861,7 +32643,7 @@ function generateSequenceDiagram(componentId, methodName, options) {
31861
32643
  lines.push(` Note over ${selfId}: ${escapeLabel(`calls unknown "${step.targetComponent}"`)}`);
31862
32644
  break;
31863
32645
  }
31864
- const targetId = declare2(target);
32646
+ const targetId = declare3(target);
31865
32647
  const expandable = depth < maxDepth && !!findMethodImpl(target.id, step.targetMethod) && target.id !== comp.id;
31866
32648
  if (step.detach) {
31867
32649
  lines.push(` ${selfId}-)${targetId}: ${escapeLabel(step.targetMethod)}() \u2014 detached`);
@@ -31955,10 +32737,6 @@ function diagramSetIndex(files, systemName) {
31955
32737
  // src/core/project-family.ts
31956
32738
  var path20 = __toESM(require("path"));
31957
32739
  var memo = /* @__PURE__ */ new WeakMap();
31958
- function dirKey(dir) {
31959
- const resolved = path20.resolve(dir);
31960
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
31961
- }
31962
32740
  function makeNodes(roots) {
31963
32741
  return roots.map((root) => {
31964
32742
  const id = root.config ? effectiveProjectId(root.config) : null;
@@ -32018,18 +32796,9 @@ function identityProblems(nodes) {
32018
32796
  }
32019
32797
  return problems;
32020
32798
  }
32021
- function relationOf(consumer, producer) {
32022
- if (consumer.parent === producer.namespace) return "parent";
32023
- if (producer.parent === consumer.namespace) return "member";
32024
- if (consumer.parent !== void 0 && consumer.parent === producer.parent) return "sibling";
32025
- return "family";
32026
- }
32027
- function familyExternal(alias, project3, consumer, producer) {
32028
- return { alias, project: project3, sourceKind: "family", relation: relationOf(consumer, producer), producer: producer.namespace, directory: producer.directory, audience: "project", role: "external" };
32029
- }
32030
32799
  function bindExternals(node, root, nodes) {
32031
32800
  if (!root.config) return [];
32032
- return [...declaredExternals(root.config).map((decl) => bindExternal(decl, node, nodes)), ...bindReferencedMembers(node, root)];
32801
+ return [...declaredExternals(root.config).map((decl) => bindDeclaredExternal(decl, node, nodes)), ...bindReferencedMembers(node, root)];
32033
32802
  }
32034
32803
  function bindReferencedMembers(node, root) {
32035
32804
  const out = [];
@@ -32054,25 +32823,6 @@ function bindReferencedMembers(node, root) {
32054
32823
  }
32055
32824
  return out;
32056
32825
  }
32057
- function bindExternal(decl, node, nodes) {
32058
- const unresolved = (problem) => ({ alias: decl.alias, project: decl.project, sourceKind: "unresolved", audience: "instance", problem, role: "external" });
32059
- if (decl.problem) return unresolved(decl.problem);
32060
- if (decl.sourceHosted !== void 0) return { alias: decl.alias, project: decl.project, sourceKind: "hosted", hosted: decl.sourceHosted, audience: "instance", role: "external" };
32061
- if (decl.sourceGit !== void 0) return { alias: decl.alias, project: decl.project, sourceKind: "git", audience: "instance", role: "external" };
32062
- if (decl.sourcePath !== void 0) {
32063
- const directory = path20.resolve(node.directory, decl.sourcePath);
32064
- const found = nodes.find((n) => dirKey(n.directory) === dirKey(directory));
32065
- if (!found) return { alias: decl.alias, project: decl.project, sourceKind: "path", directory, audience: "instance", role: "external" };
32066
- if (found.id !== decl.project) {
32067
- return unresolved(`the family project at source.path "${decl.sourcePath}" answers to ${found.id === void 0 ? "no id" : `"${found.id}"`}, not "${decl.project}"`);
32068
- }
32069
- return familyExternal(decl.alias, decl.project, node, found);
32070
- }
32071
- const holders = nodes.filter((n) => n.id === decl.project && n !== node);
32072
- if (holders.length === 0) return unresolved(`no project of the family answers to "${decl.project}", and no source.path says where to find it`);
32073
- if (holders.length > 1) return unresolved(`${holders.length} projects of the family answer to "${decl.project}" (${holders.map((n) => `"${n.namespace || "(the bound root)"}"`).join(", ")}), so the declaration could mean either`);
32074
- return familyExternal(decl.alias, decl.project, node, holders[0]);
32075
- }
32076
32826
  function crossReferences(family, specs) {
32077
32827
  const out = [];
32078
32828
  const seen = /* @__PURE__ */ new Set();
@@ -32194,6 +32944,7 @@ function projectFamilyGraph() {
32194
32944
  var CONSUMABLE = /* @__PURE__ */ new Set(["Portal", "Observer"]);
32195
32945
  var IMPLEMENTABLE = /* @__PURE__ */ new Set(["Portal", "Adapter"]);
32196
32946
  function exportKindOf(comp, authored) {
32947
+ if (comp.componentType === "Portal" && comp.transport === "InProcess") return "InProcess";
32197
32948
  if (comp.componentType === "Portal" && comp.transport) return transportExportKind(comp.transport);
32198
32949
  return authored;
32199
32950
  }
@@ -32241,6 +32992,54 @@ function checkConsumable(entry, owner, problems) {
32241
32992
  detail: implemented ? `"${entry.publicName}" exports ${entry.componentType} "${entry.component}" for consumers to implement, but an extension point is backed only by a Portal (a webhook or callback contract) or the Adapter holding the port the producer calls` : `"${entry.publicName}" exports ${entry.componentType} "${entry.component}", which no caller across the boundary may reach \u2014 only a Portal (a gateway is a Portal variant) or an Observer can serve one (an Adapter only as an extension point consumers implement, with role implement)`
32242
32993
  });
32243
32994
  }
32995
+ function renamedInSource(world, table, request) {
32996
+ const wanted = localName2(request.interface ?? request.component ?? request.typeDef ?? "");
32997
+ if (!wanted) return void 0;
32998
+ const traced2 = (holder) => (holder.previousIds ?? []).some((p) => localName2(p) === wanted);
32999
+ for (const entry of table.entries) {
33000
+ if (request.typeDef !== void 0 && entry.kind === "type") {
33001
+ const holders = world.types.get(entry.typeDef ?? "") ?? [];
33002
+ if (holders.some(traced2)) return entry.publicName;
33003
+ }
33004
+ if (request.interface !== void 0 && entry.interface !== void 0) {
33005
+ const intf = world.interfaces.get(entry.interface);
33006
+ if (intf && traced2(intf)) return entry.publicName;
33007
+ }
33008
+ if (request.component !== void 0 && entry.kind === "component" && entry.component !== void 0) {
33009
+ const comp = world.components.get(entry.component);
33010
+ if (comp && traced2(comp)) return entry.publicName;
33011
+ }
33012
+ }
33013
+ return void 0;
33014
+ }
33015
+ function followProducerRenames(table, producerOf2) {
33016
+ for (const problem of table.problems) {
33017
+ if (problem.kind !== "invalid" || problem.source === void 0 || problem.renamedTo !== void 0) continue;
33018
+ const requested = /^re-exports "([^"]+)" from /.exec(problem.detail)?.[1];
33019
+ const producer = requested !== void 0 ? producerOf2(problem.source) : void 0;
33020
+ const renamedTo = producer ? publicRenameTrace(producer.table, producer.specs).get(requested) : void 0;
33021
+ if (renamedTo === void 0) continue;
33022
+ problem.renamedTo = renamedTo;
33023
+ problem.detail = problem.detail.replace(" \u2014 another project is reached only through its L0 exports", ` \u2014 it was renamed to "${renamedTo}" there (did you mean "${renamedTo}"?): re-export the new name`);
33024
+ }
33025
+ }
33026
+ function publicRenameTrace(table, specs) {
33027
+ const trace = /* @__PURE__ */ new Map();
33028
+ const byId = (list) => list.reduce((m, x) => m.set(x.id, [...m.get(x.id) ?? [], x]), /* @__PURE__ */ new Map());
33029
+ const components = byId(specs.components);
33030
+ const interfaces = byId(specs.interfaces);
33031
+ const types = byId(specs.types);
33032
+ for (const entry of table.entries) {
33033
+ const holders = entry.kind === "type" ? types.get(entry.typeDef ?? "") ?? [] : entry.interface !== void 0 ? interfaces.get(entry.interface) ?? [] : components.get(entry.component ?? "") ?? [];
33034
+ for (const holder of holders) {
33035
+ for (const former of holder.previousIds ?? []) {
33036
+ const name = localName2(former);
33037
+ if (name !== entry.publicName && !trace.has(name)) trace.set(name, entry.publicName);
33038
+ }
33039
+ }
33040
+ }
33041
+ return trace;
33042
+ }
32244
33043
  function findInSource(world, source, table, request) {
32245
33044
  if (request.typeDef) {
32246
33045
  const type = lookupType(world, request.typeDef, source.id);
@@ -32269,7 +33068,15 @@ function bindNamed(world, owner, source, sourceTable, request, publicName, inCyc
32269
33068
  const { found, item } = findInSource(world, source, sourceTable, request);
32270
33069
  const what = request.typeDef ? `type "${request.typeDef}"` : `component "${request.component}"${request.interface ? ` (interface "${request.interface}")` : ""}`;
32271
33070
  if (!item) {
32272
- problems.push({ kind: "invalid", owner, detail: `re-exports ${what} from "${source.id}", which has no such item` });
33071
+ const renamedTo = renamedInSource(world, sourceTable, request);
33072
+ problems.push({
33073
+ kind: "invalid",
33074
+ owner,
33075
+ ...request.as !== void 0 ? { publicName: request.as } : {},
33076
+ detail: `re-exports ${what} from "${source.id}", which has no such item${renamedTo ? ` \u2014 it was renamed to "${renamedTo}" there (did you mean "${renamedTo}"?)` : ""}`,
33077
+ ...renamedTo ? { renamedTo } : {},
33078
+ source: source.id
33079
+ });
32273
33080
  return void 0;
32274
33081
  }
32275
33082
  const name = publicName(item);
@@ -32336,7 +33143,7 @@ function subsystemCandidates(world, sub, tables, group, problems) {
32336
33143
  sub.id,
32337
33144
  source,
32338
33145
  sourceTable,
32339
- { component: pi.component, interface: pi.interface, typeDef: pi.typeDef },
33146
+ { component: pi.component, interface: pi.interface, typeDef: pi.typeDef, ...pi.as !== void 0 ? { as: pi.as } : {} },
32340
33147
  (item) => pi.as ?? localName2(pi.interface ?? item.typeDef ?? item.component ?? ""),
32341
33148
  group.has(source.id),
32342
33149
  problems
@@ -32542,7 +33349,14 @@ function bindFromProject(owner, e, source, table, index, problems) {
32542
33349
  const requested = named2 ? localName2(e.interface ?? e.component ?? e.typeDef) : void 0;
32543
33350
  const picked = named2 ? table.entries.filter((x) => x.publicName === requested) : table.entries;
32544
33351
  if (named2 && picked.length === 0) {
32545
- problems.push({ kind: "invalid", owner, publicName: e.as ?? e.id ?? requested, detail: `re-exports "${requested}" from ${source.label}, whose export table has no such public name \u2014 another project is reached only through its L0 exports` });
33352
+ const from = e.from ?? e.subsystem;
33353
+ problems.push({
33354
+ kind: "invalid",
33355
+ owner,
33356
+ publicName: e.as ?? e.id ?? requested,
33357
+ detail: `re-exports "${requested}" from ${source.label}, whose export table has no such public name \u2014 another project is reached only through its L0 exports`,
33358
+ ...from !== void 0 ? { source: from } : {}
33359
+ });
32546
33360
  return [];
32547
33361
  }
32548
33362
  const declared = e.audience ?? "instance";
@@ -32572,7 +33386,7 @@ function bindFromSubsystem(world, owner, e, subsystem, subsystemTables, index, p
32572
33386
  owner,
32573
33387
  subsystem,
32574
33388
  sourceTable,
32575
- { component: e.component, interface: e.interface, typeDef: e.typeDef },
33389
+ { component: e.component, interface: e.interface, typeDef: e.typeDef, ...(e.as ?? e.id) !== void 0 ? { as: e.as ?? e.id } : {} },
32576
33390
  // The default public name is the item's own LOCAL id: a member's L0 is
32577
33391
  // read with its ids keyed under its project, and its entry names the
32578
33392
  // item as the member wrote it.
@@ -32735,7 +33549,7 @@ function sameParams2(a, b) {
32735
33549
  const right = b ?? [];
32736
33550
  return left.length === right.length && left.every((p, i) => p.name === right[i].name && p.type === right[i].type && !!p.optional === !!right[i].optional);
32737
33551
  }
32738
- function tablesOf(interfaces, components, types, references) {
33552
+ function tablesOf(interfaces, components, types, references, exportMethods = /* @__PURE__ */ new Map()) {
32739
33553
  const methodsOf = /* @__PURE__ */ new Map();
32740
33554
  for (const intf of interfaces) methodsOf.set(intf.component, [...methodsOf.get(intf.component) ?? [], ...intf.methods]);
32741
33555
  const boundTypes = /* @__PURE__ */ new Map();
@@ -32743,7 +33557,7 @@ function tablesOf(interfaces, components, types, references) {
32743
33557
  if (ref.position !== "type" || ref.binding === "outside" || ref.binding === "unresolved") continue;
32744
33558
  boundTypes.set(`${ref.specId}|${ref.authored}`, ref.resolved);
32745
33559
  }
32746
- return { methodsOf, componentKeys: new Set(components.map((c) => c.id)), types, boundTypes };
33560
+ return { methodsOf, componentKeys: new Set(components.map((c) => c.id)), types, boundTypes, exportMethods };
32747
33561
  }
32748
33562
  function methodReading(tables, intf, value) {
32749
33563
  const dot = value.lastIndexOf(".");
@@ -32759,6 +33573,11 @@ function methodReading(tables, intf, value) {
32759
33573
  }
32760
33574
  return null;
32761
33575
  }
33576
+ function exportReading(tables, intf, value) {
33577
+ if (!value.includes("::")) return null;
33578
+ const method = tables.exportMethods.get(`${namespaceOf2(intf.id)}|${value}`);
33579
+ return method ? { target: value, method } : null;
33580
+ }
32762
33581
  function typeReading(tables, intf, value) {
32763
33582
  const bound2 = value.includes("::") ? tables.boundTypes.get(`${intf.id}|${value}`) : void 0;
32764
33583
  const named2 = bound2 !== void 0 ? tables.types.filter((t) => t.id === bound2) : tables.types.filter((t) => t.id === value || typeMatchesRef(t, value));
@@ -32783,7 +33602,9 @@ function restatedFact(base, stored, params, returns) {
32783
33602
  function resolveSourced(tables, intf, stored, facts) {
32784
33603
  const source = stored.signatureFrom;
32785
33604
  const base = { interfaceId: intf.id, component: intf.component, method: stored.name, source };
32786
- const hit = methodReading(tables, intf, source);
33605
+ const local = methodReading(tables, intf, source);
33606
+ const external = local ? null : exportReading(tables, intf, source);
33607
+ const hit = local ?? external;
32787
33608
  const { signature, other } = typeReading(tables, intf, source);
32788
33609
  if (hit && signature) {
32789
33610
  facts.sources.push({ ...base, outcome: "ambiguous", candidates: [hit.target, typeTarget(signature)] });
@@ -32797,7 +33618,7 @@ function resolveSourced(tables, intf, stored, facts) {
32797
33618
  facts.sources.push({ ...base, form: "method", target: hit.target, outcome: "chained", detail: hit.method.signatureFrom });
32798
33619
  return unresolvedMethod(stored);
32799
33620
  }
32800
- const form = hit ? "method" : "signature";
33621
+ const form = external ? "export" : hit ? "method" : "signature";
32801
33622
  const target = hit ? hit.target : typeTarget(signature);
32802
33623
  const params = hit ? hit.method.params : signature.params;
32803
33624
  const returns = hit ? hit.method.returns : signature.returns ?? "unknown";
@@ -32816,10 +33637,10 @@ function withDerivedText(method, stored, specId, kind, facts) {
32816
33637
  if (stored !== void 0 && stored !== derived) facts.staleTexts.push({ specId, kind, method: method.name, stored, derived });
32817
33638
  return { ...method, signature: derived };
32818
33639
  }
32819
- function resolveTree(interfaces, components, types, references = []) {
33640
+ function resolveTree(interfaces, components, types, references = [], exportMethods = /* @__PURE__ */ new Map()) {
32820
33641
  const intfs = interfaces.map((i) => ({ ...i, methods: i.methods.map((m) => ({ ...m })) }));
32821
33642
  const typeCopies = types.map((t) => ({ ...t, methods: t.methods.map((m) => ({ ...m })) }));
32822
- const tables = tablesOf(intfs, [...components], typeCopies, references);
33643
+ const tables = tablesOf(intfs, [...components], typeCopies, references, exportMethods);
32823
33644
  const facts = emptySignatureFacts();
32824
33645
  const sourced = intfs.map((intf) => ({
32825
33646
  ...intf,
@@ -32871,6 +33692,13 @@ function assertSpecWritesPermitted(owners, what) {
32871
33692
  `SubsystemWriteDenied: ${what} would change ${denied.length === 1 ? "a spec" : "specs"} this request may not change \u2014 ${denied.join("; ")}. Nothing was written.`
32872
33693
  );
32873
33694
  }
33695
+ var TOLERATED_KEYS = /* @__PURE__ */ new Set(["schemaVersion"]);
33696
+ var UNKNOWN_KEY_HINTS = {
33697
+ "system.exports": "The L0 export table is `publicInterfaces` (sdd_set_public_interfaces writes it).",
33698
+ "system.export": "The L0 export table is `publicInterfaces` (sdd_set_public_interfaces writes it).",
33699
+ "subsystem.exports": "A subsystem publishes through `publicInterfaces`.",
33700
+ "component.type": "A component's stereotype is `componentType`."
33701
+ };
32874
33702
  function unknownKeysIn(raw, parsed, at, out) {
32875
33703
  if (Array.isArray(raw)) {
32876
33704
  if (!Array.isArray(parsed) || parsed.length !== raw.length) return;
@@ -33165,11 +33993,11 @@ function inspectChainedRoots(rootDir = getProjectRoot()) {
33165
33993
  continue;
33166
33994
  }
33167
33995
  if (member.form === "members" && !projectContentAt(childDir)) continue;
33168
- const dirKey3 = chainDirKey(childDir);
33169
- if (listed.has(dirKey3)) continue;
33170
- listed.add(dirKey3);
33996
+ const dirKey2 = chainDirKey(childDir);
33997
+ if (listed.has(dirKey2)) continue;
33998
+ listed.add(dirKey2);
33171
33999
  inspection.roots.push(path21.relative(root, childDir).split(path21.sep).join("/"));
33172
- walk2(childDir, key2, /* @__PURE__ */ new Set([...ancestors, dirKey3]), depth + 1);
34000
+ walk2(childDir, key2, /* @__PURE__ */ new Set([...ancestors, dirKey2]), depth + 1);
33173
34001
  }
33174
34002
  };
33175
34003
  walk2(root, "", /* @__PURE__ */ new Set([chainDirKey(root)]), 0);
@@ -33592,6 +34420,70 @@ function keyedSystem(system, key2, subsystems) {
33592
34420
  })
33593
34421
  };
33594
34422
  }
34423
+ function referrerSpelling(text3, rename) {
34424
+ if (text3 === void 0) return void 0;
34425
+ const parsed = parseTypeExpression(text3, "returns");
34426
+ if (!parsed.expression) return text3;
34427
+ const walk2 = (expr) => {
34428
+ const args = expr.args.map(walk2);
34429
+ return (expr.form === "named" || expr.form === "applied") && !isTypeVocabulary(expr.name) ? { ...expr, name: rename(expr.name), args } : { ...expr, args };
34430
+ };
34431
+ return canonicalTypeText(walk2(parsed.expression));
34432
+ }
34433
+ function spelledForReferrer(method, rename) {
34434
+ const { signatureFrom: _source, ...rest } = method;
34435
+ return {
34436
+ ...rest,
34437
+ returns: referrerSpelling(method.returns, rename) ?? method.returns,
34438
+ ...method.params ? { params: method.params.map((p) => ({ ...p, type: referrerSpelling(p.type, rename) ?? p.type })) } : {}
34439
+ };
34440
+ }
34441
+ function exportMethodsOf(raws) {
34442
+ const out = /* @__PURE__ */ new Map();
34443
+ const byKey3 = new Map(raws.map((r) => [r.record.namespace, r]));
34444
+ const last = (id) => id.split("::").pop().split(".").pop();
34445
+ for (const raw of raws) {
34446
+ const ns = raw.record.namespace;
34447
+ for (const [alias, key2] of raw.record.aliases) {
34448
+ const producer = byKey3.get(key2);
34449
+ const table = producer?.record.exports;
34450
+ if (!producer || !table) continue;
34451
+ const typeNames = /* @__PURE__ */ new Map();
34452
+ for (const e of table.entries) if (e.kind === "type" && e.typeDef) typeNames.set(last(e.typeDef), e.publicName);
34453
+ const rename = (name) => `${alias}::${typeNames.get(last(name)) ?? last(name)}`;
34454
+ for (const entry of table.entries) {
34455
+ if (entry.kind !== "component" || !entry.component) continue;
34456
+ const contracts = producer.index.interfaces.filter((i) => entry.interface ? i.id === entry.interface : i.component === entry.component);
34457
+ for (const method of contracts.flatMap((i) => i.methods)) {
34458
+ if (method.signatureFrom !== void 0) continue;
34459
+ out.set(`${ns}|${alias}::${entry.publicName}.${method.name}`, spelledForReferrer(method, rename));
34460
+ }
34461
+ }
34462
+ }
34463
+ const config = raw.record.config;
34464
+ if (!config) continue;
34465
+ for (const declared of declaredExternals(config)) {
34466
+ if (declared.problem || raw.record.aliases.has(declared.alias)) continue;
34467
+ let snapshot;
34468
+ try {
34469
+ const file = path21.join(aiPathsAt(raw.dir).root(), "externals", `${declared.alias}.yaml`);
34470
+ if (!fs15.existsSync(file)) continue;
34471
+ snapshot = SurfaceSnapshotSchema.parse(readYamlFile(file));
34472
+ } catch {
34473
+ continue;
34474
+ }
34475
+ const publicOf = new Map((snapshot.exportedTypes ?? []).map((t) => [t.type, t.id]));
34476
+ const closure = new Set(snapshot.types.map((t) => t.id));
34477
+ const rename = (name) => closure.has(name) ? `${declared.alias}::${publicOf.get(name) ?? last(name)}` : name;
34478
+ for (const entry of snapshot.interfaces) {
34479
+ for (const method of entry.methods) {
34480
+ out.set(`${ns}|${declared.alias}::${entry.id}.${method.name}`, spelledForReferrer(method, rename));
34481
+ }
34482
+ }
34483
+ }
34484
+ }
34485
+ return out;
34486
+ }
33595
34487
  function resolveFamilyTables(raws) {
33596
34488
  const byKey3 = new Map(raws.map((r) => [r.record.namespace, r]));
33597
34489
  const done = /* @__PURE__ */ new Set();
@@ -33621,6 +34513,11 @@ function resolveFamilyTables(raws) {
33621
34513
  producerTables,
33622
34514
  audienceOf
33623
34515
  });
34516
+ followProducerRenames(raw.record.exports, (source) => {
34517
+ const key3 = raw.record.aliases.get(source);
34518
+ const producer = key3 !== void 0 ? byKey3.get(key3) : void 0;
34519
+ return producer?.record.exports ? { table: producer.record.exports, specs: producer.index } : void 0;
34520
+ });
33624
34521
  done.add(key2);
33625
34522
  return raw.record.exports;
33626
34523
  } finally {
@@ -34063,8 +34960,8 @@ function publicInterfaceKey(o) {
34063
34960
  if (from || typeDef) {
34064
34961
  const item = typeDef ? `type ${typeDef}` : str(o.component) ?? "*";
34065
34962
  const narrowed2 = str(o.interface) ? `.${String(o.interface)}` : "";
34066
- const renamed = str(o.as) ? ` as ${String(o.as)}` : "";
34067
- return `${from ? `${from}:` : ""}${item}${narrowed2}${renamed}`;
34963
+ const renamed2 = str(o.as) ? ` as ${String(o.as)}` : "";
34964
+ return `${from ? `${from}:` : ""}${item}${narrowed2}${renamed2}`;
34068
34965
  }
34069
34966
  const component = str(o.component);
34070
34967
  if (component) return str(o.interface) ? `${component}.${String(o.interface)}` : component;
@@ -34517,7 +35414,7 @@ var SpecWorkspace = class {
34517
35414
  index.retiredReach.push(...raw.index.retiredReach.map((fact) => ({ ...fact, specId: keyIn(raw.record.namespace, fact.specId) })));
34518
35415
  }
34519
35416
  readRetiredReachAcrossSpecs(index);
34520
- const signatures = resolveTree(index.interfaces, index.components, index.types, raws.flatMap((r) => r.record.authoredReferences));
35417
+ const signatures = resolveTree(index.interfaces, index.components, index.types, raws.flatMap((r) => r.record.authoredReferences), exportMethodsOf(raws));
34521
35418
  index.interfaces = signatures.interfaces;
34522
35419
  index.types = signatures.types;
34523
35420
  index.signatures = {
@@ -34535,6 +35432,7 @@ var SpecWorkspace = class {
34535
35432
  readRoot(dir, parent, member, ancestors, maxDepth, depth, raws, taken) {
34536
35433
  const config = configAt(dir);
34537
35434
  this.scanVisitedConfigFiles.push(aiPathsAt(dir).projectConfig());
35435
+ if (!parent && config) this.reportUnknownConfigKeys(aiPathsAt(dir).projectConfig(), rawConfigAt(dir), config);
34538
35436
  const id = config ? effectiveProjectId(config) : null;
34539
35437
  const aliasPath = parent && member ? keyIn(parent.aliasPath, member.alias) : "";
34540
35438
  const key2 = !parent ? "" : id && !taken.has(id) ? id : aliasPath;
@@ -34639,6 +35537,7 @@ var SpecWorkspace = class {
34639
35537
  const doc = readSpecFile(systemYaml);
34640
35538
  if (doc && typeof doc === "object") raw.index.retiredReach.push(...readRetiredReachForms("system", doc));
34641
35539
  raw.rawSystem = SystemSpecSchema.parse(doc);
35540
+ this.reportUnknownKeys(systemYaml, "system", doc, raw.rawSystem, keyIn(raw.record.namespace, "system"));
34642
35541
  } catch {
34643
35542
  raw.rawSystem = null;
34644
35543
  }
@@ -34820,6 +35719,42 @@ var SpecWorkspace = class {
34820
35719
  this.memberProblem(raw, "kind-mismatch", decl.alias, `the part "${decl.alias}" says it is a part of "${partOf.project}", but "${ownId}" declares it \u2014 correct \`partOf.project\` to "${ownId}" ${whereToEdit(decl, dir)}`);
34821
35720
  }
34822
35721
  }
35722
+ /**
35723
+ * A spec file's keys its schema does not know — which the parse drops, so
35724
+ * they would mean nothing without a word — reported as UNKNOWN_SPEC_KEY,
35725
+ * once per file. A retired reachability form is read compatibly and
35726
+ * reported by its own rule, so a component's `mounts` is no unknown key.
35727
+ */
35728
+ reportUnknownKeys(file, kind, doc, parsed, specId) {
35729
+ const unknown = [];
35730
+ unknownKeysIn(doc, parsed, "", unknown);
35731
+ const keys = unknown.map((entry) => entry.slice(0, entry.indexOf(":"))).filter((key2) => !(kind === "component" && key2.startsWith("mounts")) && !TOLERATED_KEYS.has(key2));
35732
+ if (keys.length === 0) return;
35733
+ const hint = keys.map((key2) => UNKNOWN_KEY_HINTS[`${kind}.${key2}`]).filter(Boolean);
35734
+ this.loaderIssues.push({
35735
+ severity: "warning",
35736
+ code: "UNKNOWN_SPEC_KEY",
35737
+ message: `Spec file "${file}" holds ${keys.length === 1 ? "a key" : "keys"} its ${kind} schema does not know, which ${keys.length === 1 ? "is" : "are"} ignored: ${keys.join(", ")}. ${hint.length ? `${hint.join(" ")} ` : ""}Remove ${keys.length === 1 ? "it" : "them"}, or move what ${keys.length === 1 ? "it means" : "they mean"} into a field the schema has \u2014 a spec edited through the sdd_* tools never holds one.`,
35738
+ specId
35739
+ });
35740
+ }
35741
+ /**
35742
+ * The bound root's .wai/project.yaml keys its schema does not know, reported
35743
+ * as UNKNOWN_CONFIG_KEY (a warning): the configuration's parse drops them, so
35744
+ * a misspelt setting would otherwise silently configure nothing.
35745
+ */
35746
+ reportUnknownConfigKeys(file, raw, config) {
35747
+ if (!raw) return;
35748
+ const unknown = [];
35749
+ unknownKeysIn(raw, config, "", unknown);
35750
+ const keys = unknown.map((entry) => entry.slice(0, entry.indexOf(":")));
35751
+ if (keys.length === 0) return;
35752
+ this.loaderIssues.push({
35753
+ severity: "warning",
35754
+ code: "UNKNOWN_CONFIG_KEY",
35755
+ message: `"${file}" holds ${keys.length === 1 ? "a setting" : "settings"} wairon does not know, which ${keys.length === 1 ? "is" : "are"} ignored: ${keys.join(", ")}. Check the spelling against docs/cli.md (configuration) \u2014 a misspelt setting configures nothing.`
35756
+ });
35757
+ }
34823
35758
  /**
34824
35759
  * Read every spec document under a specs directory into a root's index, as
34825
35760
  * written: the root's own (`systemYaml` is its L0, skipped here) or a part's
@@ -34868,6 +35803,7 @@ var SpecWorkspace = class {
34868
35803
  if ("parentSystem" in doc) {
34869
35804
  detectedType = "subsystem";
34870
35805
  const parsed = SubsystemSpecSchema.parse(doc);
35806
+ this.reportUnknownKeys(file, "subsystem", doc, parsed, keyIn(key2, parsed.id));
34871
35807
  if (!(parsed.projectPath && parsed.projectPath.trim() !== "")) {
34872
35808
  index.retiredReach.push(...readRetiredReachForms("subsystem", doc));
34873
35809
  }
@@ -34877,17 +35813,20 @@ var SpecWorkspace = class {
34877
35813
  detectedType = "component";
34878
35814
  const facts = readRetiredReachForms("component", doc);
34879
35815
  const parsed = attachRetiredMounts(ComponentSpecSchema.parse(doc), doc);
35816
+ this.reportUnknownKeys(file, "component", doc, parsed, keyIn(key2, parsed.id));
34880
35817
  index.retiredReach.push(...facts);
34881
35818
  keep("component", index.components, parsed, file);
34882
35819
  } else if ("component" in doc) {
34883
35820
  detectedType = "interface";
34884
35821
  const facts = readRetiredReachForms("interface", doc);
34885
35822
  const parsed = InterfaceSpecSchema.parse(doc);
35823
+ this.reportUnknownKeys(file, "interface", doc, parsed, keyIn(key2, parsed.id));
34886
35824
  index.retiredReach.push(...facts);
34887
35825
  keep("interface", index.interfaces, parsed, file);
34888
35826
  } else if ("contract" in doc) {
34889
35827
  detectedType = "implementation";
34890
35828
  const parsed = ImplementationSpecSchema.parse(doc);
35829
+ this.reportUnknownKeys(file, "implementation", doc, parsed, keyIn(key2, parsed.id));
34891
35830
  const normalizeSourcePath = (p) => path21.relative(raw.dir, path21.resolve(baseDir, p)).replace(/\\/g, "/");
34892
35831
  if (parsed.sourcePath) parsed.sourcePath = normalizeSourcePath(parsed.sourcePath);
34893
35832
  for (const method of parsed.methods) {
@@ -34901,7 +35840,9 @@ var SpecWorkspace = class {
34901
35840
  keep("group", index.groups, GroupSpecSchema.parse(doc), file);
34902
35841
  } else {
34903
35842
  detectedType = "type";
34904
- keep("type", index.types, TypeSpecSchema.parse(doc), file);
35843
+ const parsed = TypeSpecSchema.parse(doc);
35844
+ this.reportUnknownKeys(file, "type", doc, parsed, keyIn(key2, parsed.id));
35845
+ keep("type", index.types, parsed, file);
34905
35846
  }
34906
35847
  }
34907
35848
  if (detectedType === "spec") {
@@ -36434,8 +37375,8 @@ var SpecWorkspace = class {
36434
37375
  if (kind !== "interface" && kind !== "type") return [];
36435
37376
  const read2 = kind === "interface" ? interfaceCanonicalTypes(merged) : typeCanonicalTypes(merged);
36436
37377
  Object.assign(merged, read2.spec);
36437
- const respelled = new Set(read2.respellings.map((r) => r.path));
36438
- const fromFile = this.scanAll().typeSpellings.respellings.filter((r) => r.specId === merged.id && r.kind === kind && !respelled.has(r.path) && typePositionText(merged, r.path) === r.stored);
37378
+ const respelled2 = new Set(read2.respellings.map((r) => r.path));
37379
+ const fromFile = this.scanAll().typeSpellings.respellings.filter((r) => r.specId === merged.id && r.kind === kind && !respelled2.has(r.path) && typePositionText(merged, r.path) === r.stored);
36439
37380
  return [...read2.respellings, ...fromFile];
36440
37381
  }
36441
37382
  /**
@@ -36562,6 +37503,18 @@ var SpecWorkspace = class {
36562
37503
  if (!result) {
36563
37504
  throw new Error(`Spec of kind "${kind}" with ID "${id}" does not exist. Define it first.`);
36564
37505
  }
37506
+ if (kind !== "system") {
37507
+ const stored = String(result.id ?? id);
37508
+ const member = this.getSubprojectPrefix(stored) ?? this.getSubprojectPrefix(id);
37509
+ if (member !== null && this.rootCovering(member)?.legacyMount === null) {
37510
+ const local = stored.startsWith(`${member}::`) ? stored.slice(member.length + 2) : stored;
37511
+ const dir = this.resolveSubprojectForNamespace(member);
37512
+ const folder = dir ? path21.relative(this.rootDir, dir).split(path21.sep).join("/") || "." : void 0;
37513
+ throw new Error(
37514
+ `chained-spec: "${stored}" lives in another project; update it from that project's own root. It belongs to the member project "${member}"${folder ? ` at ${folder}` : ""}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call sdd_update_spec ${kind} ${local} there. Nothing was written.`
37515
+ );
37516
+ }
37517
+ }
36565
37518
  if (kind === "system" && id && id !== "system" && id !== result.name) {
36566
37519
  throw new Error(
36567
37520
  `kind "system" targets the singleton L0 spec (system name "${result.name}") \u2014 pass id "system" or the system name, got "${id}". For a subsystem, use kind "subsystem".`
@@ -36821,12 +37774,12 @@ var SpecWorkspace = class {
36821
37774
  1,
36822
37775
  capture
36823
37776
  ));
36824
- const { action, remove: remove2, captureJumps, ...cleanStep } = deltaStep;
37777
+ const { action, remove: remove3, captureJumps, ...cleanStep } = deltaStep;
36825
37778
  steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
36826
37779
  } else {
36827
37780
  const idx = steps.findIndex((s) => s.stepNumber === stepNum);
36828
37781
  if (idx !== -1) {
36829
- const { action, remove: remove2, ...rawStep } = deltaStep;
37782
+ const { action, remove: remove3, ...rawStep } = deltaStep;
36830
37783
  const stored = steps[idx];
36831
37784
  const cleanStep = pinDeltaJumps(rawStep, stored[DELTA_JUMP_PINS]);
36832
37785
  const merged = withUnset(retargetByLabel(mergeElement(stored, cleanStep), cleanStep), deltaStep);
@@ -36834,7 +37787,7 @@ var SpecWorkspace = class {
36834
37787
  rebuilt[DELTA_JUMP_PINS] = cleanStep[DELTA_JUMP_PINS];
36835
37788
  steps[idx] = rebuilt;
36836
37789
  } else {
36837
- const { action, remove: remove2, ...cleanStep } = deltaStep;
37790
+ const { action, remove: remove3, ...cleanStep } = deltaStep;
36838
37791
  steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
36839
37792
  }
36840
37793
  }
@@ -36931,7 +37884,7 @@ var SpecWorkspace = class {
36931
37884
  merged.push(withUnset(deltaItem, deltaItem));
36932
37885
  }
36933
37886
  }
36934
- return merged.map(({ action, remove: remove2, ...item }) => item);
37887
+ return merged.map(({ action, remove: remove3, ...item }) => item);
36935
37888
  };
36936
37889
  const mergePublicInterfaces = (existing, delta2) => {
36937
37890
  const merged = [...existing];
@@ -36977,7 +37930,7 @@ var SpecWorkspace = class {
36977
37930
  merged.push(withUnset(deltaItem, deltaItem));
36978
37931
  }
36979
37932
  }
36980
- return merged.map((item) => item && typeof item === "object" ? (({ action, remove: remove2, ...rest }) => rest)(item) : item);
37933
+ return merged.map((item) => item && typeof item === "object" ? (({ action, remove: remove3, ...rest }) => rest)(item) : item);
36981
37934
  };
36982
37935
  const isIdentifiedArray = (field, a, b) => a.every((i) => identityKeyOf(field, i) !== null) && b.every((i) => identityKeyOf(field, i) !== null);
36983
37936
  const mergeDelta = (existing, delta2) => {
@@ -37965,6 +38918,9 @@ function setProjectType(projectType) {
37965
38918
  function describeProject(name, description) {
37966
38919
  projectConfigRepository.describeProject(name, description);
37967
38920
  }
38921
+ function setNetwork(network) {
38922
+ return projectConfigRepository.setNetwork(network);
38923
+ }
37968
38924
  function recordProfileSelection(selection) {
37969
38925
  projectConfigRepository.recordProfileSelection(selection);
37970
38926
  }
@@ -38167,7 +39123,7 @@ function resolveHosted(external) {
38167
39123
  const root = lookup2(external.hosted);
38168
39124
  return root === null ? unresolved(`the hosted producer \`${external.hosted}\` is unknown or outside this request's reach`) : { ...external, directory: path23.resolve(root) };
38169
39125
  }
38170
- function dirKey2(dir) {
39126
+ function dirKey(dir) {
38171
39127
  const resolved = path23.resolve(dir);
38172
39128
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
38173
39129
  }
@@ -38179,11 +39135,11 @@ function climb(bound2) {
38179
39135
  top = path23.resolve(parent.parentRoot);
38180
39136
  }
38181
39137
  const reach2 = getRequestParentReach();
38182
- const stoppedAtNarrowedCeiling = !!reach2?.narrowed && reach2.topRoot !== void 0 && dirKey2(top) === dirKey2(reach2.topRoot);
39138
+ const stoppedAtNarrowedCeiling = !!reach2?.narrowed && reach2.topRoot !== void 0 && dirKey(top) === dirKey(reach2.topRoot);
38183
39139
  return { top, whole: !(reach2 && !reach2.parentReach) && !stoppedAtNarrowedCeiling };
38184
39140
  }
38185
39141
  function consumerNode(family, bound2) {
38186
- return family.nodes.find((n) => dirKey2(n.directory) === dirKey2(bound2));
39142
+ return family.nodes.find((n) => dirKey(n.directory) === dirKey(bound2));
38187
39143
  }
38188
39144
  function gitDeclarationOf(config, external) {
38189
39145
  if (!config) return null;
@@ -38214,7 +39170,7 @@ function resolveGit(external, config, offline) {
38214
39170
  }
38215
39171
  var NOT_COMPARED_OFFLINE = "not compared offline \u2014 `wairon externals status` fetches it";
38216
39172
  function consumerConfig(bound2) {
38217
- return listProjectRoots().find((r) => dirKey2(r.directory) === dirKey2(bound2))?.config ?? null;
39173
+ return listProjectRoots().find((r) => dirKey(r.directory) === dirKey(bound2))?.config ?? null;
38218
39174
  }
38219
39175
  function resolveDeclared(offline) {
38220
39176
  const bound2 = path23.resolve(getProjectRoot());
@@ -38250,6 +39206,51 @@ function headAcross2(consumerDir, external) {
38250
39206
  const head2 = head(external.directory);
38251
39207
  return head2 ? { ...external, commit: head2 } : external;
38252
39208
  }
39209
+ function resolveCandidate(alias, declaration2) {
39210
+ const bound2 = path23.resolve(getProjectRoot());
39211
+ const { top, whole } = climb(bound2);
39212
+ const config = consumerConfig(bound2);
39213
+ const answer = (root) => runWithProjectRoot(root, () => {
39214
+ const family = graph();
39215
+ const node = consumerNode(family, bound2);
39216
+ if (!node) return null;
39217
+ const [declared] = declaredExternals({ externals: { [alias]: declaration2 }, ...config?.members ? { members: config.members } : {} });
39218
+ const candidate = bindDeclaredExternal(declared, node, family.nodes);
39219
+ const external = candidate.sourceKind === "git" ? resolveGit(candidate, { externals: { [alias]: declaration2 } }, false) : headAcross2(node.directory, resolveHosted(candidate));
39220
+ return { external, reachable: whole };
39221
+ });
39222
+ return answer(top) ?? answer(bound2) ?? {
39223
+ external: { alias, project: declaration2.project ?? alias, sourceKind: "unresolved", audience: "instance", role: "external", problem: "the bound project is not in the family graph its root reads" },
39224
+ reachable: whole
39225
+ };
39226
+ }
39227
+ function listConsumers() {
39228
+ const bound2 = path23.resolve(getProjectRoot());
39229
+ const { top } = climb(bound2);
39230
+ return runWithProjectRoot(top, () => {
39231
+ const family = graph();
39232
+ const producer = consumerNode(family, bound2);
39233
+ if (!producer) return [];
39234
+ const consumers = [];
39235
+ for (const node of family.nodes) {
39236
+ if (node === producer) continue;
39237
+ const external = node.externals.find((e) => e.sourceKind === "family" && e.producer === producer.namespace);
39238
+ const memberAlias = [...node.aliases.entries()].find(([alias2, key2]) => key2 === producer.namespace && !node.externals.some((e) => e.alias === alias2))?.[0];
39239
+ const alias = external?.alias ?? memberAlias;
39240
+ if (alias === void 0) continue;
39241
+ const usage = exportUsage(node.namespace, producer.namespace);
39242
+ consumers.push({
39243
+ project: node.id ?? node.namespace,
39244
+ key: node.namespace,
39245
+ directory: node.directory,
39246
+ alias,
39247
+ section: external ? "externals" : "members",
39248
+ names: usage.used.map((u) => u.publicName).sort()
39249
+ });
39250
+ }
39251
+ return consumers.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
39252
+ });
39253
+ }
38253
39254
 
38254
39255
  // src/core/part-context.ts
38255
39256
  var crypto6 = __toESM(require("crypto"));
@@ -39073,7 +40074,7 @@ function promoteMember(alias, id) {
39073
40074
  patchKeepingStamp(s.file, (raw) => raw.parentSystem === projectName ? false : (raw.parentSystem = projectName, true));
39074
40075
  }
39075
40076
  if (declaration2?.as === "part") projectConfigRepository.updateMember(alias, { as: "" });
39076
- const result = { respelled: [], memberRespelled: [], exported: [], imported: [], consumersDropped: [] };
40077
+ const result = { respelled: [], memberRespelled: [], exported: [], imported: [], consumersDropped: [], entriesDeclared: [], topicsCrossing: [] };
39077
40078
  const parentSide = rewriteRefsIn(crossing.in.map((c) => c.spec).filter(unique), (ref, position) => INTO_MEMBER_POSITIONS.has(position) && crossing.insideTargets.has(ref) ? `${alias}::${ref}` : ref, { result, member: false });
39078
40079
  const climbs = climbsOf(family, crossing.insideIds, parentId).map;
39079
40080
  const climbing = inside.filter((s) => family.authoredReferences.some((r) => r.form === "super" && r.specId === s.id));
@@ -39081,6 +40082,8 @@ function promoteMember(alias, id) {
39081
40082
  const qualified = respellQualified(inside, outside, { alias, parentId, newId }, result);
39082
40083
  invalidateSpecCache();
39083
40084
  dropCrossingConsumers(inside, outside, result);
40085
+ declareCrossingEntries(inside, outside, parentId || parentName, result);
40086
+ recordCrossingTopics(inside, outside, parentId || parentName, result);
39084
40087
  if (crossing.out.length > 0 || qualified.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
39085
40088
  for (const s of inside.filter((x) => x.kind === "subsystem")) restateReExports(s.id, alias);
39086
40089
  exportCrossings([...crossing.out, ...qualified.out], dir, alias, parentId, "out", result);
@@ -39106,6 +40109,92 @@ function promoteMember(alias, id) {
39106
40109
  invalidateSpecCache();
39107
40110
  return result;
39108
40111
  }
40112
+ function callsInto2(callers, portals) {
40113
+ const componentOf = new Map(callers.filter((s) => s.kind === "interface").map((s) => [s.id, String(s.raw.component ?? "")]));
40114
+ const out = /* @__PURE__ */ new Map();
40115
+ const add2 = (target, verb, caller) => {
40116
+ const portal = target?.includes("::") ? target.slice(target.lastIndexOf("::") + 2) : target;
40117
+ if (!portal || !verb || !portals.has(portal)) return;
40118
+ const verbs = out.get(portal) ?? /* @__PURE__ */ new Map();
40119
+ verbs.set(verb, /* @__PURE__ */ new Set([...verbs.get(verb) ?? [], caller]));
40120
+ out.set(portal, verbs);
40121
+ };
40122
+ for (const impl of callers.filter((s) => s.kind === "implementation")) {
40123
+ const caller = componentOf.get(String(impl.raw.contract ?? ""));
40124
+ if (!caller) continue;
40125
+ for (const method of Array.isArray(impl.raw.methods) ? impl.raw.methods : []) {
40126
+ for (const step of Array.isArray(method?.narrative) ? method.narrative : []) {
40127
+ if (step?.type === "call" || step?.type === "register") add2(step.targetComponent, step.targetMethod, caller);
40128
+ }
40129
+ for (const entry of Array.isArray(method?.calls) ? method.calls : []) {
40130
+ const call = typeof entry === "string" ? parseDeclaredCall(entry) : null;
40131
+ if (call) add2(call.compId, call.methodName, caller);
40132
+ }
40133
+ }
40134
+ }
40135
+ return out;
40136
+ }
40137
+ function declareCrossingEntries(inside, outside, parent, result) {
40138
+ const portals = new Map(inside.filter((s) => s.kind === "component" && s.raw.componentType === "Portal").map((s) => [s.id, s]));
40139
+ const called = callsInto2(outside, new Set(portals.keys()));
40140
+ for (const [portalId, verbs] of [...called].sort(([a], [b]) => a.localeCompare(b))) {
40141
+ const portal = portals.get(portalId);
40142
+ if (portal.raw.invokedBy?.kind === "entry") continue;
40143
+ const transport = portal.raw.transport;
40144
+ const network = transport !== void 0 && transportKind(transport) === "network";
40145
+ for (const contract of inside.filter((s) => s.kind === "interface" && s.raw.component === portalId)) {
40146
+ patchKeepingStamp(contract.file, (raw) => {
40147
+ let changed = false;
40148
+ for (const method of Array.isArray(raw.methods) ? raw.methods : []) {
40149
+ const callers = verbs.get(method?.name);
40150
+ if (!callers || method.invokedBy !== void 0) continue;
40151
+ const names = [...callers].sort().join(", ");
40152
+ method.invokedBy = {
40153
+ kind: "entry",
40154
+ ...network ? { scope: "network" } : {},
40155
+ caller: `${names} of ${parent}, calling ${portalId}.${method.name} across the project boundary (modelled calls in ${parent}; declared when this project was promoted out of it)`
40156
+ };
40157
+ result.entriesDeclared.push(`${portalId}.${method.name}: entry${network ? " (network)" : ""} \u2014 called by ${names} of ${parent}`);
40158
+ changed = true;
40159
+ }
40160
+ return changed;
40161
+ });
40162
+ }
40163
+ }
40164
+ }
40165
+ function topicEnds(specs) {
40166
+ const emits = /* @__PURE__ */ new Map();
40167
+ const subscribes = /* @__PURE__ */ new Map();
40168
+ const add2 = (into, topic, component) => {
40169
+ if (typeof topic !== "string" || topic === "") return;
40170
+ into.set(topic, /* @__PURE__ */ new Set([...into.get(topic) ?? [], component]));
40171
+ };
40172
+ for (const c of specs.filter((s) => s.kind === "component")) {
40173
+ for (const e of Array.isArray(c.raw.emits) ? c.raw.emits : []) add2(emits, e?.topic, c.id);
40174
+ for (const e of Array.isArray(c.raw.subscribesTo) ? c.raw.subscribesTo : []) add2(subscribes, e?.topic, c.id);
40175
+ }
40176
+ for (const i of specs.filter((s) => s.kind === "interface")) {
40177
+ for (const m of Array.isArray(i.raw.methods) ? i.raw.methods : []) {
40178
+ const ep = m?.endpoint;
40179
+ if (ep?.transport !== "MessageBus") continue;
40180
+ add2(ep.direction === "publish" ? emits : subscribes, ep.topic, String(i.raw.component ?? i.id));
40181
+ }
40182
+ }
40183
+ return { emits, subscribes };
40184
+ }
40185
+ function recordCrossingTopics(inside, outside, parent, result) {
40186
+ const mine = topicEnds(inside);
40187
+ const theirs = topicEnds(outside);
40188
+ const list = (ids) => [...ids ?? []].sort().join(", ");
40189
+ for (const [topic, emitters] of [...mine.emits].sort(([a], [b]) => a.localeCompare(b))) {
40190
+ if (mine.subscribes.has(topic) || !theirs.subscribes.has(topic)) continue;
40191
+ result.topicsCrossing.push(`${topic}: emitted by ${list(emitters)} (the new project), consumed in ${parent} by ${list(theirs.subscribes.get(topic))} \u2014 the family run pairs it; the new project's own gate alone reports UNCONSUMED_TOPIC`);
40192
+ }
40193
+ for (const [topic, subscribers] of [...mine.subscribes].sort(([a], [b]) => a.localeCompare(b))) {
40194
+ if (mine.emits.has(topic) || !theirs.emits.has(topic)) continue;
40195
+ result.topicsCrossing.push(`${topic}: consumed by ${list(subscribers)} (the new project), emitted in ${parent} by ${list(theirs.emits.get(topic))} \u2014 the family run pairs it; the new project's own gate alone reports UNSOURCED_SUBSCRIPTION`);
40196
+ }
40197
+ }
39109
40198
  function recordRespelling(result, edit4, member) {
39110
40199
  if (edit4.from === edit4.to) return;
39111
40200
  const list = member ? result.memberRespelled : result.respelled;
@@ -40027,7 +41116,9 @@ function rebaseImplementationPaths(specsDir, fromRoot, toRoot, only) {
40027
41116
  } catch {
40028
41117
  continue;
40029
41118
  }
40030
- if (!raw || typeof raw !== "object" || !("contract" in raw)) continue;
41119
+ if (!raw || typeof raw !== "object") continue;
41120
+ const isImplementation = "contract" in raw;
41121
+ if (!isImplementation && specKind(raw) !== "type") continue;
40031
41122
  let changed = false;
40032
41123
  const rebase = (holder, key2) => {
40033
41124
  const p = holder[key2];
@@ -40038,7 +41129,7 @@ function rebaseImplementationPaths(specsDir, fromRoot, toRoot, only) {
40038
41129
  changed = true;
40039
41130
  }
40040
41131
  };
40041
- for (const key2 of ["sourcePath", "simPath"]) rebase(raw, key2);
41132
+ for (const key2 of isImplementation ? ["sourcePath", "simPath"] : ["sourcePath"]) rebase(raw, key2);
40042
41133
  if (Array.isArray(raw.methods)) {
40043
41134
  for (const method of raw.methods) {
40044
41135
  if (method && typeof method === "object") rebase(method, "sourcePath");
@@ -40088,13 +41179,13 @@ function renameComponent(componentId, newId) {
40088
41179
  const contracts = interfaces.filter((i) => i.component === componentId);
40089
41180
  const movingInterface = contracts.find((i) => i.id === `i${componentId}`);
40090
41181
  const movingImplementation = implementations.find((impl) => impl.id === `${componentId}_impl` && contracts.some((i) => i.id === impl.contract));
40091
- const renamed = [
41182
+ const renamed2 = [
40092
41183
  { kind: "component", from: componentId, to: newId },
40093
41184
  ...movingInterface ? [{ kind: "interface", from: movingInterface.id, to: interfaceId }] : [],
40094
41185
  ...movingImplementation ? [{ kind: "implementation", from: movingImplementation.id, to: implementationId }] : []
40095
41186
  ];
40096
41187
  const rename = {
40097
- specs: renamed.map(({ from, to }) => ({ from, to })),
41188
+ specs: renamed2.map(({ from, to }) => ({ from, to })),
40098
41189
  components: [{ from: componentId, to: newId }]
40099
41190
  };
40100
41191
  const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
@@ -40109,18 +41200,18 @@ function renameComponent(componentId, newId) {
40109
41200
  const derivesName = (entry) => entry.interface !== void 0 ? movingInterface !== void 0 && entry.interface === movingInterface.id : entry.typeDef === void 0 && entry.component === componentId;
40110
41201
  const keptDry = keepPublicNames(specsDir, derivesName, true);
40111
41202
  assertSpecsInReach(
40112
- [...renamed.map(({ kind, from }) => ({ kind, id: from })), ...keptDry.specs, ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)],
41203
+ [...renamed2.map(({ kind, from }) => ({ kind, id: from })), ...keptDry.specs, ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)],
40113
41204
  `renaming component "${componentId}"`,
40114
41205
  Array.isArray(registerEdits) && registerEdits.length > 0
40115
41206
  );
40116
41207
  const kept = keepPublicNames(specsDir, derivesName);
40117
41208
  const rewrittenRefs = [...kept.specs, ...rewriteRefFields(specsDir, remap)];
40118
41209
  const allowsRekeyed = rekeyLintAllows(specsDir, rename);
40119
- const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
41210
+ const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed2.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
40120
41211
  invalidateSpecCache();
40121
41212
  moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation);
40122
41213
  const carried = projectConfigRepository.rekeyCarried(rename);
40123
- return { renamed, rewritten, carried, keptPublicNames: kept.names };
41214
+ return { renamed: renamed2, rewritten, carried, keptPublicNames: kept.names };
40124
41215
  }
40125
41216
  function moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation) {
40126
41217
  const interfaceId = `i${newId}`;
@@ -40269,7 +41360,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
40269
41360
  }
40270
41361
  const implementations = loadImplementationSpecs();
40271
41362
  const movingContracts = new Set(moving.map((i) => i.id));
40272
- const renamed = [];
41363
+ const renamed2 = [];
40273
41364
  const rename = {
40274
41365
  methods: [{
40275
41366
  component: componentId,
@@ -40304,7 +41395,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
40304
41395
  ...m.params === void 0 && m.signatureFrom === void 0 ? { signature: renameInSignature(m.signature, methodName, newName) } : {}
40305
41396
  } : m)
40306
41397
  });
40307
- renamed.push(contract.id);
41398
+ renamed2.push(contract.id);
40308
41399
  }
40309
41400
  let pinnedSymbol;
40310
41401
  for (const implementation of implementations) {
@@ -40317,7 +41408,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
40317
41408
  ...implementation,
40318
41409
  methods: implementation.methods.map((m) => m.name === methodName ? { ...m, name: newName, ...pin2 ? { symbol: methodName } : {} } : m)
40319
41410
  });
40320
- renamed.push(implementation.id);
41411
+ renamed2.push(implementation.id);
40321
41412
  }
40322
41413
  invalidateSpecCache();
40323
41414
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
@@ -40331,7 +41422,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
40331
41422
  component: componentId,
40332
41423
  from: methodName,
40333
41424
  to: newName,
40334
- renamed,
41425
+ renamed: renamed2,
40335
41426
  rewritten,
40336
41427
  mentions,
40337
41428
  ...pinnedSymbol ? { pinnedSymbol } : {},
@@ -40522,9 +41613,9 @@ function renameType(typeId, newId) {
40522
41613
  Array.isArray(registerEdits) && registerEdits.length > 0
40523
41614
  );
40524
41615
  const kept = keepPublicNames(specsDir, derivesName);
40525
- const respelled = respellAll(false);
41616
+ const respelled2 = respellAll(false);
40526
41617
  const allowsRekeyed = rekeyLintAllows(specsDir, rename);
40527
- const rewritten = [...new Set([...kept.specs, ...respelled, ...allowsRekeyed].filter((spec) => !(spec.kind === "type" && spec.id === oldId)).map((spec) => spec.id))];
41618
+ const rewritten = [...new Set([...kept.specs, ...respelled2, ...allowsRekeyed].filter((spec) => !(spec.kind === "type" && spec.id === oldId)).map((spec) => spec.id))];
40528
41619
  invalidateSpecCache();
40529
41620
  const moved = loadTypeSpecs().find((t) => t.id === oldId && (t.subsystem ?? "") === (type.subsystem ?? "")) ?? type;
40530
41621
  saveSpec("type", { ...moved, id: newId, previousIds: traced(moved.previousIds, oldId) });
@@ -40541,6 +41632,81 @@ function renameType(typeId, newId) {
40541
41632
  carried
40542
41633
  };
40543
41634
  }
41635
+ function renameField(typeId, field, newName) {
41636
+ const all = loadTypeSpecs();
41637
+ const cut = typeId.lastIndexOf("::");
41638
+ const qualifier = cut >= 0 ? typeId.slice(0, cut) : void 0;
41639
+ const ownSubsystem = qualifier !== void 0 && loadSubsystemSpecs().some((s) => s.id === qualifier);
41640
+ if (qualifier !== void 0 && !ownSubsystem) {
41641
+ throw new WaironError(`chained-type: "${typeId}" lives in another project; rename its field from that project's own root. ${memberRootWay(typeId, `sdd_rename_field ${typeId.slice(cut + 2)} ${field} <new name>`)}`);
41642
+ }
41643
+ const type = qualifier !== void 0 ? all.find((t) => t.id === typeId.slice(cut + 2) && t.subsystem === qualifier) ?? null : loadTypeSpec(typeId);
41644
+ if (!type) throw new WaironError(`type-missing: no type has the id "${typeId}".`);
41645
+ if (type.id.includes("::")) {
41646
+ throw new WaironError(`chained-type: "${type.id}" lives in another project; rename its field from that project's own root.`);
41647
+ }
41648
+ const fields = type.fields ?? [];
41649
+ const moving = fields.find((f) => f.name === field);
41650
+ if (!moving) {
41651
+ throw new WaironError(`field-missing: type "${type.id}" declares no field "${field}"${fields.length ? ` (it declares ${fields.map((f) => f.name).join(", ")})` : ""}.`);
41652
+ }
41653
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(newName)) {
41654
+ throw new WaironError(`invalid-name: "${newName}" is not an identifier (a letter or underscore, then letters, digits or underscores).`);
41655
+ }
41656
+ const taken = fields.find((f) => f !== moving && f.name === newName);
41657
+ if (taken) throw new WaironError(`name-taken: type "${type.id}" already has a field "${newName}".`);
41658
+ const retired = fields.find((f) => f !== moving && (f.previousNames ?? []).includes(newName));
41659
+ if (retired) {
41660
+ throw new WaironError(`name-retired: field "${retired.name}" of type "${type.id}" lists "${newName}" in its previousNames \u2014 the name is retired on this type. Unsetting that previousNames releases it.`);
41661
+ }
41662
+ if (newName === field) return { type: qualifiedTypeId(type), from: field, to: newName, rewritten: [] };
41663
+ const others = all.filter((t) => t !== type);
41664
+ const namesField = (ref, owner) => {
41665
+ if (typeof ref !== "string") return false;
41666
+ const dot = ref.lastIndexOf(".");
41667
+ if (dot <= 0 || ref.slice(dot + 1) !== field) return false;
41668
+ const head2 = ref.slice(0, dot);
41669
+ if (!typeMatchesRef(type, head2)) return false;
41670
+ if (!others.some((t) => typeMatchesRef(t, head2))) return true;
41671
+ return (owner ?? "") === (type.subsystem ?? "");
41672
+ };
41673
+ const specsDir = aiPathsAt(getProjectRoot()).specsDir();
41674
+ const respell = (dryRun) => {
41675
+ const out = [];
41676
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
41677
+ let raw;
41678
+ try {
41679
+ raw = readYamlFile(file);
41680
+ } catch {
41681
+ continue;
41682
+ }
41683
+ if (!raw || typeof raw !== "object" || specKind(raw) !== "type") continue;
41684
+ if (raw.id === type.id && (raw.subsystem ?? "") === (type.subsystem ?? "")) continue;
41685
+ const owner = typeof raw.subsystem === "string" ? raw.subsystem : void 0;
41686
+ let changed = false;
41687
+ for (const f of Array.isArray(raw.fields) ? raw.fields : []) {
41688
+ if (!namesField(f?.references, owner)) continue;
41689
+ f.references = `${f.references.slice(0, f.references.lastIndexOf("."))}.${newName}`;
41690
+ changed = true;
41691
+ }
41692
+ if (!changed) continue;
41693
+ if (!dryRun) writeYamlFile(file, raw);
41694
+ out.push({ kind: "type", id: String(raw.id) });
41695
+ }
41696
+ return out;
41697
+ };
41698
+ assertSpecsInReach([{ kind: "type", id: type.id }, ...respell(true)], `renaming field "${field}" of type "${typeId}"`, false);
41699
+ const rewritten = respell(false).map((spec) => spec.id);
41700
+ invalidateSpecCache();
41701
+ const stored = loadTypeSpecs().find((t) => t.id === type.id && (t.subsystem ?? "") === (type.subsystem ?? "")) ?? type;
41702
+ const renamed2 = (stored.fields ?? []).map((f) => {
41703
+ const self = namesField(f.references, type.subsystem) ? { references: `${f.references.slice(0, f.references.lastIndexOf("."))}.${newName}` } : {};
41704
+ return f.name === field ? { ...f, ...self, name: newName, previousNames: [...f.previousNames ?? [], field] } : { ...f, ...self };
41705
+ });
41706
+ saveSpec("type", { ...stored, fields: renamed2 });
41707
+ invalidateSpecCache();
41708
+ return { type: qualifiedTypeId(type), from: field, to: newName, rewritten };
41709
+ }
40544
41710
  function typeFileOf(id, type) {
40545
41711
  return getTypePath(id, type.subsystem, type.group);
40546
41712
  }
@@ -41458,17 +42624,25 @@ function composeAgentBrief(agentId) {
41458
42624
  domainName: "",
41459
42625
  variantGuidance: record.variantGuidance ?? ""
41460
42626
  });
41461
- const codeFence = codeFenceOf(record);
42627
+ const ownFence = codeFenceOf(record);
42628
+ const shared = ownFence && isSubsystemOwner(record) ? sharedProjectFiles(ownFence) : [];
42629
+ const codeFence = ownFence ? [...ownFence, ...shared] : ownFence;
41462
42630
  if (codeFence) {
41463
42631
  const shown2 = (p) => /[*?]/.test(p) || fs20.existsSync(path29.resolve(getProjectRoot(), p)) ? `- \`${p}\`` : `- \`${p}\` (planned \u2014 create it)`;
42632
+ const own2 = ownFence ?? [];
42633
+ const sharedSection = shared.length > 0 ? `
42634
+ Shared with the other subsystem owners (the project's setup and shared code; coordinate edits):
42635
+
42636
+ ${shared.map(shown2).join("\n")}
42637
+ ` : "";
41464
42638
  instructions = `${instructions.trimEnd()}
41465
42639
 
41466
42640
  ## Code write fence
41467
42641
 
41468
- ${codeFence.length > 0 ? `Write code only here (and tests beside it, as the project lays tests out):
42642
+ ${own2.length > 0 ? `Write code only here (and tests beside it, as the project lays tests out):
41469
42643
 
41470
- ${codeFence.map(shown2).join("\n")}
41471
- ` : "No spec names a code location yet, so no code location is declared. The spawning session should declare the planned `sourcePath` on the implementation now \u2014 it is code linkage, not part of the approval, so declaring it costs no re-lock \u2014 and that file is then this agent's fence.\n"}`;
42644
+ ${own2.map(shown2).join("\n")}
42645
+ ` : "No spec names a code location yet, so no code location is declared. The spawning session should declare the planned `sourcePath` on the implementation now \u2014 it is code linkage, not part of the approval, so declaring it costs no re-lock \u2014 and that file is then this agent's fence.\n"}${sharedSection}`;
41472
42646
  }
41473
42647
  const guidance = loadAgentOverride(agentId);
41474
42648
  if (guidance !== null) {
@@ -41480,8 +42654,7 @@ ${guidance.trim()}
41480
42654
  `;
41481
42655
  }
41482
42656
  const language = implementationLanguage(record);
41483
- const dialect = language ? typeDialectFor(language) : null;
41484
- const typeMapping = dialect ? dialect.mappingLines() : void 0;
42657
+ const typeMapping = (language ? typeMappingFor(language) : null) ?? void 0;
41485
42658
  if (typeMapping) {
41486
42659
  instructions = `${instructions.trimEnd()}
41487
42660
 
@@ -41492,6 +42665,19 @@ Contracts speak wairon's neutral type grammar; write each type in ${language} as
41492
42665
  ${typeMapping.map((line) => `- ${line}`).join("\n")}
41493
42666
  `;
41494
42667
  }
42668
+ const externals = externalsUsedBy(record);
42669
+ let readPaths = record.readPaths;
42670
+ if (externals.length > 0) {
42671
+ instructions = `${instructions.trimEnd()}
42672
+
42673
+ ## Externals used
42674
+
42675
+ These components reach other projects through \`alias::name\`; code against the pinned snapshot, never the producer's source:
42676
+
42677
+ ${externals.map(describeExternal).join("\n")}
42678
+ `;
42679
+ readPaths = [.../* @__PURE__ */ new Set([...record.readPaths, ...externals.map((e) => e.pin)])];
42680
+ }
41495
42681
  const profile = deriveExecutionProfile(record);
41496
42682
  const budget = resolveBudget(profile, config.execution, record.id);
41497
42683
  return {
@@ -41500,7 +42686,7 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
41500
42686
  template: record.template,
41501
42687
  domainRoot: record.domainRoot,
41502
42688
  ownedPaths: record.ownedPaths,
41503
- readPaths: record.readPaths,
42689
+ readPaths,
41504
42690
  instructions,
41505
42691
  variantGuidance: record.variantGuidance || void 0,
41506
42692
  ...typeMapping ? { typeMapping } : {},
@@ -41520,8 +42706,8 @@ function codeLocationsOf(impl) {
41520
42706
  return files;
41521
42707
  }
41522
42708
  function codeFenceOf(record) {
41523
- const implementsComponents = record.template === "implementer" || record.template === "domain-owner" && record.creationReason.startsWith("Automatically inferred from L1");
41524
- if (!implementsComponents) return null;
42709
+ const implementsComponents2 = record.template === "implementer" || record.template === "domain-owner" && record.creationReason.startsWith("Automatically inferred from L1");
42710
+ if (!implementsComponents2) return null;
41525
42711
  const files = record.ownedPaths.filter((p) => !p.startsWith(".wai/") && !p.includes("/.wai/") && !/[*?]/.test(p));
41526
42712
  if (files.length === 0) return [];
41527
42713
  const dirs = files.map((f) => path29.posix.dirname(f.replace(/\\/g, "/")));
@@ -41543,7 +42729,7 @@ function implementationLanguage(record) {
41543
42729
  for (const impl of implementations) {
41544
42730
  for (const technology of impl.technologies ?? []) {
41545
42731
  const name = typeof technology === "string" ? technology : technology.name;
41546
- if (typeDialectFor(name)) return name.toLowerCase();
42732
+ if (typeMappingFor(name)) return name.toLowerCase();
41547
42733
  }
41548
42734
  }
41549
42735
  for (const impl of implementations) {
@@ -41556,7 +42742,13 @@ function implementationLanguage(record) {
41556
42742
  const language = languageOfSourcePath(file);
41557
42743
  if (language) return language;
41558
42744
  }
41559
- return void 0;
42745
+ if (!implementsComponents(record)) return void 0;
42746
+ const subsystem = loadSubsystemSpecs().find((s) => record.ownedPaths.some((p) => p.includes(`/${s.id}/`) || p.endsWith(`/${s.id}.yaml`)));
42747
+ const declared = subsystem?.targetLanguage ?? loadSystemSpec()?.targetLanguage;
42748
+ return declared ? declared.toLowerCase() : void 0;
42749
+ }
42750
+ function implementsComponents(record) {
42751
+ return record.template === "implementer" || isSubsystemOwner(record);
41560
42752
  }
41561
42753
  function memberHop(agentId) {
41562
42754
  const at = agentId.indexOf("::");
@@ -41587,6 +42779,103 @@ function loadRegistry() {
41587
42779
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
41588
42780
  };
41589
42781
  }
42782
+ function isSubsystemOwner(record) {
42783
+ return record.template === "domain-owner" && record.creationReason.startsWith("Automatically inferred from L1");
42784
+ }
42785
+ var SHARED_SETUP_FILES = [
42786
+ "package.json",
42787
+ "package-lock.json",
42788
+ "tsconfig.json",
42789
+ "Cargo.toml",
42790
+ "Cargo.lock",
42791
+ "pyproject.toml",
42792
+ "setup.py",
42793
+ "setup.cfg",
42794
+ "requirements.txt",
42795
+ "go.mod"
42796
+ ];
42797
+ var SHARED_ROOT_FILES = ["src/lib.rs", "src/main.rs", "src/index.ts", "src/__init__.py"];
42798
+ function sharedProjectFiles(fence) {
42799
+ const root = getProjectRoot();
42800
+ const claimed = new Set(fence);
42801
+ for (const impl of loadImplementationSpecs()) for (const file of implementationSourceFiles(impl)) claimed.add(file);
42802
+ const out = [];
42803
+ const add2 = (file) => {
42804
+ if (!claimed.has(file) && !out.includes(file)) out.push(file);
42805
+ };
42806
+ for (const file of [...SHARED_SETUP_FILES, ...SHARED_ROOT_FILES]) {
42807
+ if (fs20.existsSync(path29.join(root, file))) add2(file);
42808
+ }
42809
+ for (const type of loadTypeSpecs()) {
42810
+ if (type.subsystem) continue;
42811
+ for (const file of typeSourceFiles(type)) add2(file);
42812
+ }
42813
+ return out;
42814
+ }
42815
+ function implementedComponents(record) {
42816
+ const owned = new Set(record.ownedPaths);
42817
+ const components = loadComponentSpecs();
42818
+ const contractOwner = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
42819
+ const ids = /* @__PURE__ */ new Set();
42820
+ for (const impl of loadImplementationSpecs()) {
42821
+ if (!implementationSourceFiles(impl).some((file) => owned.has(file))) continue;
42822
+ const component = contractOwner.get(impl.contract);
42823
+ if (component) ids.add(component);
42824
+ }
42825
+ if (record.id.endsWith("-implementer")) ids.add(record.id.slice(0, -"-implementer".length));
42826
+ return components.filter((c) => ids.has(c.id));
42827
+ }
42828
+ function externalsUsedBy(record) {
42829
+ const components = implementedComponents(record);
42830
+ if (components.length === 0) return [];
42831
+ const ids = new Set(components.map((c) => c.id));
42832
+ const refs = [];
42833
+ for (const c of components) refs.push(...c.dependsOn, ...c.owns ?? []);
42834
+ const contracts = loadInterfaceSpecs().filter((i) => ids.has(i.component));
42835
+ for (const contract of contracts) if (contract.implements) refs.push(contract.implements);
42836
+ const contractIds = new Set(contracts.map((i) => i.id));
42837
+ for (const impl of loadImplementationSpecs()) {
42838
+ if (!contractIds.has(impl.contract)) continue;
42839
+ for (const method of impl.methods) {
42840
+ for (const step of method.narrative) if (step.targetComponent) refs.push(step.targetComponent);
42841
+ for (const call of method.calls ?? []) refs.push(call.slice(0, call.lastIndexOf(".")));
42842
+ }
42843
+ }
42844
+ const byAlias = /* @__PURE__ */ new Map();
42845
+ for (const ref of refs) {
42846
+ const cut = ref.indexOf("::");
42847
+ if (cut <= 0) continue;
42848
+ const alias = ref.slice(0, cut);
42849
+ const names = byAlias.get(alias) ?? /* @__PURE__ */ new Set();
42850
+ names.add(ref.slice(cut + 2));
42851
+ byAlias.set(alias, names);
42852
+ }
42853
+ const root = getProjectRoot();
42854
+ const out = [];
42855
+ for (const [alias, names] of [...byAlias].sort(([a], [b]) => a.localeCompare(b))) {
42856
+ const pin2 = `.wai/externals/${alias}.yaml`;
42857
+ const bindings = /* @__PURE__ */ new Map();
42858
+ let pinned = false;
42859
+ try {
42860
+ const doc = readYamlFile(path29.join(root, pin2));
42861
+ pinned = !!doc;
42862
+ for (const entry of doc?.interfaces ?? []) {
42863
+ if (entry.id && names.has(entry.id)) bindings.set(entry.id, { transport: entry.transport, abi: entry.abi });
42864
+ }
42865
+ } catch {
42866
+ }
42867
+ out.push({ alias, names: [...names].sort(), pin: pin2, pinned, bindings });
42868
+ }
42869
+ return out;
42870
+ }
42871
+ function describeExternal(use) {
42872
+ const names = use.names.map((name) => {
42873
+ const binding = use.bindings.get(name);
42874
+ const how = [binding?.transport ? `transport ${binding.transport}` : "", binding?.abi ? `abi ${binding.abi}` : ""].filter(Boolean).join(", ");
42875
+ return `\`${use.alias}::${name}\`${how ? ` (${how})` : ""}`;
42876
+ });
42877
+ return `- **${use.alias}** \u2014 uses ${names.join(", ")}; pinned snapshot \`${use.pin}\`${use.pinned ? "" : " (not pinned yet \u2014 run `wairon externals pin` before coding against it)"}`;
42878
+ }
41590
42879
 
41591
42880
  // src/exporters/generate.ts
41592
42881
  var path33 = __toESM(require("path"));
@@ -42432,10 +43721,10 @@ function migrate(apply3) {
42432
43721
  rewrite(id, "retired-allow", `allow of ${a.code}${at} removed`, `${a.code} is retired with the rule that reported it.`);
42433
43722
  continue;
42434
43723
  }
42435
- const renamed = RENAMED_CODES[a.code];
42436
- if (renamed !== void 0) {
42437
- rewrite(id, "retired-allow", `allow of ${a.code}${at} rekeyed to ${renamed}`, `${a.code} is renamed ${renamed}.`);
42438
- kept.push({ ...a, code: renamed });
43724
+ const renamed2 = RENAMED_CODES[a.code];
43725
+ if (renamed2 !== void 0) {
43726
+ rewrite(id, "retired-allow", `allow of ${a.code}${at} rekeyed to ${renamed2}`, `${a.code} is renamed ${renamed2}.`);
43727
+ kept.push({ ...a, code: renamed2 });
42439
43728
  continue;
42440
43729
  }
42441
43730
  kept.push(a);
@@ -42485,23 +43774,59 @@ function designDigest(kind, content) {
42485
43774
  const key2 = `${kind ?? ""}\0${content}`;
42486
43775
  const memo2 = designDigestMemo.get(key2);
42487
43776
  if (memo2 !== void 0) return memo2;
43777
+ const parsed = parsedSpec(content);
43778
+ if (!parsed) return digest(content);
43779
+ const out = viewDigest(designViewOf(kind, parsed));
43780
+ if (designDigestMemo.size > 2e4) designDigestMemo.clear();
43781
+ designDigestMemo.set(key2, out);
43782
+ return out;
43783
+ }
43784
+ var STATUS_CANDIDATES = [void 0, "draft", "design", "complete"];
43785
+ function designViewOf(kind, parsed) {
43786
+ switch (kind) {
43787
+ case "implementation":
43788
+ return implementationDesignView(parsed);
43789
+ case "type":
43790
+ return typeDesignView(parsed);
43791
+ case "component":
43792
+ return componentDesignView(parsed);
43793
+ case "subsystem":
43794
+ return subsystemDesignView(parsed);
43795
+ case "interface":
43796
+ return interfaceDesignView(parsed);
43797
+ default:
43798
+ return parsed;
43799
+ }
43800
+ }
43801
+ function parsedSpec(content) {
42488
43802
  let parsed;
42489
43803
  try {
42490
43804
  parsed = parseYaml(content);
42491
43805
  } catch {
42492
- return digest(content);
43806
+ return null;
42493
43807
  }
42494
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return digest(content);
42495
- const view3 = kind === "implementation" ? implementationDesignView(parsed) : kind === "type" ? typeDesignView(parsed) : kind === "component" ? componentDesignView(parsed) : parsed;
42496
- const out = crypto7.createHash("sha256").update(canonicalize(view3), "utf8").digest("hex");
42497
- if (designDigestMemo.size > 2e4) designDigestMemo.clear();
42498
- designDigestMemo.set(key2, out);
42499
- return out;
43808
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
43809
+ }
43810
+ function viewDigest(view3) {
43811
+ return crypto7.createHash("sha256").update(canonicalize(view3), "utf8").digest("hex");
43812
+ }
43813
+ function statusApprovedIn(kind, content, recorded) {
43814
+ const parsed = parsedSpec(content);
43815
+ if (!parsed) return void 0;
43816
+ const view3 = designViewOf(kind, parsed);
43817
+ for (const status of STATUS_CANDIDATES) {
43818
+ const candidate = status === void 0 ? view3 : { ...view3, status };
43819
+ if (viewDigest(candidate) === recorded) return status ?? null;
43820
+ }
43821
+ return void 0;
43822
+ }
43823
+ function designMatches(kind, content, current2, recorded) {
43824
+ return current2 === recorded || statusApprovedIn(kind, content, recorded) !== void 0;
42500
43825
  }
42501
43826
  function linkedKindsByFile() {
42502
43827
  const paths = scanAllSpecs().paths;
42503
43828
  const kinds = /* @__PURE__ */ new Map();
42504
- for (const kind of ["implementation", "type", "component"]) {
43829
+ for (const kind of ["implementation", "type", "component", "subsystem", "interface"]) {
42505
43830
  for (const file of Object.values(paths[kind])) kinds.set(path35.resolve(file), kind);
42506
43831
  }
42507
43832
  return kinds;
@@ -42560,15 +43885,24 @@ function diffAgainstApproval(root = getProjectRoot()) {
42560
43885
  const record = approvalRecord(root);
42561
43886
  const approved = record?.specs ?? null;
42562
43887
  if (!approved) return null;
42563
- const current2 = currentSpecDigests(root, readingOf(record));
43888
+ const reading = readingOf(record);
43889
+ const files = new Map(ownSpecFiles2(root).map((f) => [f.key, f]));
43890
+ const current2 = {};
43891
+ for (const [key2, file] of files) current2[key2] = digestIn(reading, file.kind, file.content);
42564
43892
  const added = [];
42565
43893
  const changed = [];
42566
43894
  const removed = [];
42567
43895
  const unchangedPaths = [];
43896
+ const same = (rel2, before, now) => {
43897
+ if (before === now) return true;
43898
+ if (reading !== "design") return false;
43899
+ const file = files.get(rel2);
43900
+ return designMatches(file.kind, file.content, now, before);
43901
+ };
42568
43902
  for (const [rel2, d] of Object.entries(current2)) {
42569
43903
  const before = approved[rel2];
42570
43904
  if (before === void 0) added.push(rel2);
42571
- else if (before !== d) changed.push(rel2);
43905
+ else if (!same(rel2, before, d)) changed.push(rel2);
42572
43906
  else unchangedPaths.push(rel2);
42573
43907
  }
42574
43908
  for (const rel2 of Object.keys(approved)) {
@@ -42591,23 +43925,38 @@ function settledSpecPaths(root = getProjectRoot()) {
42591
43925
  var REEXPRESSED_BY = "wairon doctor --fix";
42592
43926
  function reexpress(gate) {
42593
43927
  const record = readLockRecord();
43928
+ if (!record) return null;
43929
+ const fromReading = readingOf(record);
42594
43930
  const asRecorded = gate.asRecorded;
42595
- const holds = !!asRecorded && record?.stateId.algorithm === asRecorded.algorithm && record.stateId.digest === asRecorded.digest;
42596
- if (!record || record.specsReading === "design" || !holds) return null;
42597
- const specs = captureApprovedSpecs();
42598
- return {
42599
- ...record,
42600
- stateId: { algorithm: gate.algorithm, digest: gate.digest },
42601
- specs,
42602
- specsReading: "design",
42603
- format: 3,
42604
- reexpressed: {
42605
- at: (/* @__PURE__ */ new Date()).toISOString(),
42606
- fromAlgorithm: record.stateId.algorithm,
42607
- fromReading: "content",
42608
- by: REEXPRESSED_BY
42609
- }
42610
- };
43931
+ const holds = !!asRecorded && record.stateId.algorithm === asRecorded.algorithm && record.stateId.digest === asRecorded.digest;
43932
+ const reexpressed = { at: (/* @__PURE__ */ new Date()).toISOString(), fromAlgorithm: record.stateId.algorithm, fromReading, by: REEXPRESSED_BY };
43933
+ if (holds) {
43934
+ return {
43935
+ ...record,
43936
+ stateId: { algorithm: gate.algorithm, digest: gate.digest },
43937
+ specs: captureApprovedSpecs(),
43938
+ specsReading: "design",
43939
+ format: 3,
43940
+ reexpressed
43941
+ };
43942
+ }
43943
+ if (fromReading !== "content" || !record.specs) return null;
43944
+ const diff2 = diffAgainstApproval();
43945
+ if (!diff2 || diffSize(diff2) > 0) return null;
43946
+ return { ...record, specs: captureApprovedSpecs(), specsReading: "design", format: 3, reexpressed };
43947
+ }
43948
+ function approvedStatuses(record, root = getProjectRoot()) {
43949
+ const out = /* @__PURE__ */ new Map();
43950
+ if (record.specsReading !== "design" || !record.specs) return out;
43951
+ for (const file of ownSpecFiles2(root)) {
43952
+ const recorded = record.specs[file.key];
43953
+ if (recorded === void 0 || !file.kind) continue;
43954
+ const approved = statusApprovedIn(file.kind, file.content, recorded);
43955
+ if (approved === void 0) continue;
43956
+ const id = parsedSpec(file.content)?.id;
43957
+ if (typeof id === "string") out.set(`${file.kind}:${id}`, approved ?? "complete");
43958
+ }
43959
+ return out;
42611
43960
  }
42612
43961
  function approvalVerdict(approvals) {
42613
43962
  const quiet = { text: "", drifted: false };
@@ -42615,10 +43964,10 @@ function approvalVerdict(approvals) {
42615
43964
  const approval = approvalRecord();
42616
43965
  if (!approval) return quiet;
42617
43966
  const by = describeApprover(approval.lockedBy);
42618
- const notes = memberNotes(approvals ?? [], approval);
43967
+ const diff2 = diffAgainstApproval();
43968
+ const notes = memberNotes(approvals ?? [], approval, diff2);
42619
43969
  const pinDrift = notes.drift;
42620
43970
  const childNote = notes.text;
42621
- const diff2 = diffAgainstApproval();
42622
43971
  if (!diff2) {
42623
43972
  return {
42624
43973
  text: `
@@ -42665,14 +44014,16 @@ function isContentReadingRecord(record) {
42665
44014
  var REEXPRESS_HINT = "This approval predates lock format 3 and is read in its original reading; `wairon doctor --fix` carries it into the design reading without a review (the approval stays the approver's), after which adding or moving a sourcePath never drifts it.";
42666
44015
  var FORMAT2_STALE_NOTE = "it was taken before code linkage (sourcePath, symbol, simPath and the like) left the approval, and something it covered moved since \u2014 the design, or only code linkage, which this record can no longer tell apart. One `wairon lock` clears it for good: from then on linkage never drifts the approval";
42667
44016
  var GATE_UPGRADE_NOTE = "the gate identity gained inputs in this release (members' composition subjects, `composition`; code conformance moved beside the claim)";
42668
- function memberNotes(approvals, record) {
44017
+ var GATE_MOVED_NOTE = "no own spec file changed since it was taken \u2014 not even its code linkage \u2014 so what moved is the gate it was judged under: the design rules this wairon release judges by, or the project's rule tuning, `composition`, network declaration, consumed contracts or a member's approval. One `wairon lock` re-approves the unchanged design under the current gate";
44018
+ function memberNotes(approvals, record, diff2) {
42669
44019
  const own2 = approvals.find((a) => a.key === "");
42670
44020
  const direct = approvals.filter((a) => a.parent === "" && a.as !== "part");
42671
44021
  const moved = direct.filter((a) => a.pinned === "moved");
42672
44022
  const unapproved = direct.filter((a) => a.state !== "approved");
42673
44023
  const lines = [];
42674
44024
  if (own2?.upgraded) {
42675
- lines.push(record && record.format === 2 ? `This approval no longer matches: ${FORMAT2_STALE_NOTE}.` : `This approval was taken under an earlier gate identity \u2014 ${GATE_UPGRADE_NOTE}. Re-lock once.`);
44025
+ const by = record?.validatorVersion ? ` (taken by wairon ${record.validatorVersion})` : "";
44026
+ lines.push(diff2 && diffSize(diff2) === 0 ? `This approval no longer matches${by}: ${GATE_MOVED_NOTE}.` : record && record.format === 2 ? `This approval no longer matches: ${FORMAT2_STALE_NOTE}.` : `This approval was taken under an earlier gate identity \u2014 ${GATE_UPGRADE_NOTE}. Re-lock once.`);
42676
44027
  }
42677
44028
  if (moved.length) {
42678
44029
  lines.push(`${moved.length} member pin(s) moved since approval: ${moved.map((a) => a.alias ?? a.key).join(", ")} \u2014 re-lock to approve them.`);
@@ -42774,17 +44125,17 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
42774
44125
  - **Deprecated forms** (they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them): a leading \`::\` (\`::shared::error-type\`), \`super::\` (\`super::sibling_comp\`), member paths (\`billing::invoice::invoice_portal\`), and an L1 subsystem carrying \`projectPath\` (\`DEPRECATED_MOUNT_FORM\`).
42775
44126
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
42776
44127
  - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
42777
- - **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`.
44128
+ - **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`. It is left out of the approval, so promoting a status never reopens an approved design.
42778
44129
  - **Code linkage is not approval**: the lock approves the DESIGN. Where it is realized \u2014 \`sourcePath\`, \`symbol\`, \`exportedVia\`, \`simPath\`, \`injectedParams\`, conformance tiers, timestamps \u2014 is outside the approved digests, so setting or changing it never asks for a re-lock. Declare each implementation's planned \`sourcePath\` at design time: a named file not written yet is \`SOURCE_FILE_PLANNED\` (a notice), at method level as at implementation level; once the component's realization begins (any file it names exists), a contract method naming no file is \`METHOD_SOURCE_PATH_MISSING\` (warning) and one its existing file does not hold is \`UNREALIZED_METHOD\`. \`rules.conformance.requireCode: true\` makes the planned and unlinked notices errors. Briefs fence planned files, marked \`(planned \u2014 create it)\`.
42779
- - **Externals \u2014 the pin gates, live drift is visible**: declare a project this one consumes with \`sdd_add_external\` (alias, and a source \`../sibling\`, \`hosted:<id>\`, \`<git url>\` or \`<git url>#<commit>\`) \u2014 never by hand-editing \`.wai/project.yaml\`; it is checked against the producer and pinned. The owner's gate judges every external against its pin. \`sdd_validate_tree\` and \`sdd_get_status\` also compare each external with its LIVE producer, offline, as ADVISORY findings (\`advisory: true\`): \`EXTERNAL_LIVE_INCOMPATIBLE\` (a used member changed, was renamed \u2014 the new name is given \u2014 or is gone), \`EXTERNAL_DRIFTED\`, \`EXTERNAL_LIVE_UNCOMPARED\`. They never make the tree invalid or fail \`--ci\`; the fix is to adapt the uses, then ask the human to re-pin (\`wairon externals pin <alias>\`). A git producer is compared live only by \`wairon externals status\` \u2014 the opt-in live CI gate (exit 1 incompatible, 2 not compared, 0 otherwise).
44130
+ - **Externals \u2014 the pin gates, live drift is visible**: declare a project this one consumes with \`sdd_add_external\` (alias, and a source \`../sibling\`, \`hosted:<id>\`, \`<git url>\` or \`<git url>#<commit>\`) \u2014 never by hand-editing \`.wai/project.yaml\`; it is checked against the producer and pinned. Change its \`use\` imports with \`sdd_update_external\` and remove it (declaration and pin together) with \`sdd_remove_external\`. A name the producer exports to a narrower audience (\`project\` < \`department\` < \`instance\` < \`partner\` < \`external\`) than this project is read at is refused, naming both. The owner's gate judges every external against its pin. \`sdd_validate_tree\` and \`sdd_get_status\` also compare each external with its LIVE producer, offline, as ADVISORY findings (\`advisory: true\`): \`EXTERNAL_LIVE_INCOMPATIBLE\` (a used member changed, was renamed \u2014 the new name is given \u2014 or is gone), \`EXTERNAL_DRIFTED\`, \`EXTERNAL_LIVE_UNCOMPARED\`. They never make the tree invalid or fail \`--ci\`; the fix is to adapt the uses, then ask the human to re-pin (\`wairon externals pin <alias>\`). A git producer is compared live only by \`wairon externals status\` \u2014 the opt-in live CI gate (exit 1 incompatible, 2 not compared, 0 otherwise).
42780
44131
  - **To implement code**: Delegate via the \`sdd-delegate\` skill: fetch the component's live brief with the \`sdd_get_agent_brief\` MCP tool (or the \`wairon-agent://\` resource) and spawn a subagent from it. Briefs are composed per call from the current spec tree, so they are always current \u2014 never wait for a restart. Implementations must match L3 interfaces and L5 narratives exactly. Generated agent files under \`.claude/agents/\` are an optional materialized view of the same topology \u2014 the live briefs are canonical.
42781
44132
 
42782
44133
  ### Rules (enforced by \`sdd_validate_tree\`)
42783
44134
  1. **Design before code**: Complete spec and pass validator before writing source code.
42784
44135
  2. **Human-in-the-loop**: Ask user approval for each spec layer before proceeding.
42785
44136
  3. **Spec consistency**: If a 1:1 narrative match is incorrect or conflicts with L0 requirements, escalate a spec revision first. Never ship mismatched code.
42786
- 4. **No persistence shortcuts & strict layers**: A Portal must never depend directly on a Store, Registry, or Adapter. Portal reads MAY go through a Repository/Index facade (passthrough reads need no per-entity Orchestrator ceremony), but a Portal narrative call or dispatch-table binding that reaches a write-effect facade method is an error (\`PORTAL_WRITE_SHORTCUT\`) \u2014 writes always route through an Orchestrator. Held domain state always lives in a dedicated data component, never as fields inside an Orchestrator. Two sanctioned shapes: the RECOMMENDED Repository pattern (owns Store + Registry + Index; consumers depend on the facade), or \u2014 for genuinely simple state \u2014 a deliberately standalone Store (workflow-layer consumers only, acknowledged with a lint.allow reason on the UNOWNED_STORE warning). Never combine Store/Registry/Index roles into one component, and never fold state into a consuming component because a link was refused.
42787
- 5. **Every Portal verb is reached**: a modelled caller reaches it (a \`call\` step along \`dependsOn\`, \`alias::portal.verb\` across projects), or it is an **entry** \u2014 callers outside the design reach it: \`invokedBy: { kind: entry, caller }\` on the Portal (every verb inherits it) or on one verb. Declare an entry only for real outside callers (browsers, a CLI user, an AI tool over stdio, applications linking a library) \u2014 **never invent one to silence an \`UNUSED_*\` finding**: model the caller, or remove the verb. A Portal states one \`transport\` (\`HTTP\`, \`gRPC\`, \`GraphQL\`, \`MessageBus\`, \`CLI\`, \`IPC\`, \`NamedPipe\`, \`JSONRPC\` for stdio JSON-RPC, \`InProcess\` for a library, which binds no endpoint). A project may declare \`network\` in \`project.yaml\`: an entry's \`scope\` is then \`outside\` (default) or \`network\` (sibling services), and inside a network only a \`gateway\` Portal is entered from outside. Another project's \`InProcess\` library is called directly from any component \u2014 no client Adapter \u2014 within the purity (\`effect\`) and language-bridge (\`abi\`) checks.
44137
+ 4. **No persistence shortcuts & strict layers**: A Portal must never depend directly on a Store, Registry, or Adapter. Portal reads MAY go through a Repository/Index facade (passthrough reads need no per-entity Orchestrator ceremony), but a Portal narrative call or dispatch-table binding that reaches a write-effect facade method is an error (\`PORTAL_WRITE_SHORTCUT\`) \u2014 writes always route through an Orchestrator. Held domain state always lives in a dedicated data component, never as fields inside an Orchestrator. Design the SMALLEST sound shape: genuinely simple state (keyed reads and writes) is one standalone Store \u2014 the sanctioned shape, with workflow-layer consumers and a lint.allow reason on the UNOWNED_STORE warning \u2014 and the Repository pattern (a facade owning Store + Registry + one Index per real lookup) is for state with real lookup or index needs. Never split Store/Registry/Index for simple state, never combine their roles into one component, and never fold state into a consuming component because a link was refused.
44138
+ 5. **Every Portal verb is reached**: a modelled caller reaches it (a \`call\` step along \`dependsOn\`, \`alias::portal.verb\` across projects), or it is an **entry** \u2014 callers outside the design reach it: \`invokedBy: { kind: entry, caller }\` on the Portal (every verb inherits it) or on one verb. Declare an entry only for real outside callers (browsers, a CLI user, an AI tool over stdio, applications linking a library) \u2014 **never invent one to silence an \`UNUSED_*\` finding**: model the caller, or remove the verb. A Portal states one \`transport\` (\`HTTP\`, \`gRPC\`, \`GraphQL\`, \`MessageBus\`, \`CLI\`, \`IPC\`, \`NamedPipe\`, \`JSONRPC\` for stdio JSON-RPC, \`InProcess\` for a library, which binds no endpoint). A project may declare a network (\`sdd_set_network\`; the human runs \`wairon network declare\` \u2014 never a hand edit of \`project.yaml\`): an entry's \`scope\` is then \`outside\` (default) or \`network\` (sibling services), and inside a network only a \`gateway\` Portal is entered from outside. Another project's \`InProcess\` library is called directly from any component \u2014 no client Adapter \u2014 within the purity (\`effect\`) and language-bridge (\`abi\`) checks.
42788
44139
  6. **Naming follows the target language**: methods are named in the project's method casing (camelCase for TypeScript/Java, snake_case for Rust/Python, from \`targetLanguage\` unless \`rules.naming\` says otherwise); a wire name that differs (an HTTP path, a CLI command, a JSON-RPC method) belongs on the method's \`endpoint\`, never in the method name.
42789
44140
 
42790
44141
  ### Component Vocabulary
@@ -43073,7 +44424,7 @@ function getStatusReport(options = {}, decor) {
43073
44424
  const mark = fillDecor(decor);
43074
44425
  scanAllSpecs({ memberDepth: options.memberDepth });
43075
44426
  const system = loadSystemSpec();
43076
- const loaderErrors = getLoaderIssues();
44427
+ const loaderErrors = getLoaderIssues().filter((issue2) => issue2.severity === "error");
43077
44428
  if (loaderErrors.length > 0) {
43078
44429
  let errText2 = "Failed to parse specification files:\n";
43079
44430
  for (const issue2 of loaderErrors) {
@@ -43087,7 +44438,10 @@ function getStatusReport(options = {}, decor) {
43087
44438
  const partLine = partOf ? `${mark.layer("system", "\u25CF Part of:")} ${mark.emphasis(partOf.project)} ${parentPinState(partOf.project)}
43088
44439
  ` : "";
43089
44440
  if (!system && !partOf) {
43090
- return { text: "L0 System specification (system.yaml) is missing.", failed: true };
44441
+ if (!pathExists(fromProjectRoot(".wai/project.yaml"))) {
44442
+ return { text: `No wairon project binds this folder (no .wai/project.yaml at ${fromProjectRoot(".")}): there is no design to report.`, failed: true };
44443
+ }
44444
+ return { text: "This project has no L0 System spec yet (.wai/specs/.index.yaml): its design has not been started.", failed: true };
43091
44445
  }
43092
44446
  let subsystems = loadSubsystemSpecs();
43093
44447
  let components = loadComponentSpecs();
@@ -43107,6 +44461,13 @@ function getStatusReport(options = {}, decor) {
43107
44461
  }
43108
44462
  let output = "";
43109
44463
  const componentScores = /* @__PURE__ */ new Map();
44464
+ const family = graph();
44465
+ const ownerOf4 = (key2) => family.owners.get(key2) ?? "";
44466
+ const sourceExists = (specId, file) => {
44467
+ const owner = ownerOf4(specId);
44468
+ const dir = owner === "" ? void 0 : family.nodes.find((n) => n.namespace === owner)?.directory;
44469
+ return pathExists(dir ? path37.resolve(dir, file) : fromProjectRoot(file));
44470
+ };
43110
44471
  for (const comp of components) {
43111
44472
  let score = 20;
43112
44473
  const intf = interfaces.find((i) => i.component === comp.id);
@@ -43117,7 +44478,7 @@ function getStatusReport(options = {}, decor) {
43117
44478
  if (impl) {
43118
44479
  score += 30;
43119
44480
  const sourceFiles = implementationSourceFiles(impl);
43120
- if (sourceFiles.length > 0 && sourceFiles.every((f) => pathExists(fromProjectRoot(f)))) {
44481
+ if (sourceFiles.length > 0 && sourceFiles.every((f) => sourceExists(impl.id, f))) {
43121
44482
  score += 20;
43122
44483
  }
43123
44484
  }
@@ -43147,13 +44508,6 @@ function getStatusReport(options = {}, decor) {
43147
44508
  const systemScore = subsystems.length > 0 ? Math.round(totalSubsystemsScore / subsystems.length) : 0;
43148
44509
  output += system ? `${mark.layer("system", "\u25CF System:")} ${mark.emphasis(system.name)} ${mark.score(systemScore, `(${systemScore}% Complete)`)}
43149
44510
  ` : partLine;
43150
- const family = graph();
43151
- const ownerOf4 = (key2) => family.owners.get(key2) ?? "";
43152
- const sourceExists = (specId, file) => {
43153
- const owner = ownerOf4(specId);
43154
- const dir = owner === "" ? void 0 : family.nodes.find((n) => n.namespace === owner)?.directory;
43155
- return pathExists(dir ? path37.resolve(dir, file) : fromProjectRoot(file));
43156
- };
43157
44511
  const holdsShown = (ns) => subsystems.some((s) => ownerOf4(s.id) === ns) || family.nodes.some((n) => n.parent === ns && n.namespace !== ns && holdsShown(n.namespace));
43158
44512
  const partMark = /* @__PURE__ */ new Map();
43159
44513
  for (const node of family.nodes) {
@@ -43194,7 +44548,7 @@ function getStatusReport(options = {}, decor) {
43194
44548
  const methodsWithOwnPath = impl.methods.filter((m) => m.sourcePath);
43195
44549
  let pathStr;
43196
44550
  if (impl.sourcePath) {
43197
- pathStr = sourceExists(impl.id, impl.sourcePath) ? mark.present(` -> ${impl.sourcePath}`) : mark.missing(` -> ${impl.sourcePath} (File Missing!)`);
44551
+ pathStr = sourceExists(impl.id, impl.sourcePath) ? mark.present(` -> ${impl.sourcePath}`) : mark.draft(` -> ${impl.sourcePath} (planned \u2014 not written yet)`);
43198
44552
  } else if (methodsWithOwnPath.length > 0) {
43199
44553
  pathStr = "";
43200
44554
  } else {
@@ -43208,7 +44562,7 @@ function getStatusReport(options = {}, decor) {
43208
44562
  const isLastMethod = k === methodsWithOwnPath.length - 1;
43209
44563
  const methodPrefix = isLastMethod ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
43210
44564
  const methodPath = method.sourcePath;
43211
- const methodLine = sourceExists(impl.id, methodPath) ? mark.present(`method ${method.name} -> ${methodPath}`) : mark.missing(`method ${method.name} -> ${methodPath} (File Missing!)`);
44565
+ const methodLine = sourceExists(impl.id, methodPath) ? mark.present(`method ${method.name} -> ${methodPath}`) : mark.draft(`method ${method.name} -> ${methodPath} (planned \u2014 not written yet)`);
43212
44566
  output += `${mark.structure(methodIndent + methodPrefix)}${methodLine}
43213
44567
  `;
43214
44568
  }
@@ -43816,6 +45170,15 @@ function carryInto(restatement, existing, candidate) {
43816
45170
  carried.push(`previousNames (${String(method.name)})`);
43817
45171
  }
43818
45172
  }
45173
+ if (restatement.kind === "type" && Array.isArray(candidate.fields)) {
45174
+ const stored = existing.fields ?? [];
45175
+ for (const field of candidate.fields) {
45176
+ const prev = stored.find((p) => p.name === field.name);
45177
+ if (prev?.previousNames === void 0 || field.previousNames !== void 0) continue;
45178
+ field.previousNames = prev.previousNames;
45179
+ carried.push(`previousNames (field ${String(field.name)})`);
45180
+ }
45181
+ }
43819
45182
  carried.push(...carryUnexpressed(existing, candidate, [...restatement.fields, "createdAt"]));
43820
45183
  return carried;
43821
45184
  }
@@ -44133,7 +45496,7 @@ function buildServerInstructions3() {
44133
45496
  }
44134
45497
 
44135
45498
  // src/network/adapters/files.ts
44136
- var import_yaml14 = require("yaml");
45499
+ var import_yaml15 = require("yaml");
44137
45500
 
44138
45501
  // src/network/types.ts
44139
45502
  function workload(party) {
@@ -44214,6 +45577,23 @@ function addFlow(flows3, flow) {
44214
45577
  function byCalleeThenCaller(a, b) {
44215
45578
  return (a.to.project ?? "").localeCompare(b.to.project ?? "") || key(a.to).localeCompare(key(b.to)) || key(a.from).localeCompare(key(b.from));
44216
45579
  }
45580
+ function findingLine(f) {
45581
+ return `${f.code}: ${f.message}`;
45582
+ }
45583
+ function markFlows(flows3, f) {
45584
+ if (f.specId === void 0) return;
45585
+ const onVerb = (flow) => flow.from.scope !== void 0 && flow.to.component === f.specId && (f.at === void 0 || flow.to.verb === f.at);
45586
+ const onCall = (flow) => flow.from.scope === void 0 && flow.from.component === f.specId && f.at !== void 0 && flow.evidence.includes(`call ${f.at}`);
45587
+ for (const flow of flows3.filter((x) => onVerb(x) || onCall(x))) {
45588
+ const field = f.severity === "error" ? "refusedBy" : "flaggedBy";
45589
+ const list = flow[field] ?? [];
45590
+ if (!list.includes(findingLine(f))) list.push(findingLine(f));
45591
+ flow[field] = list;
45592
+ }
45593
+ }
45594
+ function isRefused(flow) {
45595
+ return (flow.refusedBy?.length ?? 0) > 0;
45596
+ }
44217
45597
  function project2(model) {
44218
45598
  const flows3 = /* @__PURE__ */ new Map();
44219
45599
  for (const verb of model.verbs.filter((v) => transportKind(v.transport) === "network")) {
@@ -44226,7 +45606,9 @@ function project2(model) {
44226
45606
  }
44227
45607
  addFlow(flows3, outsideFlow(model, verb));
44228
45608
  }
44229
- return [...flows3.values()].sort(byCalleeThenCaller);
45609
+ const marked = [...flows3.values()];
45610
+ for (const f of model.findings ?? []) markFlows(marked, f);
45611
+ return marked.sort(byCalleeThenCaller);
44230
45612
  }
44231
45613
  function networkName2(id) {
44232
45614
  return id === "" ? "(root network)" : id;
@@ -44236,24 +45618,89 @@ function chainOf(flow) {
44236
45618
  for (const network of flow.crosses) lines.push(`enters network ${networkName2(network)}`);
44237
45619
  if (flow.via !== void 0) lines.push(`through gateway ${flow.via}`);
44238
45620
  lines.push(`reaches ${key(flow.to)} over ${flow.transport}${flow.binding ? ` (${flow.binding})` : ""}`);
45621
+ for (const flag of flow.flaggedBy ?? []) lines.push(`marked by the gate: ${flag}`);
44239
45622
  return lines;
44240
45623
  }
44241
- function notAllowed(flows3, from, to) {
44242
- const nearest = flows3.filter((f) => isNamed(f.to, to)).slice(0, 10);
45624
+ function verbParty(v) {
45625
+ return { project: v.project, ...v.subsystem !== void 0 ? { subsystem: v.subsystem } : {}, component: v.portal, verb: v.verb };
45626
+ }
45627
+ function reachesOverNetwork(model, to) {
45628
+ return model.verbs.some((v) => transportKind(v.transport) === "network" && isNamed(verbParty(v), to));
45629
+ }
45630
+ function isKnown(model, name) {
45631
+ if (name === "outside" || name === "network") return true;
45632
+ if (name.startsWith("network:")) return model.networks.some((n) => n.id === name.slice("network:".length));
45633
+ const placed = (model.placements ?? []).some((p) => isNamed({ project: p.project, ...p.subsystem !== void 0 ? { subsystem: p.subsystem } : {}, component: p.component }, name));
45634
+ return placed || model.verbs.some((v) => isNamed(verbParty(v), name)) || model.calls.some((c) => isNamed(callerOf(c), name));
45635
+ }
45636
+ function notAllowed(model, flows3, refused3, from, to) {
45637
+ if (refused3.length > 0) {
45638
+ const refusedBy = [...new Set(refused3.flatMap((f) => f.refusedBy ?? []))];
45639
+ const chain2 = [`the design names ${refused3.length === 1 ? "a flow" : `${refused3.length} flows`} from ${from} to ${to}, but the gate refuses ${refused3.length === 1 ? "it" : "them"}: nothing admits it until the design passes \`wairon validate\``];
45640
+ for (const f of refused3) chain2.push(`refused: ${key(f.from)} -> ${key(f.to)} over ${f.transport}${f.binding ? ` (${f.binding})` : ""}`);
45641
+ for (const line of refusedBy) chain2.push(` ${line}`);
45642
+ return { allowed: false, flows: [], chain: chain2, refusedBy };
45643
+ }
45644
+ if (!reachesOverNetwork(model, to)) {
45645
+ return {
45646
+ allowed: false,
45647
+ flows: [],
45648
+ inProcess: true,
45649
+ chain: [
45650
+ `${to} takes no network flow: it is reached in-process (a direct call, or a local transport, inside one process), so the design neither allows nor forbids a network flow to it`,
45651
+ "network flows are made only by verbs of Portals with a network transport (HTTP, gRPC, GraphQL, MessageBus, Custom)"
45652
+ ]
45653
+ };
45654
+ }
45655
+ const nearest = flows3.filter((f) => isNamed(f.to, to) && !isRefused(f)).slice(0, 10);
44243
45656
  const chain = [`nothing in the design allows ${from} to reach ${to}`];
44244
45657
  if (nearest.length === 0) chain.push(`no flow reaches ${to} at all`);
44245
45658
  for (const f of nearest) chain.push(`allowed instead: ${key(f.from)} -> ${key(f.to)} over ${f.transport}`);
44246
45659
  return { allowed: false, flows: [], chain };
44247
45660
  }
44248
- function explain(flows3, from, to) {
45661
+ function explain(flows3, from, to, model) {
45662
+ const unknown = [from, to].filter((n) => !isKnown(model, n));
45663
+ if (unknown.length > 0) {
45664
+ return {
45665
+ allowed: false,
45666
+ flows: [],
45667
+ unknown,
45668
+ chain: [
45669
+ ...unknown.map((n) => `unknown party "${n}": nothing in the design is named so`),
45670
+ "name a party as outside, network[:<id>], a project, a subsystem or a component, and a callee also as portal.verb; a subsystem of this project is named bare (a project is reached as <alias>::<name>)"
45671
+ ]
45672
+ };
45673
+ }
44249
45674
  const selected = flows3.filter((f) => isNamed(f.from, from) && isNamed(f.to, to));
44250
- if (selected.length === 0) return notAllowed(flows3, from, to);
44251
- const chain = selected.flatMap((f, i) => [...selected.length > 1 ? [`flow ${i + 1}:`] : [], ...chainOf(f)]);
44252
- return { allowed: true, flows: selected, chain };
45675
+ const admitted = selected.filter((f) => !isRefused(f));
45676
+ if (admitted.length === 0) return notAllowed(model, flows3, selected.filter(isRefused), from, to);
45677
+ const chain = admitted.flatMap((f, i) => [...admitted.length > 1 ? [`flow ${i + 1}:`] : [], ...chainOf(f)]);
45678
+ return { allowed: true, flows: admitted, chain };
44253
45679
  }
44254
45680
 
44255
45681
  // src/network/codec.ts
44256
- var COLUMNS = ["from", "to", "transport", "binding", "crosses", "via", "evidence"];
45682
+ var COLUMNS = ["from", "to", "transport", "binding", "crosses", "via", "evidence", "gate"];
45683
+ function refusedFlow(flow) {
45684
+ return (flow.refusedBy?.length ?? 0) > 0;
45685
+ }
45686
+ function codeOf(line) {
45687
+ const cut = line.indexOf(":");
45688
+ return cut === -1 ? line : line.slice(0, cut);
45689
+ }
45690
+ function gateCell(flow) {
45691
+ if (refusedFlow(flow)) return `REFUSED: ${[...new Set(flow.refusedBy.map(codeOf))].join("; ")}`;
45692
+ return [...new Set((flow.flaggedBy ?? []).map(codeOf))].join("; ");
45693
+ }
45694
+ function gateFindingsOf(flows3) {
45695
+ const errors = /* @__PURE__ */ new Set();
45696
+ const flags = /* @__PURE__ */ new Set();
45697
+ for (const f of flows3) {
45698
+ for (const line of f.refusedBy ?? []) errors.add(line);
45699
+ for (const line of f.flaggedBy ?? []) flags.add(line);
45700
+ }
45701
+ const withSeverity = (line, severity) => `${codeOf(line)} (${severity})${line.slice(codeOf(line).length)}`;
45702
+ return [...[...errors].map((l) => withSeverity(l, "error")), ...[...flags].map((l) => withSeverity(l, "warning"))];
45703
+ }
44257
45704
  function networkName3(id) {
44258
45705
  return id === "" ? "(root network)" : id;
44259
45706
  }
@@ -44265,7 +45712,8 @@ function cells(flow) {
44265
45712
  flow.binding ?? "",
44266
45713
  flow.crosses.map(networkName3).join("; "),
44267
45714
  flow.via ?? "",
44268
- flow.evidence.join("; ")
45715
+ flow.evidence.join("; "),
45716
+ gateCell(flow)
44269
45717
  ];
44270
45718
  }
44271
45719
  function sortedKeys2(value) {
@@ -44282,16 +45730,19 @@ function markdownOf(flows3) {
44282
45730
  const escape = (cell) => cell === "" ? " " : cell.replace(/\|/g, "\\|").replace(/\n/g, " ");
44283
45731
  const rows = flows3.map((f) => `| ${cells(f).map(escape).join(" | ")} |`);
44284
45732
  const header = [`| ${COLUMNS.join(" | ")} |`, `|${COLUMNS.map(() => "---").join("|")}|`];
44285
- return [...header, ...rows].join("\n") + "\n" + (flows3.length === 0 ? "\n_No network-transport flows: in-process and local verbs never produce one._\n" : "");
45733
+ const refused3 = flows3.filter(refusedFlow);
45734
+ const warning = refused3.length > 0 ? [`> **The design fails the gate:** ${refused3.length} row(s) marked REFUSED are refused by \`wairon validate\`, not allowed. Fix the design before trusting this matrix.`, ...gateFindingsOf(flows3).map((l) => `> - ${l.replace(/\n/g, " ")}`), ""] : [];
45735
+ return [...warning, ...header, ...rows].join("\n") + "\n" + (flows3.length === 0 ? "\n_No network-transport flows: in-process and local verbs never produce one._\n" : "");
44286
45736
  }
44287
45737
  function encodeFlows(flows3, format) {
45738
+ const gate = { gateFindings: gateFindingsOf(flows3), refused: flows3.some(refusedFlow) };
44288
45739
  switch (format) {
44289
45740
  case "json":
44290
- return { format, content: JSON.stringify(sortedKeys2(flows3), null, 2) + "\n", unbound: [] };
45741
+ return { format, content: JSON.stringify(sortedKeys2(flows3), null, 2) + "\n", unbound: [], ...gate };
44291
45742
  case "csv":
44292
- return { format, content: csvOf(flows3), unbound: [] };
45743
+ return { format, content: csvOf(flows3), unbound: [], ...gate };
44293
45744
  default:
44294
- return { format: "markdown", content: markdownOf(flows3), unbound: [] };
45745
+ return { format: "markdown", content: markdownOf(flows3), unbound: [], ...gate };
44295
45746
  }
44296
45747
  }
44297
45748
 
@@ -44304,7 +45755,7 @@ function flows(options, format) {
44304
45755
  function why(options, from, to) {
44305
45756
  const model = reachModel2(options);
44306
45757
  const matrix = project2(model);
44307
- return explain(matrix, from, to);
45758
+ return explain(matrix, from, to, model);
44308
45759
  }
44309
45760
 
44310
45761
  // src/mcp/network.ts
@@ -44320,6 +45771,10 @@ function flows2(to) {
44320
45771
  function explain2(from, to) {
44321
45772
  return why({}, from, to);
44322
45773
  }
45774
+ function declare2(declared, description) {
45775
+ const network = declared ? description ? { description } : {} : null;
45776
+ return setNetwork(network);
45777
+ }
44323
45778
 
44324
45779
  // src/migrations/orchestrator.ts
44325
45780
  var path47 = __toESM(require("path"));
@@ -45629,7 +47084,7 @@ function place(path49, bytes) {
45629
47084
  throw e;
45630
47085
  }
45631
47086
  }
45632
- function remove(path49) {
47087
+ function remove2(path49) {
45633
47088
  retrying("remove", path49, () => {
45634
47089
  try {
45635
47090
  fs25.unlinkSync(path49);
@@ -46806,6 +48261,12 @@ function recordCrossing(plan6, bound2, member, result) {
46806
48261
  for (const line of crossed.exported) plan6.edits.push({ project: bound2, kind: "export", detail: `export ${line}` });
46807
48262
  for (const line of crossed.imported) plan6.edits.push({ project: bound2, kind: "external", detail: `import: ${line}` });
46808
48263
  for (const line of crossed.consumersDropped) plan6.edits.push({ project: bound2, kind: "reference", detail: `consumers across the boundary to ${member} dropped: ${line}` });
48264
+ for (const line of crossed.entriesDeclared ?? []) plan6.edits.push({ project: member, kind: "reference", detail: `entry declared for the callers left across the boundary \u2014 ${line}` });
48265
+ const topics = crossed.topicsCrossing ?? [];
48266
+ if (topics.length > 0) {
48267
+ plan6.notes.push(`${topics.length} topic(s) now cross the boundary to "${member}"; the family run (\`wairon validate\` at ${label6(bound2)}) pairs them, while "${member}" validated alone reports them:`);
48268
+ for (const line of topics) plan6.notes.push(` ${line}`);
48269
+ }
46809
48270
  }
46810
48271
  function writeOne3(rehearsal, w) {
46811
48272
  const args = w.args;
@@ -47238,7 +48699,7 @@ function swap(journals) {
47238
48699
  function swapEntry(journal, entry) {
47239
48700
  const target = liveOf(journal.owner, entry.path);
47240
48701
  if (entry.action === "delete") {
47241
- guarded(journal, `remove ${target}`, () => remove(target));
48702
+ guarded(journal, `remove ${target}`, () => remove2(target));
47242
48703
  guarded(journal, `prune ${path46.dirname(target)}`, () => {
47243
48704
  for (const d of prune2(path46.dirname(target), journal.owner)) if (!journal.dirsRemoved.includes(d)) journal.dirsRemoved.push(d);
47244
48705
  });
@@ -47280,7 +48741,7 @@ function restoreOwner(journal) {
47280
48741
  }
47281
48742
  for (const entry of journal.entries.filter((e) => e.action === "create")) {
47282
48743
  const target = liveOf(journal.owner, entry.path);
47283
- if (digest2(target) === entry.stagedDigest) attempt(() => remove(target));
48744
+ if (digest2(target) === entry.stagedDigest) attempt(() => remove2(target));
47284
48745
  }
47285
48746
  for (const d of [...journal.dirsCreated].sort((a, b) => b.length - a.length)) attempt(() => prune2(d, path46.dirname(d)));
47286
48747
  for (const entry of journal.entries) {
@@ -47580,8 +49041,8 @@ function relockOf(plan6, changes) {
47580
49041
  const moved = movedOf(plan6);
47581
49042
  const relocated = moved !== null && approvalRecord(moved.from) !== null ? [moved.to] : [];
47582
49043
  if (plan6.request.verb !== "rename") return [...owners.filter((owner) => approvalRecord(owner) !== null), ...relocated];
47583
- const renamed = plan6.edits.find((e) => e.write?.call === "renameId")?.write?.root;
47584
- const first = renamed !== void 0 ? path47.resolve(renamed) : void 0;
49044
+ const renamed2 = plan6.edits.find((e) => e.write?.call === "renameId")?.write?.root;
49045
+ const first = renamed2 !== void 0 ? path47.resolve(renamed2) : void 0;
47585
49046
  return [...owners.filter((o) => o === first), ...owners.filter((o) => o !== first)];
47586
49047
  }
47587
49048
  function apply(plan6) {
@@ -47801,11 +49262,27 @@ var externalAdditionOutput = {
47801
49262
  unreachable: import_zod12.z.string().optional().describe("Set when the declaration was written but its producer could not be read: why, and that nothing was pinned."),
47802
49263
  ...staleServerOutput
47803
49264
  };
49265
+ var externalRemovalOutput = {
49266
+ alias: import_zod12.z.string(),
49267
+ removed: import_zod12.z.boolean().describe("Whether .wai/project.yaml no longer carries the declaration because this call took it out."),
49268
+ unpinned: import_zod12.z.boolean().describe("Whether the alias's lock entry and pinned snapshot were removed (or would be, on a dry run)."),
49269
+ refusal: import_zod12.z.string().optional().describe("Why nothing was removed, in one sentence."),
49270
+ ...staleServerOutput
49271
+ };
49272
+ var externalUseChangeOutput = {
49273
+ alias: import_zod12.z.string(),
49274
+ written: import_zod12.z.boolean().describe("Whether .wai/project.yaml now carries the changed `use` (false on a refusal, a dry run and when nothing changed)."),
49275
+ use: import_zod12.z.array(import_zod12.z.string()).describe("The `use` list after the change, in order."),
49276
+ added: import_zod12.z.array(import_zod12.z.string()).describe("The names this change imports that were not imported before."),
49277
+ removed: import_zod12.z.array(import_zod12.z.string()).describe("The names this change stops importing."),
49278
+ refusal: import_zod12.z.string().optional().describe("Why nothing was written, in one sentence."),
49279
+ ...staleServerOutput
49280
+ };
47804
49281
  function additionAnswer(request, addition) {
47805
49282
  if (addition.refusal) {
47806
49283
  return { ...structured(addition.refusal, { ...addition }), isError: true };
47807
49284
  }
47808
- const summary = request.dryRun ? `Dry run: would declare "${addition.alias}" \u2192 ${addition.project ?? addition.alias} as ${JSON.stringify(addition.declaration)}; the producer is checked and the external pinned on the real run. Nothing was written.` : `Declared "${addition.alias}" \u2192 ${addition.project ?? addition.alias} as ${JSON.stringify(addition.declaration)}` + (addition.pin ? `; pin ${addition.pin.outcome}${addition.pin.detail ? ` \u2014 ${addition.pin.detail}` : ""}` : "") + (addition.unreachable ? `. Declared but not pinned: ${addition.unreachable}` : "") + ".";
49285
+ const summary = request.dryRun ? `Dry run: would declare "${addition.alias}" \u2192 ${addition.project ?? addition.alias} as ${JSON.stringify(addition.declaration)}${addition.unreachable ? `; ${addition.unreachable}` : "; the producer was read and agrees"}. Nothing was written.` : `Declared "${addition.alias}" \u2192 ${addition.project ?? addition.alias} as ${JSON.stringify(addition.declaration)}` + (addition.pin ? `; pin ${addition.pin.outcome}${addition.pin.detail ? ` \u2014 ${addition.pin.detail}` : ""}` : "") + (addition.unreachable ? `. Declared but not pinned: ${addition.unreachable}` : "") + ".";
47809
49286
  return structured(summary, { ...addition });
47810
49287
  }
47811
49288
  var flowPartyOutput = import_zod12.z.object({
@@ -47824,7 +49301,9 @@ var networkFlowOutput = import_zod12.z.object({
47824
49301
  crosses: import_zod12.z.array(import_zod12.z.string()),
47825
49302
  via: import_zod12.z.string().optional(),
47826
49303
  evidence: import_zod12.z.array(import_zod12.z.string()),
47827
- verbs: import_zod12.z.array(import_zod12.z.string()).optional()
49304
+ verbs: import_zod12.z.array(import_zod12.z.string()).optional(),
49305
+ refusedBy: import_zod12.z.array(import_zod12.z.string()).optional().describe("The gate's error findings on this flow (`CODE: message`): the design refuses it, so it is never allowed."),
49306
+ flaggedBy: import_zod12.z.array(import_zod12.z.string()).optional().describe("The gate's warnings and notices on this flow (`CODE: message`): allowed, but marked.")
47828
49307
  });
47829
49308
  var networkFlowsOutput = {
47830
49309
  flows: import_zod12.z.array(networkFlowOutput).describe("One allowed flow per row: who reaches which Portal verb, over which transport and binding, through which networks and gateway, on what evidence."),
@@ -47833,7 +49312,15 @@ var networkFlowsOutput = {
47833
49312
  var flowExplanationOutput = {
47834
49313
  allowed: import_zod12.z.boolean().describe("Whether any flow of the design allows it."),
47835
49314
  flows: import_zod12.z.array(networkFlowOutput).describe("The flows that allow it."),
47836
- chain: import_zod12.z.array(import_zod12.z.string()).describe("Each step of the justification, one line each; when not allowed, what reaches the callee instead."),
49315
+ chain: import_zod12.z.array(import_zod12.z.string()).describe("Each step of the justification, one line each; when not allowed, why not: refused by the gate, in-process, an unknown party, or what reaches the callee instead."),
49316
+ inProcess: import_zod12.z.boolean().optional().describe("True when the callee takes no network flow at all: it is reached in-process, never over the network."),
49317
+ unknown: import_zod12.z.array(import_zod12.z.string()).optional().describe("The party names the design does not know: a typo, not a refusal."),
49318
+ refusedBy: import_zod12.z.array(import_zod12.z.string()).optional().describe("The gate's error findings on the flows between the pair: the design names them, the gate refuses them."),
49319
+ ...staleServerOutput
49320
+ };
49321
+ var networkDeclarationOutput = {
49322
+ written: import_zod12.z.boolean().describe("Whether .wai/project.yaml changed: false when it already read that way."),
49323
+ declared: import_zod12.z.boolean().describe("Whether the project now declares a network."),
47837
49324
  ...staleServerOutput
47838
49325
  };
47839
49326
  function flowLine(f) {
@@ -48338,6 +49825,7 @@ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
48338
49825
  "sdd_rename_component",
48339
49826
  "sdd_rename_method",
48340
49827
  "sdd_rename_type",
49828
+ "sdd_rename_field",
48341
49829
  "sdd_add_component",
48342
49830
  "sdd_define_interface",
48343
49831
  "sdd_set_endpoints",
@@ -48884,6 +50372,27 @@ ${renderChangeReport(report3)}`,
48884
50372
  }
48885
50373
  }
48886
50374
  );
50375
+ reg(
50376
+ server,
50377
+ "sdd_rename_field",
50378
+ {
50379
+ description: "Rename a field of a type and respell every reference to it in the bound tree: a foreign key's `references: <type>.<field>` naming it from any type (the type's own self-references included). The field keeps its place, type and description under the new name, and its old name joins the field's rename trace (previousNames), which the design export shows as `formerly` so a consumer holding the old name can follow the move. Rename traces and prose are never rewritten. Refuses, writing nothing: a type that does not exist (type-missing), one in another project (chained-type), a field the type does not declare (field-missing), a new name that is not an identifier (invalid-name), and a name another field of the type holds (name-taken) or retired (name-retired \u2014 unsetting the holder's previousNames releases it). Returns the type, from and to, and the specs rewritten. The approval reads the type as changed until the next `wairon lock`, as with any edit.",
50380
+ inputSchema: {
50381
+ id: import_zod12.z.string().describe("The type whose field is renamed (qualified by its owning subsystem if needed, `subsystem::id`)"),
50382
+ field: import_zod12.z.string().describe("The field's name as it stands"),
50383
+ newName: import_zod12.z.string().describe("Its new name: an identifier")
50384
+ }
50385
+ },
50386
+ ({ id, field, newName }) => {
50387
+ try {
50388
+ const bare = id.startsWith("::") ? id.slice(2) : id;
50389
+ const resolved = loadTypeSpec(bare) ? bare : id;
50390
+ return json(renameField(resolved, field, newName));
50391
+ } catch (e) {
50392
+ return errText(String(e));
50393
+ }
50394
+ }
50395
+ );
48887
50396
  reg(
48888
50397
  server,
48889
50398
  "sdd_move_methods",
@@ -48916,7 +50425,7 @@ ${renderChangeReport(report3)}`,
48916
50425
  componentType: import_zod12.z.enum(["Portal", "Orchestrator", "Supervisor", "Actor", "Store", "Index", "Query", "Registry", "Adapter", "Observer", "Repository"]).describe("The building block, or the pattern Repository. Specialist and Gateway are retired and cannot be authored: logic is an Orchestrator with a dependencyClass, a gateway is a Portal with the gateway variant"),
48917
50426
  owns: import_zod12.z.array(import_zod12.z.string()).optional().describe("Member block ids privately owned by this component (patterns only)"),
48918
50427
  dependsOn: import_zod12.z.array(import_zod12.z.string()).optional().describe("IDs of other components this collaborates with (facades or standalone blocks)"),
48919
- transport: import_zod12.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "CLI", "NamedPipe", "IPC", "JSONRPC", "InProcess", "Custom"]).optional().describe("Portal-only, and expected on every Portal: how callers reach its verbs, one vocabulary with the endpoint discriminator. Network: HTTP, gRPC, GraphQL, MessageBus, Custom. Local: CLI, NamedPipe, IPC, JSONRPC (JSON-RPC over stdio, e.g. a language server or stdio MCP). InProcess: a library (or FFI/DLL with an abi), whose verbs are its contract methods and bind no endpoint. Replaces the retired portalType (HTTP_API is HTTP). OMIT IT on any other componentType \u2014 passing it there is refused at the write (UNEXPECTED_PORTAL_FIELD), nothing is saved."),
50428
+ transport: import_zod12.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "CLI", "NamedPipe", "IPC", "JSONRPC", "InProcess", "Custom"]).optional().describe("Expected on every Portal: how callers reach its verbs, one vocabulary with the endpoint discriminator. Network: HTTP, gRPC, GraphQL, MessageBus, Custom. Local: CLI, NamedPipe, IPC, JSONRPC (JSON-RPC over stdio, e.g. a language server or stdio MCP). InProcess: a library (or FFI/DLL with an abi), whose verbs are its contract methods and bind no endpoint. Replaces the retired portalType (HTTP_API is HTTP). On an Adapter, optional: the transport it calls its target Portal over \u2014 omit it and it is taken from the Portals its dependsOn names when they share one; a stated one that differs from the target's is ADAPTER_TRANSPORT_MISMATCH. OMIT IT on any other componentType \u2014 passing it there is refused at the write (UNEXPECTED_PORTAL_FIELD), nothing is saved."),
48920
50429
  abi: import_zod12.z.string().optional().describe(`Portal-only, InProcess only: how a foreign language links this library \u2014 "c" (a C-ABI shared library, DLL or FFI) or "wasm" (a WebAssembly component). Omit for a native API in the project's targetLanguage.`),
48921
50430
  invokedBy: import_zod12.z.object({
48922
50431
  kind: import_zod12.z.enum(["entry"]).describe("entry: callers outside the design reach every verb of this Portal over its transport"),
@@ -48959,6 +50468,13 @@ ${renderChangeReport(report3)}`,
48959
50468
  if (invokedBy?.scope !== void 0 && transport !== void 0 && transportKind(transport) !== "network") {
48960
50469
  return errText(`Component "${id}": an entry scope applies only to a network transport, and "${transport}" is ${transportKind(transport)} \u2014 drop the scope.`);
48961
50470
  }
50471
+ let inferredTransport;
50472
+ if (componentType === "Adapter" && transport === void 0) {
50473
+ const portals = (dependsOn ?? []).map((dep) => loadComponentSpec(dep)).filter((c) => c?.componentType === "Portal" && c.transport !== void 0);
50474
+ const kinds = [...new Set(portals.map((p) => p.transport))];
50475
+ if (kinds.length === 1) inferredTransport = kinds[0];
50476
+ }
50477
+ const statedTransport = transport ?? inferredTransport;
48962
50478
  const restatement = {
48963
50479
  kind: "component",
48964
50480
  spec: {
@@ -48969,7 +50485,7 @@ ${renderChangeReport(report3)}`,
48969
50485
  componentType,
48970
50486
  owns: owns ?? [],
48971
50487
  dependsOn: dependsOn ?? [],
48972
- ...transport ? { transport } : {},
50488
+ ...statedTransport ? { transport: statedTransport } : {},
48973
50489
  ...abi ? { abi } : {},
48974
50490
  ...invokedBy ? { invokedBy } : {},
48975
50491
  ...basePath ? { basePath } : {},
@@ -48984,8 +50500,9 @@ ${renderChangeReport(report3)}`,
48984
50500
  status
48985
50501
  };
48986
50502
  const receipt = writeSpec(restatement);
50503
+ const inferred = inferredTransport ? ` Its transport, ${inferredTransport}, was taken from the Portal it calls (an Adapter calls its target's verbs over the target's transport).` : "";
48987
50504
  return writeReceipt(
48988
- `Successfully ${receipt.replacedExisting ? "re-authored" : "added"} L2 Component Spec "${name}" (${id}, ${componentType}).`,
50505
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "added"} L2 Component Spec "${name}" (${id}, ${componentType}).${inferred}`,
48989
50506
  receipt
48990
50507
  );
48991
50508
  } catch (e) {
@@ -49641,6 +51158,58 @@ ${testsBlock}` : ""}`, deletion);
49641
51158
  }
49642
51159
  }
49643
51160
  );
51161
+ reg(
51162
+ server,
51163
+ "sdd_remove_external",
51164
+ {
51165
+ description: "Remove one external of the bound project: its declaration in .wai/project.yaml and its pin (the lock entry and the pinned snapshot) together, so no orphaned pin is left; an orphaned pin alone (a declaration someone deleted by hand) is removed too. Refused in one sentence when the alias is neither declared nor pinned. References to the alias are the validator's to report afterwards. Use it instead of editing .wai/project.yaml by hand. A tree-scoped write.",
51166
+ inputSchema: {
51167
+ alias: import_zod12.z.string().describe("The external to remove"),
51168
+ dryRun: import_zod12.z.boolean().optional().describe("Answer what would be removed, writing nothing")
51169
+ },
51170
+ outputSchema: externalRemovalOutput
51171
+ },
51172
+ ({ alias, dryRun }) => {
51173
+ try {
51174
+ const removal = removeExternal2(alias, dryRun);
51175
+ if (removal.refusal) return { ...structured(removal.refusal, { ...removal }), isError: true };
51176
+ const what = [removal.removed || dryRun ? "its declaration" : "", removal.unpinned ? "its pin (lock entry and snapshot)" : ""].filter(Boolean).join(" and ") || "nothing more";
51177
+ return structured(dryRun ? `Dry run: would remove "${alias}" \u2014 ${what}. Nothing was written.` : `Removed "${alias}": ${what}.`, { ...removal });
51178
+ } catch (e) {
51179
+ return errMessage(e);
51180
+ }
51181
+ }
51182
+ );
51183
+ reg(
51184
+ server,
51185
+ "sdd_update_external",
51186
+ {
51187
+ description: "Add and remove `use` imports of one declared external, so this project's specs may name the producer's public names bare \u2014 e.g. so an `implements` of another project's trait may spell the producer's types as the producer does. An added name is checked against what the producer exports to this project (refused naming the closest exported names, or the audience that keeps it out). The pin is untouched. Use it instead of editing .wai/project.yaml by hand. A tree-scoped write.",
51188
+ inputSchema: {
51189
+ alias: import_zod12.z.string().describe("The declared external whose `use` changes"),
51190
+ addUse: import_zod12.z.array(import_zod12.z.string()).optional().describe("Public names to import bare (`*` for all)"),
51191
+ removeUse: import_zod12.z.array(import_zod12.z.string()).optional().describe("Names to stop importing"),
51192
+ dryRun: import_zod12.z.boolean().optional().describe("Answer the resulting list, writing nothing")
51193
+ },
51194
+ outputSchema: externalUseChangeOutput
51195
+ },
51196
+ ({ alias, addUse, removeUse, dryRun }) => {
51197
+ try {
51198
+ const change = updateExternalUse({
51199
+ alias,
51200
+ ...addUse !== void 0 ? { add: addUse } : {},
51201
+ ...removeUse !== void 0 ? { remove: removeUse } : {},
51202
+ ...dryRun ? { dryRun: true } : {}
51203
+ });
51204
+ if (change.refusal) return { ...structured(change.refusal, { ...change }), isError: true };
51205
+ const moved = [change.added.length ? `+ ${change.added.join(", ")}` : "", change.removed.length ? `- ${change.removed.join(", ")}` : ""].filter(Boolean).join("; ") || "no change";
51206
+ const list = change.use.length ? change.use.join(", ") : "(none)";
51207
+ return structured(`${dryRun ? "Dry run: " : ""}${alias}.use ${dryRun ? "would be" : change.written ? "is now" : "is already"} [${list}] (${moved}).`, { ...change });
51208
+ } catch (e) {
51209
+ return errMessage(e);
51210
+ }
51211
+ }
51212
+ );
49644
51213
  reg(
49645
51214
  server,
49646
51215
  "sdd_get_network_flows",
@@ -49675,13 +51244,35 @@ ${testsBlock}` : ""}`, deletion);
49675
51244
  ({ from, to }) => {
49676
51245
  try {
49677
51246
  const explanation = explain2(from, to);
49678
- const head2 = explanation.allowed ? `${from} may reach ${to}:` : `${from} may NOT reach ${to}:`;
51247
+ const head2 = explanation.allowed ? `${from} may reach ${to}:` : explanation.unknown?.length ? `unknown party: ${explanation.unknown.join(", ")}:` : explanation.refusedBy?.length ? `${from} may NOT reach ${to} \u2014 the gate refuses the flow:` : explanation.inProcess ? `${to} is reached in-process \u2014 never a network flow:` : `${from} may NOT reach ${to}:`;
49679
51248
  return structured([head2, ...explanation.chain.map((l) => ` ${l}`)].join("\n"), { ...explanation });
49680
51249
  } catch (e) {
49681
51250
  return errMessage(e);
49682
51251
  }
49683
51252
  }
49684
51253
  );
51254
+ reg(
51255
+ server,
51256
+ "sdd_set_network",
51257
+ {
51258
+ description: "Declare (declared: true, with an optional description) or remove (declared: false) the bound project's `network` in .wai/project.yaml: that this project and every member below it form one isolated network boundary. It changes which rules fire (GATEWAY_BYPASSED, ENTRY_UNPROVEN, ...), so run sdd_validate_tree after it; it is part of the gate identity, so the human re-locks. Use it instead of editing .wai/project.yaml by hand. A project-scoped write.",
51259
+ inputSchema: {
51260
+ declared: import_zod12.z.boolean().describe("true declares the network, false removes the declaration"),
51261
+ description: import_zod12.z.string().optional().describe("What the network encloses, in the project's words (shown on the network diagram); only with declared: true")
51262
+ },
51263
+ outputSchema: networkDeclarationOutput
51264
+ },
51265
+ ({ declared, description }) => {
51266
+ try {
51267
+ if (!declared && description !== void 0) return errText("sdd_set_network: a description goes with declared: true \u2014 removing the declaration takes none.");
51268
+ const written = declare2(declared, description ?? null);
51269
+ const text3 = declared ? written ? `Declared this project's network${description ? ` ("${description}")` : ""}. Run sdd_validate_tree: the network rules now judge the design. The human re-locks once it validates.` : "This project already declares that network: nothing was written." : written ? "Removed this project's network declaration. Run sdd_validate_tree; the human re-locks." : "This project declares no network: nothing was removed.";
51270
+ return structured(text3, { written, declared });
51271
+ } catch (e) {
51272
+ return errMessage(e);
51273
+ }
51274
+ }
51275
+ );
49685
51276
  reg(
49686
51277
  server,
49687
51278
  "sdd_get_agent_brief",
@@ -49883,6 +51474,7 @@ function loadProjectConfig3() {
49883
51474
  SurfaceSnapshotSchema,
49884
51475
  SurfaceTypeDefSchema,
49885
51476
  SurfaceTypeExportSchema,
51477
+ SurfaceUnresolvedExportSchema,
49886
51478
  SwitchCaseSchema,
49887
51479
  SystemPublicInterfaceSchema,
49888
51480
  SystemSpecSchema,
@@ -49916,12 +51508,14 @@ function loadProjectConfig3() {
49916
51508
  approvalKeyIn,
49917
51509
  approvalRecord,
49918
51510
  approvalVerdict,
51511
+ approvedStatuses,
49919
51512
  assertContainedProjectPath,
49920
51513
  assertProjectInitialized,
49921
51514
  attachRetiredMounts,
49922
51515
  backendCommand,
49923
51516
  backfillChainedSubprojectConfigs,
49924
51517
  backupBeside,
51518
+ bindDeclaredExternal,
49925
51519
  bodySitesOf,
49926
51520
  buildCanvasDataModel,
49927
51521
  buildProjectGraph,
@@ -49933,6 +51527,7 @@ function loadProjectConfig3() {
49933
51527
  canonicalTypeRef,
49934
51528
  canonicalTypeText,
49935
51529
  captureApprovedSpecs,
51530
+ carriedFactChanges,
49936
51531
  carryRetiredReachForms,
49937
51532
  checkDeclarativePack,
49938
51533
  clearLoaderIssues,
@@ -49944,6 +51539,7 @@ function loadProjectConfig3() {
49944
51539
  composeRuleSequence,
49945
51540
  computeGateStateId,
49946
51541
  computeOwnDesignId,
51542
+ computeOwnDesignIdAsRecorded,
49947
51543
  computeOwnStateId,
49948
51544
  computeStateId,
49949
51545
  computeStateIdAt,
@@ -50047,6 +51643,7 @@ function loadProjectConfig3() {
50047
51643
  injectGuide,
50048
51644
  installPackFromDirectory,
50049
51645
  interfaceCanonicalTypes,
51646
+ interfaceDesignView,
50050
51647
  interfaceGenericParameters,
50051
51648
  internalizeMember,
50052
51649
  isDraftSubsystem,
@@ -50066,6 +51663,7 @@ function loadProjectConfig3() {
50066
51663
  languageOfSourcePath,
50067
51664
  lineEndingFor,
50068
51665
  listDirectChainedSubprojects,
51666
+ listExternalConsumers,
50069
51667
  listExternals,
50070
51668
  listFiles,
50071
51669
  listFilesRecursive,
@@ -50167,6 +51765,7 @@ function loadProjectConfig3() {
50167
51765
  removeSnapshot,
50168
51766
  renameAlias,
50169
51767
  renameComponent,
51768
+ renameField,
50170
51769
  renameId,
50171
51770
  renameMethod,
50172
51771
  renameType,
@@ -50181,6 +51780,7 @@ function loadProjectConfig3() {
50181
51780
  resolveChainingParent,
50182
51781
  resolveDomains,
50183
51782
  resolveExpectedOutputPaths,
51783
+ resolveExternalCandidate,
50184
51784
  resolveExternals,
50185
51785
  resolveImport,
50186
51786
  resolveInstalledPack,
@@ -50209,6 +51809,7 @@ function loadProjectConfig3() {
50209
51809
  setExecutionTier,
50210
51810
  setId,
50211
51811
  setLogLevel,
51812
+ setNetwork,
50212
51813
  setProjectRoot,
50213
51814
  setProjectType,
50214
51815
  settledSpecPaths,
@@ -50222,6 +51823,7 @@ function loadProjectConfig3() {
50222
51823
  stepGraph,
50223
51824
  storedMethodSignature,
50224
51825
  storedTypeMethod,
51826
+ subsystemDesignView,
50225
51827
  syncContextFiles,
50226
51828
  technologyName,
50227
51829
  technologyTokens,
@@ -50235,6 +51837,7 @@ function loadProjectConfig3() {
50235
51837
  typeDialectFor,
50236
51838
  typeGenericParameters,
50237
51839
  typeIsMany,
51840
+ typeMappingFor,
50238
51841
  typeMatchesRef,
50239
51842
  typeNamedRefs,
50240
51843
  typeProblemEnumProposal,