@wairon/cli 5.1.1-dev.79 → 5.1.1-dev.80

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
@@ -3451,7 +3451,9 @@ __export(src_exports, {
3451
3451
  NarrativeStepTypeSchema: () => NarrativeStepTypeSchema,
3452
3452
  OutputTargetSchema: () => OutputTargetSchema,
3453
3453
  PATTERN_TYPES: () => PATTERN_TYPES,
3454
+ PROJECT_ID_RE: () => PROJECT_ID_RE,
3454
3455
  PROJECT_KINDS: () => PROJECT_KINDS,
3456
+ PUBLIC_NAME_RE: () => PUBLIC_NAME_RE,
3455
3457
  PackSelectionSchema: () => PackSelectionSchema,
3456
3458
  ParallelBranchSchema: () => ParallelBranchSchema,
3457
3459
  PathsConfigSchema: () => PathsConfigSchema,
@@ -3486,6 +3488,7 @@ __export(src_exports, {
3486
3488
  SurfaceOriginSchema: () => SurfaceOriginSchema,
3487
3489
  SurfaceSnapshotSchema: () => SurfaceSnapshotSchema,
3488
3490
  SurfaceTypeDefSchema: () => SurfaceTypeDefSchema,
3491
+ SurfaceTypeExportSchema: () => SurfaceTypeExportSchema,
3489
3492
  SwitchCaseSchema: () => SwitchCaseSchema,
3490
3493
  SystemPublicInterfaceSchema: () => SystemPublicInterfaceSchema,
3491
3494
  SystemSpecSchema: () => SystemSpecSchema,
@@ -3556,10 +3559,12 @@ __export(src_exports, {
3556
3559
  discoverPacks: () => discoverPacks,
3557
3560
  dryRunSerializeSpecs: () => dryRunSerializeSpecs,
3558
3561
  effectiveDetail: () => effectiveDetail,
3562
+ effectiveProjectId: () => effectiveProjectId,
3559
3563
  emptyExtensions: () => emptyExtensions,
3560
3564
  ensureDir: () => ensureDir,
3561
3565
  ensureProjectInitialized: () => ensureProjectInitialized,
3562
3566
  exportSpecTree: () => exportSpecTree,
3567
+ exportTargetKey: () => exportTargetKey,
3563
3568
  externalizeSubsystem: () => externalizeSubsystem,
3564
3569
  extractTypeIdentifiers: () => extractTypeIdentifiers,
3565
3570
  factsFor: () => factsFor,
@@ -3600,6 +3605,7 @@ __export(src_exports, {
3600
3605
  internalizeSubsystem: () => internalizeSubsystem,
3601
3606
  isDraftSubsystem: () => isDraftSubsystem,
3602
3607
  isLogic: () => isLogic,
3608
+ isOwnComponentEntry: () => isOwnComponentEntry,
3603
3609
  isPattern: () => isPattern,
3604
3610
  isProjectInitialized: () => isProjectInitialized,
3605
3611
  isProvidedBy: () => isProvidedBy,
@@ -3653,6 +3659,7 @@ __export(src_exports, {
3653
3659
  pathKey: () => pathKey,
3654
3660
  pinInstalledPacksAsSelections: () => pinInstalledPacksAsSelections,
3655
3661
  projectConfigExists: () => projectConfigExists,
3662
+ projectIdentity: () => projectIdentity,
3656
3663
  provisionProject: () => provisionProject,
3657
3664
  qualifiedTypeId: () => qualifiedTypeId,
3658
3665
  readFileOrNull: () => readFileOrNull,
@@ -3678,7 +3685,9 @@ __export(src_exports, {
3678
3685
  resolveExpectedOutputPaths: () => resolveExpectedOutputPaths,
3679
3686
  resolveImport: () => resolveImport,
3680
3687
  resolveInstalledPack: () => resolveInstalledPack,
3688
+ resolveProjectExports: () => resolveProjectExports,
3681
3689
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
3690
+ resolveSubsystemExports: () => resolveSubsystemExports,
3682
3691
  resolveVariantGuidance: () => resolveVariantGuidance,
3683
3692
  retireSpecialists: () => retireSpecialists,
3684
3693
  runWithProjectBinding: () => runWithProjectBinding,
@@ -4462,7 +4471,16 @@ var ProjectConfigSchema = import_zod3.z.object({
4462
4471
  * CLI versions.
4463
4472
  */
4464
4473
  schemaVersion: import_zod3.z.string().default("1.0.0"),
4465
- /** Human-readable project name */
4474
+ /**
4475
+ * The project's stable identity: a slug of [a-z0-9-_.] that starts and ends
4476
+ * alphanumeric (PROJECT_ID_RE). Optional in the file: a project that declares
4477
+ * none answers to its name slugified (effectiveProjectId) until a deliberate
4478
+ * writer sets one — an ordinary save never writes the default. Deliberately a plain
4479
+ * string here rather than the grammar: a malformed id must be REPORTED
4480
+ * (PROJECT_ID_AMBIGUOUS), not make the whole configuration unreadable.
4481
+ */
4482
+ id: import_zod3.z.string().optional(),
4483
+ /** Human-readable display name. Not an identity — `id` is. */
4466
4484
  name: import_zod3.z.string(),
4467
4485
  /**
4468
4486
  * The type/profile of the project, which configures targeted guidelines, rules,
@@ -4571,6 +4589,40 @@ function declaredPackNames(config) {
4571
4589
  function declaredProfileIds(config) {
4572
4590
  return [...new Set(config.profileSelection?.profileIds ?? [])];
4573
4591
  }
4592
+ var PROJECT_ID_RE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
4593
+ function slugifyProjectName(name) {
4594
+ const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "");
4595
+ return slug === "" ? null : slug;
4596
+ }
4597
+ function effectiveProjectId(config) {
4598
+ if (config.id !== void 0) return config.id;
4599
+ return slugifyProjectName(config.name);
4600
+ }
4601
+ function projectIdentity(config, lockedProjectId) {
4602
+ const id = effectiveProjectId(config);
4603
+ const source = config.id !== void 0 ? "declared" : id !== null ? "defaulted" : "none";
4604
+ const problems = [];
4605
+ if (source === "defaulted") {
4606
+ problems.push({ kind: "defaulted", detail: `no id is declared; the name "${config.name}" yields "${id}"` });
4607
+ } else if (source === "none") {
4608
+ problems.push({ kind: "ambiguous", detail: `no id is declared, and the name "${config.name}" yields no id` });
4609
+ } else if (!PROJECT_ID_RE.test(config.id)) {
4610
+ problems.push({ kind: "ambiguous", detail: `the declared id "${config.id}" breaks the project-id grammar` });
4611
+ }
4612
+ if (lockedProjectId !== void 0 && id !== lockedProjectId) {
4613
+ problems.push({
4614
+ kind: "changed",
4615
+ detail: id === null ? `the lock approved "${lockedProjectId}", and the project now has no id` : `the lock approved "${lockedProjectId}", and the project now resolves to "${id}"`
4616
+ });
4617
+ }
4618
+ return {
4619
+ ...id !== null ? { id } : {},
4620
+ source,
4621
+ name: config.name,
4622
+ ...lockedProjectId !== void 0 ? { lockedId: lockedProjectId } : {},
4623
+ problems
4624
+ };
4625
+ }
4574
4626
 
4575
4627
  // src/config/project-config.ts
4576
4628
  function waiDirAt(root) {
@@ -4858,18 +4910,19 @@ function registryOver(store, root) {
4858
4910
  if (!config) throw new ProjectNotInitializedError();
4859
4911
  return config;
4860
4912
  };
4913
+ const save = (config) => store.write(config);
4861
4914
  return {
4862
4915
  create(config) {
4863
4916
  if (store.exists()) {
4864
4917
  throw new WaironError(`A project configuration already exists at ${root}; creating one never overwrites it.`);
4865
4918
  }
4866
- store.write(config);
4919
+ save(config);
4867
4920
  },
4868
4921
  upsertPackSelection(selection) {
4869
4922
  const config = current2();
4870
4923
  const existing = packsOf(config);
4871
4924
  const without = existing.filter((entry) => typeof entry === "string" || entry.name !== selection.name);
4872
- store.write(withPacks(config, [...without, selection]));
4925
+ save(withPacks(config, [...without, selection]));
4873
4926
  return without.length !== existing.length;
4874
4927
  },
4875
4928
  removePackSelection(packName) {
@@ -4877,24 +4930,24 @@ function registryOver(store, root) {
4877
4930
  const existing = packsOf(config);
4878
4931
  const remaining = existing.filter((entry) => typeof entry === "string" || entry.name !== packName);
4879
4932
  if (remaining.length === existing.length) return false;
4880
- store.write(withPacks(config, remaining));
4933
+ save(withPacks(config, remaining));
4881
4934
  return true;
4882
4935
  },
4883
4936
  setProjectType(projectType) {
4884
- store.write({ ...current2(), projectType });
4937
+ save({ ...current2(), projectType });
4885
4938
  },
4886
4939
  recordProfileSelection(selection) {
4887
- store.write({ ...current2(), profileSelection: selection });
4940
+ save({ ...current2(), profileSelection: selection });
4888
4941
  },
4889
4942
  setExecutionTier(tier) {
4890
4943
  const config = current2();
4891
- store.write({ ...config, execution: { ...config.execution, tier } });
4944
+ save({ ...config, execution: { ...config.execution, tier } });
4892
4945
  },
4893
4946
  registerPackRef(ref) {
4894
4947
  const config = current2();
4895
4948
  const packs = packsOf(config);
4896
4949
  if (packs.includes(ref)) return false;
4897
- store.write(withPacks(config, [...packs, ref]));
4950
+ save(withPacks(config, [...packs, ref]));
4898
4951
  return true;
4899
4952
  },
4900
4953
  deregisterPackRef(ref) {
@@ -4902,16 +4955,16 @@ function registryOver(store, root) {
4902
4955
  const packs = packsOf(config);
4903
4956
  const remaining = packs.filter((entry) => entry !== ref);
4904
4957
  if (remaining.length === packs.length) return false;
4905
- store.write(withPacks(config, remaining));
4958
+ save(withPacks(config, remaining));
4906
4959
  return true;
4907
4960
  },
4908
4961
  markSelectionsBundled(bundled) {
4909
4962
  const config = current2();
4910
- store.write(withPacks(config, bundleInPlace(packsOf(config), bundled)));
4963
+ save(withPacks(config, bundleInPlace(packsOf(config), bundled)));
4911
4964
  },
4912
4965
  pinGlobalPacksAsSelections(selections) {
4913
4966
  const config = current2();
4914
- store.write({
4967
+ save({
4915
4968
  ...config,
4916
4969
  extensions: { ...config.extensions, packs: [...packsOf(config), ...selections], useGlobalPacks: false }
4917
4970
  });
@@ -5406,15 +5459,21 @@ var DiagramConfigSchema = import_zod8.z.object({
5406
5459
  var SURFACE_AUDIENCES = ["project", "department", "instance", "partner", "external"];
5407
5460
  var SurfaceAudienceSchema = import_zod8.z.enum(SURFACE_AUDIENCES);
5408
5461
  var SystemPublicInterfaceSchema = import_zod8.z.object({
5409
- /** Stable public interface id within the system. */
5462
+ /** Legacy public name, read as `as` when `as` is absent. */
5410
5463
  id: import_zod8.z.string().optional(),
5411
5464
  name: import_zod8.z.string().optional(),
5412
- /** Subsystem publishing the backing L1 public interface. */
5465
+ /** Legacy source subsystem, read as `from` when `from` is absent. */
5413
5466
  subsystem: import_zod8.z.string().optional(),
5414
- /** Portal (or compatible published component) backing this entry. */
5467
+ /** The component re-exported one to one; without it and without `typeDef`, a wildcard. */
5415
5468
  component: import_zod8.z.string().optional(),
5416
- /** Optional L3 interface id backing the surface. */
5469
+ /** The L3 interface the export narrows to. */
5417
5470
  interface: import_zod8.z.string().optional(),
5471
+ /** The source subsystem the entry re-exports from. */
5472
+ from: import_zod8.z.string().optional(),
5473
+ /** A type exported as is, giving it a public name. */
5474
+ typeDef: import_zod8.z.string().optional(),
5475
+ /** The public name: defaults to `id`, then `interface`, then `component` or `typeDef`. */
5476
+ as: import_zod8.z.string().optional(),
5418
5477
  /** Surface kind: REST, GraphQL, MessageBus, RPC, or Custom. */
5419
5478
  type: import_zod8.z.string().optional(),
5420
5479
  details: import_zod8.z.string().optional(),
@@ -5455,19 +5514,33 @@ var SystemSpecSchema = import_zod8.z.object({
5455
5514
  });
5456
5515
  var PublicInterfaceTypeSchema = import_zod8.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]);
5457
5516
  var PublicInterfaceSchema = import_zod8.z.object({
5458
- type: PublicInterfaceTypeSchema,
5459
- details: import_zod8.z.string(),
5460
- /** The L2 component that realizes this public interface (the subsystem's published surface). */
5517
+ /** Transport kind of an own item; a re-export inherits its target's. */
5518
+ type: PublicInterfaceTypeSchema.optional(),
5519
+ /** Consumer-facing description of an own item; a re-export inherits its target's. */
5520
+ details: import_zod8.z.string().optional(),
5521
+ /** For an own item, the L2 component that realizes it; for a re-export, the component re-exported from `from`. */
5461
5522
  component: SpecIdSchema.optional(),
5462
- /** Optional L3 interface on that component backing this entry. */
5523
+ /** Optional L3 interface the entry narrows to. */
5463
5524
  interface: SpecIdSchema.optional(),
5525
+ /** Re-export source: another subsystem of the same project. */
5526
+ from: SpecIdSchema.optional(),
5527
+ /** A type exported as is: owned by this subsystem, or re-exported from `from`. */
5528
+ typeDef: SpecIdSchema.optional(),
5529
+ /** The public name; defaults to the narrowed interface, then the component or type id. */
5530
+ as: import_zod8.z.string().optional(),
5464
5531
  /**
5465
5532
  * The subsystems this surface is published to. Absent, anyone may depend on
5466
5533
  * it; present, only these subsystems may — when every entry publishing the
5467
5534
  * component names consumers, the union of those lists is the whole set.
5468
5535
  */
5469
5536
  consumers: import_zod8.z.array(SpecIdSchema).optional()
5537
+ }).refine((pi) => pi.from !== void 0 || pi.typeDef !== void 0 || pi.type !== void 0 && pi.details !== void 0, {
5538
+ message: "An own component entry states its `type` and `details`; only a re-export (`from`) or a type export (`typeDef`) may leave them out.",
5539
+ path: ["type"]
5470
5540
  });
5541
+ function isOwnComponentEntry(pi) {
5542
+ return pi.from === void 0 && pi.typeDef === void 0;
5543
+ }
5471
5544
  var TrustedLinkSchema = import_zod8.z.object({
5472
5545
  /** The peer subsystem id this link sanctions tight coupling with. */
5473
5546
  subsystem: SpecIdSchema,
@@ -5784,8 +5857,8 @@ var MethodParamSchema = import_zod8.z.object({
5784
5857
  var FindingDeclarationSchema = import_zod8.z.object({
5785
5858
  /** UPPER_SNAKE and unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>). */
5786
5859
  code: import_zod8.z.string().regex(/^[A-Z][A-Z0-9_]*$/, "Finding code must be UPPER_SNAKE, e.g. UNREALIZED_FINDING"),
5787
- /** The default before project severity overrides and draft-context downgrades. */
5788
- severity: import_zod8.z.enum(["error", "warning"]),
5860
+ /** The default before project severity overrides and draft-context downgrades; a notice is reported but never fails the gate. */
5861
+ severity: import_zod8.z.enum(["error", "warning", "notice"]),
5789
5862
  /** One line saying what the finding means. */
5790
5863
  summary: import_zod8.z.string().min(1, "Finding summary must say what the finding means")
5791
5864
  });
@@ -6329,6 +6402,14 @@ var SurfaceTypeDefSchema = import_zod8.z.object({
6329
6402
  optional: import_zod8.z.boolean().optional()
6330
6403
  })).default([])
6331
6404
  });
6405
+ var SurfaceTypeExportSchema = import_zod8.z.object({
6406
+ /** The type's public name in the producer's export table. */
6407
+ id: import_zod8.z.string(),
6408
+ /** The id of the type's definition in the snapshot's type closure. */
6409
+ type: import_zod8.z.string(),
6410
+ /** Exposure level from the L0 entry. */
6411
+ audience: import_zod8.z.string().default("instance")
6412
+ });
6332
6413
  var SurfaceContractEntrySchema = import_zod8.z.object({
6333
6414
  id: import_zod8.z.string(),
6334
6415
  name: import_zod8.z.string(),
@@ -6349,7 +6430,11 @@ var SurfaceContractEntrySchema = import_zod8.z.object({
6349
6430
  * emits it as OpenAPI securitySchemes/security. */
6350
6431
  auth: PortalAuthSchema.optional(),
6351
6432
  /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
6352
- basePath: import_zod8.z.string().optional()
6433
+ basePath: import_zod8.z.string().optional(),
6434
+ /** Stereotype of the backing component; absent on snapshots written before exports carried it. */
6435
+ componentType: import_zod8.z.string().optional(),
6436
+ /** The L3 interface the export narrows to, when it narrows. */
6437
+ interface: import_zod8.z.string().optional()
6353
6438
  });
6354
6439
  var SurfaceSnapshotSchema = import_zod8.z.object({
6355
6440
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -6361,8 +6446,12 @@ var SurfaceSnapshotSchema = import_zod8.z.object({
6361
6446
  version: import_zod8.z.string().optional(),
6362
6447
  generatedAt: import_zod8.z.string(),
6363
6448
  interfaces: import_zod8.z.array(SurfaceContractEntrySchema).default([]),
6364
- /** Transitive type closure of every exported signature — self-contained. */
6365
- types: import_zod8.z.array(SurfaceTypeDefSchema).default([])
6449
+ /** Transitive type closure of every exported signature and exported type — self-contained. */
6450
+ types: import_zod8.z.array(SurfaceTypeDefSchema).default([]),
6451
+ /** The producing project's effective id, beside the display-oriented projectName. */
6452
+ projectId: import_zod8.z.string().optional(),
6453
+ /** The types the producer exports by name, listed apart from the contract entries. */
6454
+ exportedTypes: import_zod8.z.array(SurfaceTypeExportSchema).optional()
6366
6455
  });
6367
6456
  var NamedOpenApiSpecSchema = import_zod8.z.object({
6368
6457
  portalId: import_zod8.z.string(),
@@ -6905,6 +6994,12 @@ function ambiguityMessage(resolution, subject, ref) {
6905
6994
  return `${subject} cross-tree component "${ref}", which the surface snapshots of ${providers.map((p) => `"${p}"`).join(", ")} expose with different contracts \u2014 the reference matches more than one declared contract, so none of them can judge it. Name the provider it means (super::<provider>::${local}), or remove the snapshot that no longer applies.`;
6906
6995
  }
6907
6996
 
6997
+ // src/models/exports.ts
6998
+ var PUBLIC_NAME_RE = /^[a-z0-9_-]+$/;
6999
+ function exportTargetKey(e) {
7000
+ return e.kind === "type" ? `type:${e.typeDef}` : `component:${e.component}${e.interface ? `#${e.interface}` : ""}`;
7001
+ }
7002
+
6908
7003
  // src/core/narrative-labels.ts
6909
7004
  var SCALAR_REFS = [
6910
7005
  ["onTrueLabel", "onTrueStep"],
@@ -6924,7 +7019,7 @@ function resolveNarrativeLabels(methodName, steps) {
6924
7019
  else labels.set(s.label, s.stepNumber);
6925
7020
  }
6926
7021
  }
6927
- const lookup = (ref, where) => {
7022
+ const lookup2 = (ref, where) => {
6928
7023
  if (typeof ref !== "string" || !ref.length) {
6929
7024
  errors.push(`${where} is not a label string`);
6930
7025
  return void 0;
@@ -6938,7 +7033,7 @@ function resolveNarrativeLabels(methodName, steps) {
6938
7033
  for (const s of steps) {
6939
7034
  for (const [labelField, stepField] of SCALAR_REFS) {
6940
7035
  if (s[labelField] === void 0) continue;
6941
- const n = lookup(s[labelField], `step ${s.stepNumber} ${labelField}`);
7036
+ const n = lookup2(s[labelField], `step ${s.stepNumber} ${labelField}`);
6942
7037
  if (n !== void 0) {
6943
7038
  if (typeof s[stepField] === "number" && s[stepField] !== n) {
6944
7039
  errors.push(`step ${s.stepNumber} sets both ${stepField}=${s[stepField]} and ${labelField}="${s[labelField]}" (= step ${n}) \u2014 they disagree`);
@@ -6954,7 +7049,7 @@ function resolveNarrativeLabels(methodName, steps) {
6954
7049
  for (const entry of arr) {
6955
7050
  if (!entry || typeof entry !== "object" || entry.label === void 0) continue;
6956
7051
  const e = entry;
6957
- const n = lookup(e.label, `step ${s.stepNumber} ${key} entry`);
7052
+ const n = lookup2(e.label, `step ${s.stepNumber} ${key} entry`);
6958
7053
  if (n !== void 0) {
6959
7054
  if (typeof e.step === "number" && e.step !== n) {
6960
7055
  errors.push(`step ${s.stepNumber} ${key} entry sets both step=${e.step} and label="${e.label}" (= step ${n}) \u2014 they disagree`);
@@ -7394,13 +7489,17 @@ function buildCanvasModel(issues = []) {
7394
7489
  const componentIds = new Set(components.map((c) => c.id));
7395
7490
  const publicComponents = /* @__PURE__ */ new Set();
7396
7491
  for (const sub of subsystems) {
7397
- for (const pi of sub.publicInterfaces) {
7492
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
7398
7493
  if (pi.component) publicComponents.add(pi.component);
7399
7494
  }
7400
7495
  }
7401
7496
  const apiTagOf = /* @__PURE__ */ new Map();
7402
- for (const pi of system?.publicInterfaces ?? []) {
7403
- if (pi.component && pi.id) apiTagOf.set(pi.component, pi.id);
7497
+ if (system) {
7498
+ for (const entry of resolveProjectExports().entries) {
7499
+ if (entry.kind === "component" && entry.component && !apiTagOf.has(entry.component)) {
7500
+ apiTagOf.set(entry.component, entry.publicName);
7501
+ }
7502
+ }
7404
7503
  }
7405
7504
  const patternTypes = /* @__PURE__ */ new Set(["Repository", "FeatureComponent", "RouterComponent"]);
7406
7505
  const ownerOf2 = /* @__PURE__ */ new Map();
@@ -7566,7 +7665,7 @@ function buildCanvasModel(issues = []) {
7566
7665
  }
7567
7666
  const portalOf = /* @__PURE__ */ new Map();
7568
7667
  for (const sub of subsystems) {
7569
- for (const pi of sub.publicInterfaces) {
7668
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
7570
7669
  if (pi.component && !portalOf.has(sub.id)) portalOf.set(sub.id, pi.component);
7571
7670
  }
7572
7671
  }
@@ -11837,19 +11936,25 @@ function stateIdString() {
11837
11936
  const s = computeStateId();
11838
11937
  return `${s.algorithm}:${s.digest}`;
11839
11938
  }
11840
- function computeTypeClosure(entries, types) {
11939
+ function qualifiedTypeId2(spec) {
11940
+ return spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
11941
+ }
11942
+ function computeTypeClosure(entries, types, exported = []) {
11841
11943
  const included = /* @__PURE__ */ new Map();
11842
11944
  const queue = [];
11945
+ const include = (spec) => {
11946
+ const key = qualifiedTypeId2(spec);
11947
+ if (included.has(key)) return;
11948
+ included.set(key, spec);
11949
+ queue.push(key);
11950
+ };
11843
11951
  const enqueueRef = (ref) => {
11844
11952
  if (BUILTIN_TYPES.has(ref.toLowerCase())) return;
11845
11953
  for (const spec of types) {
11846
- const qualifiedId = spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
11847
- if (matchTypeRef(ref, qualifiedId) && !included.has(spec.id)) {
11848
- included.set(spec.id, spec);
11849
- queue.push(spec.id);
11850
- }
11954
+ if (matchTypeRef(ref, qualifiedTypeId2(spec))) include(spec);
11851
11955
  }
11852
11956
  };
11957
+ for (const spec of exported) include(spec);
11853
11958
  for (const entry of entries) {
11854
11959
  for (const m of entry.methods) {
11855
11960
  for (const ref of methodTypeRefs(m)) enqueueRef(ref);
@@ -11861,63 +11966,87 @@ function computeTypeClosure(entries, types) {
11861
11966
  for (const ref of extractTypeIdentifiers(field.type)) enqueueRef(ref);
11862
11967
  }
11863
11968
  }
11864
- return [...included.values()].map((t) => ({
11865
- id: t.id,
11866
- name: t.name,
11867
- kind: t.kind,
11868
- fields: t.fields.map((f) => ({
11869
- name: f.name,
11870
- type: f.type,
11871
- ...f.description ? { description: f.description } : {},
11872
- ...f.optional ? { optional: true } : {}
11873
- }))
11874
- }));
11969
+ const usedIds = /* @__PURE__ */ new Set();
11970
+ return [...included.entries()].map(([qualified, t]) => {
11971
+ const id = usedIds.has(t.id) ? qualified : t.id;
11972
+ usedIds.add(id);
11973
+ return {
11974
+ id,
11975
+ name: t.name,
11976
+ kind: t.kind,
11977
+ fields: t.fields.map((f) => ({
11978
+ name: f.name,
11979
+ type: f.type,
11980
+ ...f.description ? { description: f.description } : {},
11981
+ ...f.optional ? { optional: true } : {}
11982
+ }))
11983
+ };
11984
+ });
11985
+ }
11986
+ function boundProjectId() {
11987
+ try {
11988
+ const config = loadProjectConfig();
11989
+ return config ? effectiveProjectId(config) ?? void 0 : void 0;
11990
+ } catch {
11991
+ return void 0;
11992
+ }
11993
+ }
11994
+ function contractEntry(entry, comp, interfaces) {
11995
+ const methods = interfaces.filter((i) => i.component === comp.id && (!entry.interface || i.id === entry.interface)).flatMap((i) => i.methods);
11996
+ return {
11997
+ id: entry.publicName,
11998
+ name: entry.name ?? comp.name,
11999
+ audience: entry.audience ?? "instance",
12000
+ type: entry.type ?? "Custom",
12001
+ component: comp.id,
12002
+ methods,
12003
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
12004
+ // Project the backing Portal's auth + basePath so the codec can emit
12005
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
12006
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
12007
+ ...comp.basePath ? { basePath: comp.basePath } : {},
12008
+ details: entry.details ?? "",
12009
+ ...entry.version ? { version: entry.version } : {},
12010
+ ...entry.stability ? { stability: entry.stability } : {},
12011
+ componentType: comp.componentType,
12012
+ ...entry.interface ? { interface: entry.interface } : {}
12013
+ };
11875
12014
  }
11876
12015
  function projectOwnSurface(maxAudience) {
11877
12016
  const system = loadSystemSpec();
11878
12017
  if (!system) {
11879
12018
  throw new Error("Cannot project a surface: the L0 system spec is missing.");
11880
12019
  }
11881
- const subsystems = loadSubsystemSpecs();
12020
+ const table = resolveProjectExports();
11882
12021
  const components = loadComponentSpecs();
11883
12022
  const interfaces = loadInterfaceSpecs();
11884
12023
  const types = loadTypeSpecs();
11885
12024
  const floor = audienceRank(maxAudience);
11886
- const rawEntries = system.publicInterfaces ?? [];
12025
+ const visible = table.entries.filter((e) => audienceRank(e.audience ?? "instance") >= floor);
11887
12026
  const entries = [];
11888
- for (const raw of rawEntries) {
11889
- const audience = raw.audience ?? "instance";
11890
- if (audienceRank(audience) < floor) continue;
11891
- if (!raw.component) continue;
11892
- const comp = components.find((c) => c.id === raw.component);
11893
- if (!comp) continue;
11894
- const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!raw.interface || i.id === raw.interface));
11895
- const methods = compInterfaces.flatMap((i) => i.methods);
11896
- const subsystemType = subsystems.find((s) => s.id === comp.subsystem)?.publicInterfaces.find((pi) => pi.component === comp.id)?.type;
11897
- entries.push({
11898
- id: raw.id ?? raw.interface ?? comp.id,
11899
- name: raw.name ?? comp.name,
11900
- audience,
11901
- type: raw.type ?? subsystemType ?? "Custom",
11902
- component: comp.id,
11903
- methods,
11904
- ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
11905
- // Project the backing Portal's auth + basePath so the codec can emit
11906
- // OpenAPI security + per-portal servers self-contained from the snapshot.
11907
- ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
11908
- ...comp.basePath ? { basePath: comp.basePath } : {},
11909
- details: raw.details ?? "",
11910
- ...raw.version ? { version: raw.version } : {},
11911
- ...raw.stability ? { stability: raw.stability } : {}
11912
- });
11913
- }
12027
+ for (const entry of visible) {
12028
+ if (entry.kind !== "component") continue;
12029
+ const comp = components.find((c) => c.id === entry.component);
12030
+ if (comp) entries.push(contractEntry(entry, comp, interfaces));
12031
+ }
12032
+ const exportedTypes = visible.filter((e) => e.kind === "type");
12033
+ const exportedSpecs = exportedTypes.map((e) => types.find((t) => t.id === e.typeDef && (t.subsystem ?? e.source) === e.source)).filter((t) => !!t);
12034
+ const closure = computeTypeClosure(entries, types, exportedSpecs);
12035
+ const closureIdOf = (e) => {
12036
+ const spec = types.find((t) => t.id === e.typeDef && (t.subsystem ?? e.source) === e.source);
12037
+ if (!spec) return String(e.typeDef);
12038
+ return closure.some((d) => d.id === qualifiedTypeId2(spec)) ? qualifiedTypeId2(spec) : spec.id;
12039
+ };
12040
+ const projectId = boundProjectId();
11914
12041
  return SurfaceSnapshotSchema.parse({
11915
12042
  projectName: system.name,
12043
+ ...projectId ? { projectId } : {},
11916
12044
  origin: "generated",
11917
12045
  stateId: stateIdString(),
11918
12046
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11919
12047
  interfaces: entries,
11920
- types: computeTypeClosure(entries, types)
12048
+ types: closure,
12049
+ ...exportedTypes.length ? { exportedTypes: exportedTypes.map((e) => ({ id: e.publicName, type: closureIdOf(e), audience: e.audience ?? "instance" })) } : {}
11921
12050
  });
11922
12051
  }
11923
12052
  function projectChildSurface() {
@@ -12037,7 +12166,8 @@ function listExternalInterfaces() {
12037
12166
  ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
12038
12167
  ...snapshot.version ? { version: snapshot.version } : {},
12039
12168
  freshness,
12040
- interfaceIds: snapshot.interfaces.map((e) => e.id)
12169
+ interfaceIds: snapshot.interfaces.map((e) => e.id),
12170
+ ...snapshot.projectId ? { projectId: snapshot.projectId } : {}
12041
12171
  };
12042
12172
  });
12043
12173
  }
@@ -12098,7 +12228,7 @@ function defaultTargetConfig(type) {
12098
12228
  enabled: true
12099
12229
  };
12100
12230
  }
12101
- var WAIRON_VERSION = "5.1.1-dev.79";
12231
+ var WAIRON_VERSION = "5.1.1-dev.80";
12102
12232
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
12103
12233
  var ARCHITECT_AGENT_ID = "agent-architect";
12104
12234
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -13020,18 +13150,18 @@ function buildImportGraph(index, universe) {
13020
13150
  };
13021
13151
  }
13022
13152
  function buildOwnershipIndex(ctx) {
13023
- const ownedBy = /* @__PURE__ */ new Map();
13153
+ const ownedBy2 = /* @__PURE__ */ new Map();
13024
13154
  for (const comp of ctx.components) {
13025
13155
  if (isRetired(comp) || !isPattern(comp)) continue;
13026
13156
  for (const memberId of comp.owns) {
13027
13157
  const member = ctx.componentMap.get(memberId);
13028
13158
  if (!member || isPattern(member)) continue;
13029
- if (!ownedBy.has(memberId)) ownedBy.set(memberId, comp.id);
13159
+ if (!ownedBy2.has(memberId)) ownedBy2.set(memberId, comp.id);
13030
13160
  }
13031
13161
  }
13032
13162
  return {
13033
- ownedMembers: new Set(ownedBy.keys()),
13034
- ownerOf: (memberId) => ownedBy.get(memberId)
13163
+ ownedMembers: new Set(ownedBy2.keys()),
13164
+ ownerOf: (memberId) => ownedBy2.get(memberId)
13035
13165
  };
13036
13166
  }
13037
13167
  function buildDependencyEdges(ctx) {
@@ -13224,6 +13354,45 @@ var namespaceShadowingRule = {
13224
13354
  }
13225
13355
  };
13226
13356
 
13357
+ // src/core/rules/integrity/project-identity.ts
13358
+ var projectIdentityRule = {
13359
+ name: "project-identity",
13360
+ description: "A project is keyed on its id, so the id must be declared, well-formed and stable: a project that declares none answers to one derived from its display name until it writes one (PROJECT_ID_DEFAULTED), a project whose name yields no id or whose declared id breaks the grammar has nothing reliable to key on (PROJECT_ID_AMBIGUOUS), and an id that differs from the one the lock approved has moved under everything that keyed on it (PROJECT_ID_CHANGED).",
13361
+ codes: [
13362
+ { code: "PROJECT_ID_DEFAULTED", defaultSeverity: "notice", summary: "Project declares no id; it answers to one derived from its display name" },
13363
+ { code: "PROJECT_ID_AMBIGUOUS", defaultSeverity: "warning", summary: "Project has no usable id: its name yields no slug, or its declared id breaks the grammar" },
13364
+ { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" }
13365
+ ],
13366
+ check(ctx) {
13367
+ const identity = ctx.projectIdentity;
13368
+ if (!identity) return;
13369
+ const problem = (kind) => identity.problems.find((p) => p.kind === kind);
13370
+ if (problem("defaulted")) {
13371
+ ctx.addIssue(
13372
+ "notice",
13373
+ "PROJECT_ID_DEFAULTED",
13374
+ `Project "${identity.name}" declares no id in .wai/project.yaml, so it answers to "${identity.id}", derived from its display name \u2014 renaming the project would move it. No save writes it for you; declare it with \`id: ${identity.id}\` (stage 3 requires an explicit id).`
13375
+ );
13376
+ }
13377
+ const ambiguous = problem("ambiguous");
13378
+ if (ambiguous) {
13379
+ ctx.addIssue(
13380
+ "warning",
13381
+ "PROJECT_ID_AMBIGUOUS",
13382
+ `Project "${identity.name}" has no usable id: ${ambiguous.detail}. A project id is lower-case letters, digits, "-", "_" and ".", starting and ending with a letter or a digit \u2014 declare one in .wai/project.yaml.`
13383
+ );
13384
+ }
13385
+ if (problem("changed")) {
13386
+ const now = identity.id === void 0 ? "no id at all" : `"${identity.id}"`;
13387
+ ctx.addIssue(
13388
+ "error",
13389
+ "PROJECT_ID_CHANGED",
13390
+ `Project "${identity.name}" now resolves to ${now}, but its lock approved "${identity.lockedId}" \u2014 everything keyed on the approved id no longer finds this project. Restore \`id: ${identity.lockedId}\` in .wai/project.yaml, or rename deliberately and re-lock.`
13391
+ );
13392
+ }
13393
+ }
13394
+ };
13395
+
13227
13396
  // src/core/rules/integrity/roundtrip-serialization.ts
13228
13397
  var roundtripRule = {
13229
13398
  name: "roundtrip-serialization",
@@ -13349,7 +13518,7 @@ var publicSurfaceBindingRule = {
13349
13518
  check(ctx) {
13350
13519
  for (const sub of ctx.subsystems) {
13351
13520
  const isDraftCtx = isDraftSubsystem(sub);
13352
- for (const pi of sub.publicInterfaces) {
13521
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
13353
13522
  if (!pi.component) {
13354
13523
  ctx.addIssue("error", "PUBLIC_INTERFACE_UNBOUND", `Subsystem "${sub.id}" declares a ${pi.type} public interface with no backing component. Bind it to the component that realizes it (publicInterfaces[].component).`, sub.id, isDraftCtx);
13355
13524
  continue;
@@ -13436,13 +13605,15 @@ var publicSurfaceDeclaredTypeRule = {
13436
13605
  check(ctx) {
13437
13606
  for (const sub of ctx.subsystems) {
13438
13607
  const isDraftCtx = isDraftSubsystem(sub);
13439
- for (const pi of sub.publicInterfaces) {
13608
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
13440
13609
  const backing = pi.component ? ctx.componentMap.get(pi.component) : void 0;
13441
13610
  if (!backing) continue;
13442
- if (!pubTypeMatches(pi.type, backing.componentType, backing.portalType)) {
13443
- ctx.addIssue("error", "PUBLIC_INTERFACE_TYPE_MISMATCH", `Subsystem "${sub.id}" declares a ${pi.type} public interface backed by "${pi.component}" (${backing.componentType}${backing.portalType ? `/${backing.portalType}` : ""}), which cannot realize ${pi.type}. Expected ${expectedFor(pi.type)}.`, sub.id, isDraftCtx);
13611
+ const piType = pi.type ?? "Custom";
13612
+ const piDetails = pi.details ?? "";
13613
+ if (!pubTypeMatches(piType, backing.componentType, backing.portalType)) {
13614
+ ctx.addIssue("error", "PUBLIC_INTERFACE_TYPE_MISMATCH", `Subsystem "${sub.id}" declares a ${pi.type} public interface backed by "${pi.component}" (${backing.componentType}${backing.portalType ? `/${backing.portalType}` : ""}), which cannot realize ${pi.type}. Expected ${expectedFor(piType)}.`, sub.id, isDraftCtx);
13444
13615
  }
13445
- if (pi.type === "Custom" && EVENT_VOCAB.test(pi.details)) {
13616
+ if (piType === "Custom" && EVENT_VOCAB.test(piDetails)) {
13446
13617
  const eventCapable = backing.componentType === "Observer" || backing.componentType === "Portal" && backing.portalType === "MessageBus";
13447
13618
  if (!eventCapable) {
13448
13619
  ctx.addIssue("warning", "PUBLIC_INTERFACE_EVENT_MISTYPED", `Subsystem "${sub.id}" declares a Custom public interface whose description implies an event/async boundary ("${pi.details}"), but it is backed by "${pi.component}" (${backing.componentType}${backing.portalType ? `/${backing.portalType}` : ""}), which cannot realize eventing. If this is genuinely event-driven, type it MessageBus and back it with an Observer or a Portal(MessageBus); otherwise reword the description to match the synchronous contract.`, sub.id, isDraftCtx);
@@ -13463,7 +13634,7 @@ var publicSurfaceBoundContractRule = {
13463
13634
  check(ctx) {
13464
13635
  for (const sub of ctx.subsystems) {
13465
13636
  const isDraftCtx = isDraftSubsystem(sub);
13466
- for (const pi of sub.publicInterfaces) {
13637
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
13467
13638
  if (!pi.interface) continue;
13468
13639
  if (!pi.component || !ctx.componentMap.has(pi.component)) continue;
13469
13640
  const intf = ctx.interfaceMap.get(pi.interface);
@@ -13477,6 +13648,47 @@ var publicSurfaceBoundContractRule = {
13477
13648
  }
13478
13649
  };
13479
13650
 
13651
+ // src/core/rules/integrity/export-tables.ts
13652
+ var exportTablesRule = {
13653
+ name: "export-tables",
13654
+ 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 \u2014 or two explicit entries \u2014 bind a name to different targets; the name is left out), no chain leads back to itself (EXPORT_CYCLE \u2014 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, under a public name of [a-z0-9-_]+ (EXPORT_INVALID), and every re-exported component can serve a boundary caller \u2014 a Portal, a gateway being a Portal variant, or an Observer for events (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports). The rule judges the facts the export resolver gathered and resolves nothing itself.",
13655
+ codes: [
13656
+ { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
13657
+ { 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)" },
13658
+ { code: "EXPORT_INVALID", defaultSeverity: "notice", 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" },
13659
+ { code: "EXPORT_UNCONSUMABLE", defaultSeverity: "notice", summary: "An exported component no boundary caller may reach: neither a Portal nor an Observer" }
13660
+ ],
13661
+ check(ctx) {
13662
+ const tables = ctx.exportTables;
13663
+ if (!tables) return;
13664
+ for (const table of tables) {
13665
+ const sub = table.level === "subsystem" ? ctx.subsystems.find((s) => s.id === table.owner) : void 0;
13666
+ const draft = sub ? isDraftSubsystem(sub) : false;
13667
+ const where = table.level === "subsystem" ? `Subsystem "${table.owner}"` : `The project's L0 export table`;
13668
+ for (const problem of table.problems) {
13669
+ const name = problem.publicName ? `"${problem.publicName}"` : "an entry";
13670
+ switch (problem.kind) {
13671
+ case "duplicate":
13672
+ ctx.addIssue("error", "EXPORT_ID_DUPLICATE", `${where} binds ${name} to more than one target (${(problem.targets ?? []).join(", ")}), so the name is left out of the table. Narrow one of the wildcards, or add an explicit entry that picks the target.`, table.owner, draft);
13673
+ break;
13674
+ case "named-cycle":
13675
+ ctx.addIssue("error", "EXPORT_CYCLE", `${where} ${problem.detail}. Export the item from the level that owns it.`, table.owner, draft);
13676
+ break;
13677
+ case "wildcard-cycle":
13678
+ ctx.addIssue("warning", "EXPORT_CYCLE", `${where}: ${problem.detail}. Re-export in one direction only.`, table.owner, draft);
13679
+ break;
13680
+ case "invalid":
13681
+ ctx.addIssue("notice", "EXPORT_INVALID", `${where} ${problem.detail}.`, table.owner, draft);
13682
+ break;
13683
+ case "unconsumable":
13684
+ ctx.addIssue("notice", "EXPORT_UNCONSUMABLE", `${where}: ${problem.detail}.`, table.owner, draft);
13685
+ break;
13686
+ }
13687
+ }
13688
+ }
13689
+ }
13690
+ };
13691
+
13480
13692
  // src/core/rules/integrity/lint-allows.ts
13481
13693
  var lintAllowsRule = {
13482
13694
  name: "lint-allows",
@@ -14868,7 +15080,7 @@ var subsystemBoundaryDepsRule = {
14868
15080
  }
14869
15081
  };
14870
15082
  function restrictedConsumers(ctx, subsystemId, componentId) {
14871
- const entries = (ctx.subsystems.find((s) => s.id === subsystemId)?.publicInterfaces ?? []).filter((pi) => pi.component === componentId);
15083
+ const entries = (ctx.subsystems.find((s) => s.id === subsystemId)?.publicInterfaces ?? []).filter((pi) => isOwnComponentEntry(pi) && pi.component === componentId);
14872
15084
  if (entries.length === 0 || entries.some((pi) => pi.consumers === void 0)) return null;
14873
15085
  return [...new Set(entries.flatMap((pi) => pi.consumers ?? []))];
14874
15086
  }
@@ -14922,14 +15134,35 @@ var logicDependencyClassRule = {
14922
15134
  };
14923
15135
 
14924
15136
  // src/core/rules/doctrine/data-block-dependencies.ts
15137
+ function indexReaches(ctx, from, target, seen = /* @__PURE__ */ new Set()) {
15138
+ for (const dep of from.dependsOn) {
15139
+ if (dep === target) return true;
15140
+ const next = ctx.componentMap.get(dep);
15141
+ if (!next || next.componentType !== "Index" || seen.has(dep)) continue;
15142
+ seen.add(dep);
15143
+ if (indexReaches(ctx, next, target, seen)) return true;
15144
+ }
15145
+ return false;
15146
+ }
15147
+ function derivedIndexBreach(ctx, from, to) {
15148
+ const ownership = ctx.ownershipIndex();
15149
+ const owner = ownership.ownerOf(from.id);
15150
+ if (!owner || owner !== ownership.ownerOf(to.id)) {
15151
+ return "An Index may project another Index only when one Repository owns both";
15152
+ }
15153
+ if (indexReaches(ctx, to, from.id)) {
15154
+ return `"${to.id}" depends back on "${from.id}" through Index edges \u2014 derived Indexes never form a cycle`;
15155
+ }
15156
+ return void 0;
15157
+ }
14925
15158
  var dataBlockDepsRule = {
14926
15159
  name: "data-block-dependencies",
14927
- description: "Keeps the data layer facing down. A Store may depend only on another Store, a backend Adapter or pure logic \u2014 Registries and Indexes depend on the Store, never the reverse. A Registry is the write path to its Store and may depend on that Store, a backend Adapter, or the pure logic that validates the write. An Index is a read projection and a Query a computed read, each over its Store through a backend Adapter where one serves it. An Adapter is a sink toward the system: it may use pure logic, but never a Store or any other Orchestrator.",
15160
+ description: "Keeps the data layer facing down. A Store may depend only on another Store, a backend Adapter or pure logic \u2014 Registries and Indexes depend on the Store, never the reverse. A Registry is the write path to its Store and may depend on that Store, a backend Adapter, or the pure logic that validates the write. An Index is a read projection and a Query a computed read, each over its Store through a backend Adapter where one serves it; as an exceptional case an Index may project another Index of the same Repository, never in a cycle. An Adapter is a sink toward the system: it may use pure logic, but never a Store or any other Orchestrator.",
14928
15161
  codes: [
14929
15162
  { code: "ARCHITECTURE_VIOLATION_STORE_DEP", defaultSeverity: "error", summary: "Store depending on anything but another Store, a backend Adapter or pure logic" },
14930
15163
  { code: "ARCHITECTURE_VIOLATION_REGISTRY_DEP", defaultSeverity: "warning", summary: "Registry depending on anything but its Store, a backend Adapter or pure logic (warning while new; aligned to the standard \xA77 validate\u2192write path)" },
14931
15164
  { code: "ARCHITECTURE_VIOLATION_ADAPTER_DEP", defaultSeverity: "error", summary: "Adapter depending on a Store or on an Orchestrator that is not pure" },
14932
- { code: "ARCHITECTURE_VIOLATION_INDEX_DEP", defaultSeverity: "error", summary: "Index depending on anything but its Store, an Adapter or pure logic" },
15165
+ { code: "ARCHITECTURE_VIOLATION_INDEX_DEP", defaultSeverity: "error", summary: "Index depending on anything but its Store, an Adapter, pure logic or another Index of its own Repository (never in a cycle)" },
14933
15166
  { code: "ARCHITECTURE_VIOLATION_QUERY_DEP", defaultSeverity: "error", summary: "Query depending on anything but its Store, a backend Adapter or pure logic" }
14934
15167
  ],
14935
15168
  check(ctx) {
@@ -14966,11 +15199,13 @@ var dataBlockDepsRule = {
14966
15199
  edge.draftContext
14967
15200
  );
14968
15201
  }
14969
- if (comp.componentType === "Index" && ownStoreOnly) {
15202
+ const indexBreach = comp.componentType === "Index" && depComp.componentType === "Index" ? derivedIndexBreach(ctx, comp, depComp) : void 0;
15203
+ const derivedIndexOk = comp.componentType === "Index" && depComp.componentType === "Index" && !indexBreach;
15204
+ if (comp.componentType === "Index" && ownStoreOnly && !derivedIndexOk) {
14970
15205
  ctx.addIssue(
14971
15206
  "error",
14972
15207
  "ARCHITECTURE_VIOLATION_INDEX_DEP",
14973
- `Architectural violation: Index component "${comp.id}" cannot depend on "${depComp.componentType}" component "${depComp.id}". An Index is a read projection and may depend only on its Store, a backend Adapter or pure logic.`,
15208
+ `Architectural violation: Index component "${comp.id}" cannot depend on "${depComp.componentType}" component "${depComp.id}". An Index is a read projection and may depend only on its Store, a backend Adapter or pure logic \u2014 or, as an exceptional case, on another Index of the same Repository, never in a cycle.${indexBreach ? ` ${indexBreach}.` : ""}`,
14974
15209
  comp.id,
14975
15210
  edge.draftContext
14976
15211
  );
@@ -19355,6 +19590,9 @@ var SDD_RULES = [
19355
19590
  // that would anchor a bare reference to the root instead.
19356
19591
  reservedIdSegmentsRule,
19357
19592
  namespaceShadowingRule,
19593
+ // The project's own id, beside the spec ids: declared, well-formed, and the
19594
+ // one its lock approved.
19595
+ projectIdentityRule,
19358
19596
  roundtripRule,
19359
19597
  // The type vocabulary in three questions: what a type declares about
19360
19598
  // itself, then the identifiers its fields name, then the ones its
@@ -19446,6 +19684,8 @@ var SDD_RULES = [
19446
19684
  publicSurfaceConsumersRule,
19447
19685
  publicSurfaceDeclaredTypeRule,
19448
19686
  publicSurfaceBoundContractRule,
19687
+ // The export tables the resolver settled: what the published names bind.
19688
+ exportTablesRule,
19449
19689
  cyclesRule,
19450
19690
  // Semantic-edge family: dispatch/lifecycle validity BEFORE reachability so a
19451
19691
  // reader sees the broken edge finding next to the unused-detection fallout
@@ -20684,7 +20924,7 @@ function buildRuleContext(opts) {
20684
20924
  for (const sub of subsystems) {
20685
20925
  publicSet.set(
20686
20926
  sub.id,
20687
- new Set(sub.publicInterfaces.map((pi) => pi.component).filter((c) => !!c))
20927
+ new Set(sub.publicInterfaces.filter(isOwnComponentEntry).map((pi) => pi.component).filter((c) => !!c))
20688
20928
  );
20689
20929
  }
20690
20930
  const surfaceSnapshots = opts.surfaceSnapshots ?? [];
@@ -20788,7 +21028,7 @@ function buildRuleContext(opts) {
20788
21028
  const candidates = [];
20789
21029
  for (const snapshot of pool) {
20790
21030
  if (provider !== void 0 && !isProvidedBy(snapshot, provider)) continue;
20791
- const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
21031
+ const entry = snapshot.interfaces.find((e) => e.id === local) ?? snapshot.interfaces.find((e) => e.component === local && (e.componentType === void 0 || e.id === e.component || e.id === e.interface));
20792
21032
  if (entry) candidates.push({ snapshot, entry });
20793
21033
  }
20794
21034
  if (candidates.length === 0) continue;
@@ -21027,6 +21267,8 @@ function buildRuleContext(opts) {
21027
21267
  ext: { profiles: extensions.profiles, languages: extensions.languages, patterns: extensions.patterns, guarantees: extensions.guarantees, assertions: extensions.assertions, packSelections: opts.packSelections ?? [], selectionFailures: extensions.selectionFailures ?? [] },
21028
21268
  variants: opts.variants ?? [],
21029
21269
  surfaceSnapshots,
21270
+ ...opts.projectIdentity ? { projectIdentity: opts.projectIdentity } : {},
21271
+ ...opts.exportTables ? { exportTables: opts.exportTables } : {},
21030
21272
  mountSurfaceSnapshots,
21031
21273
  codeModel: opts.codeModel ?? emptyCodeModel(),
21032
21274
  roundTripIssues: opts.roundTripIssues,
@@ -21098,6 +21340,17 @@ function projectPackSelections() {
21098
21340
  return [];
21099
21341
  }
21100
21342
  }
21343
+ function boundProjectIdentity() {
21344
+ let config;
21345
+ try {
21346
+ config = loadProjectConfig();
21347
+ } catch {
21348
+ return void 0;
21349
+ }
21350
+ if (!config) return void 0;
21351
+ const lockedProjectId = readLockRecord()?.projectId;
21352
+ return projectIdentity(config, lockedProjectId);
21353
+ }
21101
21354
  function findTestsReferencing2(methods, projectRoot, testRoots = []) {
21102
21355
  return findTestsReferencing(methods, projectRoot, testRoots);
21103
21356
  }
@@ -21300,8 +21553,17 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
21300
21553
  scopeSubsystem,
21301
21554
  extensions,
21302
21555
  variants: loadProjectVariants(),
21556
+ // Every subsystem's resolved export table, then the project's: the
21557
+ // export-tables rule judges the problems the resolver met.
21558
+ exportTables: [
21559
+ ...subsystems.map((s) => resolveSubsystemExports(s.id)),
21560
+ resolveProjectExports()
21561
+ ],
21303
21562
  // By-name selections only: a legacy path ref pins nothing to check.
21304
21563
  packSelections: projectPackSelections(),
21564
+ // The run that IS the parent's verdict on a chained child judges the
21565
+ // child's specs, not the parent's identity, so it carries none.
21566
+ projectIdentity: crossTree === "off" ? void 0 : boundProjectIdentity(),
21305
21567
  surfaceSnapshots,
21306
21568
  mountSurfaceSnapshots,
21307
21569
  codeModel,
@@ -21555,7 +21817,7 @@ function loadSpecGraph() {
21555
21817
  const implementations = loadImplementationSpecs();
21556
21818
  const publicComponents = /* @__PURE__ */ new Set();
21557
21819
  for (const sub of subsystems) {
21558
- for (const pi of sub.publicInterfaces) {
21820
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
21559
21821
  if (pi.component) publicComponents.add(pi.component);
21560
21822
  }
21561
21823
  }
@@ -21964,6 +22226,350 @@ function diagramSetIndex(files, systemName) {
21964
22226
  return lines.join("\n");
21965
22227
  }
21966
22228
 
22229
+ // src/core/exports.ts
22230
+ var CONSUMABLE = /* @__PURE__ */ new Set(["Portal", "Observer"]);
22231
+ function localName2(id) {
22232
+ return id.split("::").pop() ?? id;
22233
+ }
22234
+ function ownedBy(subsystemId, declaringSubsystem) {
22235
+ return declaringSubsystem !== void 0 && (declaringSubsystem === subsystemId || declaringSubsystem.startsWith(`${subsystemId}::`));
22236
+ }
22237
+ function worldOf(subsystems, components, interfaces, types) {
22238
+ return {
22239
+ subsystems: new Map(subsystems.map((s) => [s.id, s])),
22240
+ components: new Map(components.map((c) => [c.id, c])),
22241
+ interfaces: new Map(interfaces.map((i) => [i.id, i])),
22242
+ types: types.reduce((m, t) => m.set(t.id, [...m.get(t.id) ?? [], t]), /* @__PURE__ */ new Map())
22243
+ };
22244
+ }
22245
+ function lookup(map, ref, namespace) {
22246
+ if (!ref) return void 0;
22247
+ return map.get(ref) ?? (namespace ? map.get(`${namespace}::${ref}`) : void 0);
22248
+ }
22249
+ function lookupType(world, ref, subsystemId) {
22250
+ const candidates = lookup(world.types, ref, subsystemId);
22251
+ return candidates?.find((t) => ownedBy(subsystemId, t.subsystem)) ?? candidates?.[0];
22252
+ }
22253
+ function checkPublicName(name, owner, problems) {
22254
+ if (!PUBLIC_NAME_RE.test(name)) {
22255
+ problems.push({
22256
+ kind: "invalid",
22257
+ owner,
22258
+ publicName: name,
22259
+ detail: `public name "${name}" breaks [a-z0-9-_]+ \u2014 rename it with \`as\``
22260
+ });
22261
+ }
22262
+ }
22263
+ function checkConsumable(entry, owner, problems) {
22264
+ if (entry.kind === "component" && entry.componentType && !CONSUMABLE.has(entry.componentType)) {
22265
+ problems.push({
22266
+ kind: "unconsumable",
22267
+ owner,
22268
+ publicName: entry.publicName,
22269
+ targets: [entry.component],
22270
+ detail: `"${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`
22271
+ });
22272
+ }
22273
+ }
22274
+ function findInSource(world, source, table, request) {
22275
+ if (request.typeDef) {
22276
+ const type = lookupType(world, request.typeDef, source.id);
22277
+ if (!type) return {};
22278
+ const item2 = { publicName: localName2(type.id), kind: "type", source: type.subsystem ?? source.id, typeDef: type.id, via: [] };
22279
+ return { item: item2, found: table.entries.find((e) => e.kind === "type" && e.typeDef === type.id && e.source === item2.source) };
22280
+ }
22281
+ const intf = lookup(world.interfaces, request.interface, source.id);
22282
+ const comp = lookup(world.components, request.component ?? intf?.component, source.id);
22283
+ if (!comp) return {};
22284
+ const narrowed = intf?.id ?? request.interface;
22285
+ const item = {
22286
+ publicName: localName2(narrowed ?? comp.id),
22287
+ kind: "component",
22288
+ source: comp.subsystem,
22289
+ component: comp.id,
22290
+ ...narrowed ? { interface: narrowed } : {},
22291
+ componentType: comp.componentType,
22292
+ via: []
22293
+ };
22294
+ const exported = table.entries.filter((e) => e.kind === "component" && e.component === comp.id);
22295
+ const found = narrowed ? exported.find((e) => e.interface === narrowed) ?? exported.find((e) => e.interface === void 0) : exported.find((e) => e.interface === void 0) ?? exported[0];
22296
+ return { item, found: found && narrowed ? { ...found, interface: narrowed } : found };
22297
+ }
22298
+ function bindNamed(world, owner, source, sourceTable, request, publicName, inCycle, problems) {
22299
+ const { found, item } = findInSource(world, source, sourceTable, request);
22300
+ const what = request.typeDef ? `type "${request.typeDef}"` : `component "${request.component}"${request.interface ? ` (interface "${request.interface}")` : ""}`;
22301
+ if (!item) {
22302
+ problems.push({ kind: "invalid", owner, detail: `re-exports ${what} from "${source.id}", which has no such item` });
22303
+ return void 0;
22304
+ }
22305
+ const name = publicName(item);
22306
+ if (found) return { ...found, publicName: name, via: [source.id, ...found.via] };
22307
+ if (inCycle) {
22308
+ problems.push({ kind: "named-cycle", owner, publicName: name, targets: [owner, source.id], detail: `re-exports ${what} from "${source.id}", but the chain leads back to "${owner}" without reaching the item` });
22309
+ return void 0;
22310
+ }
22311
+ const declaring = item.source;
22312
+ if (!ownedBy(source.id, declaring)) {
22313
+ problems.push({ kind: "invalid", owner, publicName: name, detail: `re-exports ${what} from "${source.id}", which neither exports nor owns it` });
22314
+ return void 0;
22315
+ }
22316
+ problems.push({ kind: "invalid", owner, publicName: name, detail: `re-exports ${what} from "${source.id}", which does not export it \u2014 publish it there first` });
22317
+ return { ...item, publicName: name, via: [source.id] };
22318
+ }
22319
+ function bindOwn(world, sub, pi, problems) {
22320
+ if (pi.typeDef) {
22321
+ const type = lookupType(world, pi.typeDef, sub.id);
22322
+ if (!type || !ownedBy(sub.id, type.subsystem)) {
22323
+ problems.push({ kind: "invalid", owner: sub.id, detail: `exports type "${pi.typeDef}" as its own, but ${type ? `it belongs to "${type.subsystem ?? "the system"}"` : "no such type exists"}` });
22324
+ return void 0;
22325
+ }
22326
+ const name2 = pi.as ?? localName2(type.id);
22327
+ checkPublicName(name2, sub.id, problems);
22328
+ return { publicName: name2, kind: "type", source: type.subsystem ?? sub.id, typeDef: type.id, via: [] };
22329
+ }
22330
+ const comp = lookup(world.components, pi.component, sub.id);
22331
+ if (!comp || !ownedBy(sub.id, comp.subsystem)) return void 0;
22332
+ const intf = lookup(world.interfaces, pi.interface, sub.id);
22333
+ const narrowed = intf?.id ?? pi.interface;
22334
+ const name = pi.as ?? localName2(narrowed ?? comp.id);
22335
+ checkPublicName(name, sub.id, problems);
22336
+ return {
22337
+ publicName: name,
22338
+ kind: "component",
22339
+ source: sub.id,
22340
+ component: comp.id,
22341
+ ...narrowed ? { interface: narrowed } : {},
22342
+ componentType: comp.componentType,
22343
+ ...pi.type ? { type: pi.type } : {},
22344
+ ...pi.details !== void 0 ? { details: pi.details } : {},
22345
+ via: []
22346
+ };
22347
+ }
22348
+ function subsystemCandidates(world, sub, tables, group, problems) {
22349
+ const out = [];
22350
+ sub.publicInterfaces.forEach((pi, index) => {
22351
+ if (pi.from === void 0) {
22352
+ const own = bindOwn(world, sub, pi, problems);
22353
+ if (own) out.push({ entry: own, explicit: true, from: index });
22354
+ return;
22355
+ }
22356
+ const source = world.subsystems.get(pi.from);
22357
+ if (!source || source.id === sub.id) {
22358
+ problems.push({ kind: "invalid", owner: sub.id, detail: source ? `re-exports from itself` : `re-exports from "${pi.from}", which is not a subsystem of this project` });
22359
+ return;
22360
+ }
22361
+ const sourceTable = tables.get(source.id) ?? emptyTable(source.id, "subsystem");
22362
+ if (pi.component !== void 0 || pi.typeDef !== void 0 || pi.interface !== void 0) {
22363
+ const bound2 = bindNamed(
22364
+ world,
22365
+ sub.id,
22366
+ source,
22367
+ sourceTable,
22368
+ { component: pi.component, interface: pi.interface, typeDef: pi.typeDef },
22369
+ (item) => pi.as ?? localName2(pi.interface ?? item.typeDef ?? item.component ?? ""),
22370
+ group.has(source.id),
22371
+ problems
22372
+ );
22373
+ if (!bound2) return;
22374
+ checkPublicName(bound2.publicName, sub.id, problems);
22375
+ const entry = { ...bound2, ...pi.type ? { type: pi.type } : {}, ...pi.details !== void 0 ? { details: pi.details } : {} };
22376
+ checkConsumable(entry, sub.id, problems);
22377
+ out.push({ entry, explicit: true, from: index });
22378
+ return;
22379
+ }
22380
+ for (const e of sourceTable.entries) {
22381
+ const entry = { ...e, via: [source.id, ...e.via] };
22382
+ checkConsumable(entry, sub.id, problems);
22383
+ out.push({ entry, explicit: false, from: index });
22384
+ }
22385
+ });
22386
+ return out;
22387
+ }
22388
+ function emptyTable(owner, level) {
22389
+ return { owner, level, entries: [], problems: [] };
22390
+ }
22391
+ function settle(owner, level, candidates, problems) {
22392
+ const byName = /* @__PURE__ */ new Map();
22393
+ for (const c of candidates) {
22394
+ const list = byName.get(c.entry.publicName) ?? [];
22395
+ list.push(c);
22396
+ byName.set(c.entry.publicName, list);
22397
+ }
22398
+ const entries = [];
22399
+ for (const [name, list] of byName) {
22400
+ const explicit = list.filter((c) => c.explicit);
22401
+ const pool = explicit.length ? explicit : list;
22402
+ const targets = [...new Set(pool.map((c) => exportTargetKey(c.entry)))];
22403
+ if (targets.length === 1) {
22404
+ entries.push(pool[0].entry);
22405
+ continue;
22406
+ }
22407
+ problems.push({
22408
+ kind: "duplicate",
22409
+ owner,
22410
+ publicName: name,
22411
+ targets,
22412
+ detail: `"${name}" is bound to ${targets.length} different targets (${targets.join(", ")}) by ${explicit.length ? "explicit entries" : "wildcard re-exports"}; it is left out of the table`
22413
+ });
22414
+ }
22415
+ return { owner, level, entries, problems };
22416
+ }
22417
+ function groupsInDependencyOrder(world) {
22418
+ const edges = /* @__PURE__ */ new Map();
22419
+ for (const sub of world.subsystems.values()) {
22420
+ edges.set(sub.id, sub.publicInterfaces.map((pi) => pi.from).filter((f) => f !== void 0 && f !== sub.id && world.subsystems.has(f)));
22421
+ }
22422
+ const index = /* @__PURE__ */ new Map();
22423
+ const low = /* @__PURE__ */ new Map();
22424
+ const stack = [];
22425
+ const onStack = /* @__PURE__ */ new Set();
22426
+ const groups = [];
22427
+ let next = 0;
22428
+ const visit = (v) => {
22429
+ index.set(v, next);
22430
+ low.set(v, next);
22431
+ next++;
22432
+ stack.push(v);
22433
+ onStack.add(v);
22434
+ for (const w of edges.get(v) ?? []) {
22435
+ if (!index.has(w)) {
22436
+ visit(w);
22437
+ low.set(v, Math.min(low.get(v), low.get(w)));
22438
+ } else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
22439
+ }
22440
+ if (low.get(v) === index.get(v)) {
22441
+ const group = [];
22442
+ let w;
22443
+ do {
22444
+ w = stack.pop();
22445
+ onStack.delete(w);
22446
+ group.push(w);
22447
+ } while (w !== v);
22448
+ groups.push(group.sort());
22449
+ }
22450
+ };
22451
+ for (const id of [...world.subsystems.keys()].sort()) if (!index.has(id)) visit(id);
22452
+ return groups;
22453
+ }
22454
+ function fingerprint(table) {
22455
+ return table.entries.map((e) => `${e.publicName}=${exportTargetKey(e)}`).sort().join("|");
22456
+ }
22457
+ function resolveSubsystemTables(subsystems, components, interfaces, types) {
22458
+ const world = worldOf(subsystems, components, interfaces, types);
22459
+ const tables = /* @__PURE__ */ new Map();
22460
+ for (const group of groupsInDependencyOrder(world)) {
22461
+ const members = new Set(group);
22462
+ const cyclic = group.length > 1;
22463
+ for (let pass = 0, changed = true; changed && pass <= (cyclic ? group.length * 4 + 4 : 0); pass++) {
22464
+ changed = false;
22465
+ for (const id of group) {
22466
+ const problems = [];
22467
+ const table = settle(id, "subsystem", subsystemCandidates(world, world.subsystems.get(id), tables, cyclic ? members : /* @__PURE__ */ new Set(), problems), problems);
22468
+ const before = tables.get(id);
22469
+ if (!before || fingerprint(before) !== fingerprint(table)) changed = true;
22470
+ tables.set(id, table);
22471
+ }
22472
+ }
22473
+ const wildcardLoop = cyclic && group.some((id) => world.subsystems.get(id).publicInterfaces.some((pi) => pi.from !== void 0 && pi.component === void 0 && pi.typeDef === void 0 && pi.interface === void 0 && members.has(pi.from)));
22474
+ if (wildcardLoop) {
22475
+ tables.get(group[0]).problems.push({
22476
+ kind: "wildcard-cycle",
22477
+ owner: group[0],
22478
+ targets: group,
22479
+ detail: `subsystems ${group.map((g) => `"${g}"`).join(", ")} re-export each other through wildcards; their tables resolve to the union, but they form one circular module surface`
22480
+ });
22481
+ }
22482
+ }
22483
+ return tables;
22484
+ }
22485
+ function sourceOf(world, e) {
22486
+ if (e.from ?? e.subsystem) return e.from ?? e.subsystem;
22487
+ if (e.component) return world.components.get(e.component)?.subsystem;
22488
+ if (e.interface) {
22489
+ const intf = world.interfaces.get(e.interface);
22490
+ return intf ? world.components.get(intf.component)?.subsystem : void 0;
22491
+ }
22492
+ if (e.typeDef) return world.types.get(e.typeDef)?.[0]?.subsystem;
22493
+ return void 0;
22494
+ }
22495
+ function withEntryMetadata(entry, e) {
22496
+ return {
22497
+ ...entry,
22498
+ audience: e.audience ?? "instance",
22499
+ ...e.name ? { name: e.name } : {},
22500
+ ...e.type ? { type: e.type } : {},
22501
+ ...e.details !== void 0 ? { details: e.details } : {},
22502
+ ...e.authPolicy ? { authPolicy: e.authPolicy } : {},
22503
+ ...e.version ? { version: e.version } : {},
22504
+ ...e.stability ? { stability: e.stability } : {}
22505
+ };
22506
+ }
22507
+ function resolveProjectTable(system, subsystems, components, interfaces, types, subsystemTables) {
22508
+ const world = worldOf(subsystems, components, interfaces, types);
22509
+ const owner = system.name;
22510
+ const problems = [];
22511
+ const candidates = [];
22512
+ (system.publicInterfaces ?? []).forEach((e, index) => {
22513
+ const sourceId = sourceOf(world, e);
22514
+ const source = sourceId ? world.subsystems.get(sourceId) : void 0;
22515
+ if (!source) {
22516
+ const label = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
22517
+ problems.push({ kind: "invalid", owner, publicName: label, detail: sourceId ? `"${label}" re-exports from "${sourceId}", which is not a subsystem of this project` : `"${label}" names no source subsystem and no item whose owner could supply one` });
22518
+ return;
22519
+ }
22520
+ const sourceTable = subsystemTables.get(source.id) ?? emptyTable(source.id, "subsystem");
22521
+ if (e.component !== void 0 || e.typeDef !== void 0 || e.interface !== void 0) {
22522
+ const bound2 = bindNamed(
22523
+ world,
22524
+ owner,
22525
+ source,
22526
+ sourceTable,
22527
+ { component: e.component, interface: e.interface, typeDef: e.typeDef },
22528
+ () => e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef,
22529
+ false,
22530
+ problems
22531
+ );
22532
+ if (!bound2) return;
22533
+ checkPublicName(bound2.publicName, owner, problems);
22534
+ const entry = withEntryMetadata(bound2, e);
22535
+ checkConsumable(entry, owner, problems);
22536
+ candidates.push({ entry, explicit: true, from: index });
22537
+ return;
22538
+ }
22539
+ for (const found of sourceTable.entries) {
22540
+ const entry = withEntryMetadata({ ...found, via: [source.id, ...found.via] }, e);
22541
+ checkConsumable(entry, owner, problems);
22542
+ candidates.push({ entry, explicit: false, from: index });
22543
+ }
22544
+ });
22545
+ return settle(owner, "project", candidates, problems);
22546
+ }
22547
+ var subsystemMemo = /* @__PURE__ */ new WeakMap();
22548
+ var projectMemo = /* @__PURE__ */ new WeakMap();
22549
+ function scanTables(subsystems) {
22550
+ const memo = subsystemMemo.get(subsystems);
22551
+ if (memo) return memo;
22552
+ const tables = resolveSubsystemTables(subsystems, loadComponentSpecs(), loadInterfaceSpecs(), loadTypeSpecs());
22553
+ subsystemMemo.set(subsystems, tables);
22554
+ return tables;
22555
+ }
22556
+ function resolveSubsystemExportTable(subsystemId) {
22557
+ const tables = scanTables(loadSubsystemSpecs());
22558
+ return tables.get(subsystemId) ?? emptyTable(subsystemId, "subsystem");
22559
+ }
22560
+ function resolveProjectExportTable() {
22561
+ const system = loadSystemSpec();
22562
+ if (!system) return emptyTable("", "project");
22563
+ const subsystems = loadSubsystemSpecs();
22564
+ const key = JSON.stringify([system.name, system.publicInterfaces ?? []]);
22565
+ const memo = projectMemo.get(subsystems);
22566
+ if (memo && memo.key === key) return memo.table;
22567
+ const subsystemTables = new Map(subsystems.map((sub) => [sub.id, resolveSubsystemExportTable(sub.id)]));
22568
+ const table = resolveProjectTable(system, subsystems, loadComponentSpecs(), loadInterfaceSpecs(), loadTypeSpecs(), subsystemTables);
22569
+ projectMemo.set(subsystems, { key, table });
22570
+ return table;
22571
+ }
22572
+
21967
22573
  // src/core/specs.ts
21968
22574
  function emptyIndex() {
21969
22575
  return {
@@ -22212,13 +22818,18 @@ function mergeMountRealizations(subs) {
22212
22818
  function stripNamespaceFromSubsystem(spec, prefix) {
22213
22819
  const memberPrefix = spec.projectPath ? spec.id : prefix;
22214
22820
  const relMember = (id) => id.includes("::") ? relativizeId(id, memberPrefix) : id;
22821
+ const relPeer = (id) => id.includes("::") ? relativizeId(id, prefix) : id;
22215
22822
  return {
22216
22823
  ...spec,
22217
22824
  id: relativizeId(spec.id, prefix),
22218
22825
  publicInterfaces: spec.publicInterfaces.map((pi) => ({
22219
22826
  ...pi,
22220
- component: pi.component ? relMember(pi.component) : void 0,
22221
- interface: pi.interface ? relMember(pi.interface) : void 0,
22827
+ // A re-export names a peer's item, written relative to the namespace the
22828
+ // subsystem is declared in (the loader's inverse), not its members'.
22829
+ component: pi.component ? pi.from ? relPeer(pi.component) : relMember(pi.component) : void 0,
22830
+ interface: pi.interface ? pi.from ? relPeer(pi.interface) : relMember(pi.interface) : void 0,
22831
+ ...pi.typeDef ? { typeDef: pi.from ? relPeer(pi.typeDef) : relMember(pi.typeDef) } : {},
22832
+ ...pi.from ? { from: relPeer(pi.from) } : {},
22222
22833
  // consumers name SUBSYSTEMS — peers of this one, so they are relative to
22223
22834
  // the namespace the subsystem itself is declared in, never its members'.
22224
22835
  ...pi.consumers ? { consumers: pi.consumers.map((c) => c.includes("::") ? relativizeId(c, prefix) : c) } : {}
@@ -22495,6 +23106,14 @@ function identityKeyOf(field, item) {
22495
23106
  }
22496
23107
  function publicInterfaceKey(o) {
22497
23108
  const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
23109
+ const from = str(o.from);
23110
+ const typeDef = str(o.typeDef);
23111
+ if (from || typeDef) {
23112
+ const item = typeDef ? `type ${typeDef}` : str(o.component) ?? "*";
23113
+ const narrowed = str(o.interface) ? `.${String(o.interface)}` : "";
23114
+ const renamed = str(o.as) ? ` as ${String(o.as)}` : "";
23115
+ return `${from ? `${from}:` : ""}${item}${narrowed}${renamed}`;
23116
+ }
22498
23117
  const component = str(o.component);
22499
23118
  if (component) return str(o.interface) ? `${component}.${String(o.interface)}` : component;
22500
23119
  if (typeof o.type === "string" && typeof o.details === "string") return `${o.type} ${o.details}`;
@@ -22711,8 +23330,9 @@ function identityFieldsOf(field) {
22711
23330
  case "dispatch":
22712
23331
  return ["capability"];
22713
23332
  // Bound: component + interface; not yet bound: type + details.
23333
+ // Re-exports and type exports: from + item + interface + as.
22714
23334
  case "publicInterfaces":
22715
- return ["component", "interface", "type", "details"];
23335
+ return ["component", "interface", "type", "details", "from", "typeDef", "as"];
22716
23336
  case "lifecycle":
22717
23337
  return ["phase", "component", "method"];
22718
23338
  case "emits":
@@ -22984,14 +23604,19 @@ var SpecWorkspace = class {
22984
23604
  return {
22985
23605
  ...sub,
22986
23606
  id: qualifiedSubId,
22987
- publicInterfaces: sub.publicInterfaces.map((p) => ({
22988
- ...p,
22989
- component: p.component ? qualifyId(p.component, componentPrefix, this.rootSubsystems) : void 0,
22990
- interface: p.interface ? qualifyId(p.interface, componentPrefix, this.rootSubsystems) : void 0,
22991
- // Each consumer is a subsystem reference: qualified into the namespace
22992
- // this subsystem is declared in, like a component's `subsystem` field.
22993
- ...p.consumers ? { consumers: p.consumers.map((c) => qualifySubsystemRef(c, namespacePrefix, this.rootSubsystems)) } : {}
22994
- })),
23607
+ publicInterfaces: sub.publicInterfaces.map((p) => {
23608
+ const itemPrefix = p.from ? namespacePrefix : componentPrefix;
23609
+ return {
23610
+ ...p,
23611
+ component: p.component ? qualifyId(p.component, itemPrefix, this.rootSubsystems) : void 0,
23612
+ interface: p.interface ? qualifyId(p.interface, itemPrefix, this.rootSubsystems) : void 0,
23613
+ ...p.typeDef ? { typeDef: qualifyId(p.typeDef, itemPrefix, this.rootSubsystems) } : {},
23614
+ ...p.from ? { from: qualifySubsystemRef(p.from, namespacePrefix, this.rootSubsystems) } : {},
23615
+ // Each consumer is a subsystem reference: qualified into the namespace
23616
+ // this subsystem is declared in, like a component's `subsystem` field.
23617
+ ...p.consumers ? { consumers: p.consumers.map((c) => qualifySubsystemRef(c, namespacePrefix, this.rootSubsystems)) } : {}
23618
+ };
23619
+ }),
22995
23620
  lifecycle: sub.lifecycle?.map((le) => ({
22996
23621
  ...le,
22997
23622
  component: qualifyId(le.component, componentPrefix, this.rootSubsystems)
@@ -24603,8 +25228,12 @@ var SpecWorkspace = class {
24603
25228
  if (Array.isArray(out.publicInterfaces)) {
24604
25229
  out.publicInterfaces = out.publicInterfaces.map((pi) => ({
24605
25230
  ...pi,
24606
- ...typeof pi?.component === "string" ? { component: qm(pi.component) } : {},
24607
- ...typeof pi?.interface === "string" ? { interface: qm(pi.interface) } : {},
25231
+ // A re-export names a peer's item: qualified into the declaring
25232
+ // namespace (q), not this subsystem's members' (qm).
25233
+ ...typeof pi?.component === "string" ? { component: (typeof pi?.from === "string" ? q : qm)(pi.component) } : {},
25234
+ ...typeof pi?.interface === "string" ? { interface: (typeof pi?.from === "string" ? q : qm)(pi.interface) } : {},
25235
+ ...typeof pi?.typeDef === "string" ? { typeDef: (typeof pi?.from === "string" ? q : qm)(pi.typeDef) } : {},
25236
+ ...typeof pi?.from === "string" ? { from: qualifySubsystemRef(pi.from, prefix, this.rootSubsystems) } : {},
24608
25237
  ...Array.isArray(pi?.consumers) ? { consumers: pi.consumers.map((c) => typeof c === "string" ? qualifySubsystemRef(c, prefix, this.rootSubsystems) : c) } : {}
24609
25238
  }));
24610
25239
  }
@@ -25398,6 +26027,12 @@ function loadTypeSpecs() {
25398
26027
  function loadTypeSpec(id) {
25399
26028
  return current().loadTypeSpec(id);
25400
26029
  }
26030
+ function resolveSubsystemExports(subsystemId) {
26031
+ return resolveSubsystemExportTable(subsystemId);
26032
+ }
26033
+ function resolveProjectExports() {
26034
+ return resolveProjectExportTable();
26035
+ }
25401
26036
  function dryRunSerializeSpecs(include) {
25402
26037
  return current().dryRunSerializeSpecs(include);
25403
26038
  }
@@ -25590,9 +26225,11 @@ function unregisterDomain(id) {
25590
26225
  // src/core/provision.ts
25591
26226
  var fs12 = __toESM(require("fs"));
25592
26227
  var path17 = __toESM(require("path"));
25593
- function defaultProjectConfig(name, now) {
26228
+ function defaultProjectConfig(name, now, id) {
26229
+ const declared = id ?? effectiveProjectId({ name });
25594
26230
  return {
25595
26231
  schemaVersion: "1.0.0",
26232
+ ...declared !== null ? { id: declared } : {},
25596
26233
  name,
25597
26234
  projectType: "backend",
25598
26235
  targets: [{ type: "claude", outputDir: ".claude/agents", enabled: true }],
@@ -25634,7 +26271,7 @@ function provisionProject(name) {
25634
26271
  projectConfigRepository.create(config);
25635
26272
  saveSystemSpec(bootstrapSystemSpec(name, now));
25636
26273
  }
25637
- function ensureProjectInitialized(fallbackName) {
26274
+ function ensureProjectInitialized(fallbackName, id) {
25638
26275
  const now = (/* @__PURE__ */ new Date()).toISOString();
25639
26276
  const paths = aiPathsAt(getProjectRoot());
25640
26277
  const hasSystem = fs12.existsSync(paths.specsSystem());
@@ -25646,7 +26283,7 @@ function ensureProjectInitialized(fallbackName) {
25646
26283
  let wroteConfig = false;
25647
26284
  let wroteSystem = false;
25648
26285
  if (!projectConfigRepository.exists()) {
25649
- projectConfigRepository.create(defaultProjectConfig(name, now));
26286
+ projectConfigRepository.create(defaultProjectConfig(name, now, id));
25650
26287
  wroteConfig = true;
25651
26288
  }
25652
26289
  if (!hasSystem) {
@@ -25727,7 +26364,7 @@ function backfillChainedSubprojectConfigs(projectRoot) {
25727
26364
  walkChainedSubprojects(projectRoot, (childDir, subsystemId) => {
25728
26365
  if (childHasSpecsButNoConfig(childDir)) {
25729
26366
  runWithProjectRoot(childDir, () => {
25730
- ensureProjectInitialized(subsystemId);
26367
+ ensureProjectInitialized(subsystemId, localSegment(subsystemId));
25731
26368
  });
25732
26369
  backfilled.push(childDir);
25733
26370
  }
@@ -25743,7 +26380,7 @@ function createChainedSubsystem(subsystem, projectName) {
25743
26380
  const childDir = assertContainedProjectPath(getProjectRoot(), projectPath);
25744
26381
  runWithProjectRoot(childDir, () => {
25745
26382
  ensureDir(aiPathsAt(childDir).specsDir());
25746
- ensureProjectInitialized(projectName);
26383
+ ensureProjectInitialized(projectName, localSegment(subsystem.id));
25747
26384
  });
25748
26385
  invalidateSpecCache();
25749
26386
  }
@@ -25774,6 +26411,9 @@ function moveSubsystemProject(subsystemId, newProjectPath) {
25774
26411
  saveSubsystemSpec({ ...sub, projectPath: nextPath, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
25775
26412
  invalidateSpecCache();
25776
26413
  }
26414
+ function localSegment(subsystemId) {
26415
+ return subsystemId.split("::").pop();
26416
+ }
25777
26417
  function toPosixPath(p) {
25778
26418
  return p.replace(/\\/g, "/");
25779
26419
  }
@@ -25810,7 +26450,7 @@ function externalizeSubsystem(subsystemId, projectPath) {
25810
26450
  const childSystemName = foo.name || subsystemId;
25811
26451
  runWithProjectRoot(childDir, () => {
25812
26452
  const now = (/* @__PURE__ */ new Date()).toISOString();
25813
- projectConfigRepository.create(defaultProjectConfig(childSystemName, now));
26453
+ projectConfigRepository.create(defaultProjectConfig(childSystemName, now, subsystemId));
25814
26454
  ensureDir(path17.join(childDir, ".wai", "specs"));
25815
26455
  saveSystemSpec(bootstrapSystemSpec(childSystemName, now));
25816
26456
  });
@@ -29018,7 +29658,13 @@ function qualifiedComponentId(id) {
29018
29658
  return chained.length === 1 ? chained[0] : id;
29019
29659
  }
29020
29660
  function publicInterfacesReplacement(subsystem, stored, list) {
29021
- const identity = (pi) => pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
29661
+ const identity = (pi) => {
29662
+ if (pi.from || pi.typeDef) {
29663
+ const item = pi.typeDef ? `type ${pi.typeDef}` : pi.component ?? "*";
29664
+ return `${pi.from ? `${pi.from}:` : ""}${item}${pi.interface ? `.${pi.interface}` : ""}${pi.as ? ` as ${pi.as}` : ""}`;
29665
+ }
29666
+ return pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
29667
+ };
29022
29668
  const restated = /* @__PURE__ */ new Map();
29023
29669
  for (const pi of list) restated.set(identity(pi), (restated.get(identity(pi)) ?? 0) + 1);
29024
29670
  const twice = [...restated].filter(([, n]) => n > 1).map(([key]) => `"${key}"`);
@@ -29031,7 +29677,7 @@ function publicInterfacesReplacement(subsystem, stored, list) {
29031
29677
  const key = identity(pi);
29032
29678
  seen.set(key, (seen.get(key) ?? 0) + 1);
29033
29679
  if (seen.get(key) > (restated.get(key) ?? 0)) {
29034
- deletes.push(pi.component ? { component: pi.component, interface: pi.interface, action: "delete" } : { type: pi.type, details: pi.details, action: "delete" });
29680
+ deletes.push(pi.from || pi.typeDef ? { from: pi.from, component: pi.component, interface: pi.interface, typeDef: pi.typeDef, as: pi.as, action: "delete" } : pi.component ? { component: pi.component, interface: pi.interface, action: "delete" } : { type: pi.type, details: pi.details, action: "delete" });
29035
29681
  }
29036
29682
  }
29037
29683
  const restatedEntries = list.map((pi) => {
@@ -29656,17 +30302,21 @@ function createMcpServer(options = {}) {
29656
30302
  }
29657
30303
  }
29658
30304
  );
30305
+ const publicInterfaceItemInput = import_zod11.z.object({
30306
+ type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]).optional().describe("Transport kind of an own item (required on an own component item); a re-export inherits its target's"),
30307
+ details: import_zod11.z.string().optional().describe("Consumer-facing description of an own item (required on an own component item); a re-export inherits its target's"),
30308
+ component: import_zod11.z.string().optional().describe("For an own item, the L2 component that realizes it (this subsystem's published surface); for a re-export, the component re-exported from `from`"),
30309
+ interface: import_zod11.z.string().optional().describe("Optional L3 interface id the entry narrows to"),
30310
+ consumers: import_zod11.z.array(import_zod11.z.string()).optional().describe("The subsystem ids this surface is published to. Absent, any subsystem may depend on the component; present, only these may \u2014 when every entry publishing the component names consumers, the union of the lists is the whole set, and a dependency from any other subsystem is CROSS_SUBSYSTEM_UNLISTED_CONSUMER. Every id must name a subsystem of the tree (PUBLIC_INTERFACE_UNKNOWN_CONSUMER)."),
30311
+ from: import_zod11.z.string().optional().describe("Re-export source: another subsystem of this project. With neither component nor typeDef, the entry re-exports everything that subsystem exports (a wildcard)"),
30312
+ typeDef: import_zod11.z.string().optional().describe("A type exported as is: owned by this subsystem, or re-exported from `from`"),
30313
+ as: import_zod11.z.string().optional().describe("The public name; defaults to the narrowed interface, then the component or type id. Must fit [a-z0-9-_]+ (EXPORT_INVALID otherwise)")
30314
+ }).strict();
29659
30315
  const subsystemInput = {
29660
30316
  id: import_zod11.z.string().describe("Lowercase identifier for the subsystem"),
29661
30317
  name: import_zod11.z.string().describe("Human-readable display name"),
29662
30318
  description: import_zod11.z.string().describe("Purpose and details of the subsystem"),
29663
- publicInterfaces: import_zod11.z.array(import_zod11.z.object({
29664
- type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
29665
- details: import_zod11.z.string(),
29666
- component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface (this subsystem's published surface)"),
29667
- interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component backing this entry"),
29668
- consumers: import_zod11.z.array(import_zod11.z.string()).optional().describe("The subsystem ids this surface is published to. Absent, any subsystem may depend on the component; present, only these may \u2014 when every entry publishing the component names consumers, the union of the lists is the whole set, and a dependency from any other subsystem is CROSS_SUBSYSTEM_UNLISTED_CONSUMER. Every id must name a subsystem of the tree (PUBLIC_INTERFACE_UNKNOWN_CONSUMER).")
29669
- }).strict()).optional().describe("Public entrypoints exposed by this subsystem, each bound to a realizing component"),
30319
+ publicInterfaces: import_zod11.z.array(publicInterfaceItemInput).optional().describe("The subsystem's export table: its own entrypoints, each bound to a realizing component, and re-exports of what other subsystems of this project export"),
29670
30320
  projectPath: import_zod11.z.string().optional().describe("Relative path to external project root for subsystem chaining"),
29671
30321
  targetLanguage: import_zod11.z.string().optional().describe("Override of the system-level targetLanguage for this subsystem"),
29672
30322
  profile: import_zod11.z.string().optional().describe("Architectural profile override for this subsystem (built-ins: backend, frontend-reactive, frontend-controller, lowlevel-os, game-ecs, realtime-embedded, plc-cyclic; extension packs may add more \u2014 unknown names get UNKNOWN_PROFILE)"),
@@ -29728,16 +30378,10 @@ function createMcpServer(options = {}) {
29728
30378
  server,
29729
30379
  "sdd_set_public_interfaces",
29730
30380
  {
29731
- description: "Set (replace) an existing subsystem's publicInterfaces, binding each to the component (and optional interface) that realizes it. Use this to backfill bindings once the subsystem's components exist \u2014 cross-subsystem dependencies may only target a published public component. A replacement, not a merge: an entry left out is removed. An entry's identity is its component and interface, so a list naming one identity twice is refused. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every entry added, changed or removed, or that nothing changed \u2014 as structured content beside the sentence.",
30381
+ description: "Set (replace) an existing subsystem's export table (its publicInterfaces). An OWN item binds type + details to the component (and optional interface) that realizes it, or exports a type the subsystem owns (typeDef). A RE-EXPORT names another subsystem of the project in `from` and one component (optionally narrowed with `interface`) or one typeDef from what it exports, optionally renamed with `as` \u2014 `from` alone re-exports everything it exports; type and details are optional on a re-export, which inherits its target's. Use this to backfill bindings once the subsystem's components exist \u2014 cross-subsystem dependencies may only target a component its owning subsystem publishes; a re-export is a name, not a licence. A replacement, not a merge: an entry left out is removed. An own entry's identity is its component and interface, a re-export's its source, item and name, so a list naming one identity twice is refused. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every entry added, changed or removed, or that nothing changed \u2014 as structured content beside the sentence.",
29732
30382
  inputSchema: {
29733
30383
  subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
29734
- publicInterfaces: import_zod11.z.array(import_zod11.z.object({
29735
- type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
29736
- details: import_zod11.z.string(),
29737
- component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface"),
29738
- interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component"),
29739
- consumers: import_zod11.z.array(import_zod11.z.string()).optional().describe("The subsystem ids this surface is published to. Absent, any subsystem may depend on the component; present, only these may \u2014 when every entry publishing the component names consumers, the union of the lists is the whole set, and a dependency from any other subsystem is CROSS_SUBSYSTEM_UNLISTED_CONSUMER. Every id must name a subsystem of the tree (PUBLIC_INTERFACE_UNKNOWN_CONSUMER).")
29740
- }).strict()).describe("The full replacement list of public interfaces for this subsystem")
30384
+ publicInterfaces: import_zod11.z.array(publicInterfaceItemInput).describe("The full replacement list of the subsystem's export table: own items and re-exports")
29741
30385
  },
29742
30386
  outputSchema: specChangeReportOutput
29743
30387
  },
@@ -30003,7 +30647,7 @@ ${renderChangeReport(report)}`,
30003
30647
  }).strict().optional().describe("Typed acknowledgment of a real caller OUTSIDE the modeled narrative graph. Unused-detection seeds the method as an entrypoint so reachability propagates through its narrative (unlike lint.allow); a method the internal walk already reaches is flagged stale (INVOKED_BY_REDUNDANT). Prefer a `register` narrative step when the wiring is internal."),
30004
30648
  findings: import_zod11.z.array(import_zod11.z.object({
30005
30649
  code: import_zod11.z.string().describe("UPPER_SNAKE finding code, unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>)"),
30006
- severity: import_zod11.z.enum(["error", "warning"]).describe("Default severity, before project severity overrides and draft-context downgrades"),
30650
+ severity: import_zod11.z.enum(["error", "warning", "notice"]).describe("Default severity, before project severity overrides and draft-context downgrades. A notice is reported but never fails the gate"),
30007
30651
  summary: import_zod11.z.string().describe("One line saying what the finding means")
30008
30652
  }).strict()).optional().describe("The finding codes this method can report, each with its default severity and summary. Each declared code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). A code is declared once per method; sdd_update_spec upserts and deletes findings by code."),
30009
30653
  ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim")
@@ -30447,9 +31091,9 @@ ${testsBlock}` : ""}`, deletion);
30447
31091
  {
30448
31092
  description: "Update/patch an existing SDD specification (subsystem, component, interface, implementation, or type) using a granular delta. Updates fields, appends/merges array elements, or inserts/deletes narrative steps. Answers with exactly what changed, and with every path the delta named that the write did not act on. Pass dryRun to be told what it would do without writing it.",
30449
31093
  inputSchema: {
30450
- kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project gateway surface, each entry {id, name, subsystem, component, type, details, audience: project|department|instance|partner|external}; id is informational)"),
31094
+ kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project export table, each entry a re-export {from, component, interface, typeDef, as, name, type, details, audience: project|department|instance|partner|external} \u2014 from alone re-exports everything a subsystem exports, and a legacy {id, subsystem} reads as {as, from})"),
30451
31095
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
30452
- delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
31096
+ delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component, and by from+item+interface+as for a re-export or a type export), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
30453
31097
  dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
30454
31098
  },
30455
31099
  outputSchema: specChangeReportOutput
@@ -30702,7 +31346,9 @@ function loadProjectConfig3() {
30702
31346
  NarrativeStepTypeSchema,
30703
31347
  OutputTargetSchema,
30704
31348
  PATTERN_TYPES,
31349
+ PROJECT_ID_RE,
30705
31350
  PROJECT_KINDS,
31351
+ PUBLIC_NAME_RE,
30706
31352
  PackSelectionSchema,
30707
31353
  ParallelBranchSchema,
30708
31354
  PathsConfigSchema,
@@ -30737,6 +31383,7 @@ function loadProjectConfig3() {
30737
31383
  SurfaceOriginSchema,
30738
31384
  SurfaceSnapshotSchema,
30739
31385
  SurfaceTypeDefSchema,
31386
+ SurfaceTypeExportSchema,
30740
31387
  SwitchCaseSchema,
30741
31388
  SystemPublicInterfaceSchema,
30742
31389
  SystemSpecSchema,
@@ -30807,10 +31454,12 @@ function loadProjectConfig3() {
30807
31454
  discoverPacks,
30808
31455
  dryRunSerializeSpecs,
30809
31456
  effectiveDetail,
31457
+ effectiveProjectId,
30810
31458
  emptyExtensions,
30811
31459
  ensureDir,
30812
31460
  ensureProjectInitialized,
30813
31461
  exportSpecTree,
31462
+ exportTargetKey,
30814
31463
  externalizeSubsystem,
30815
31464
  extractTypeIdentifiers,
30816
31465
  factsFor,
@@ -30851,6 +31500,7 @@ function loadProjectConfig3() {
30851
31500
  internalizeSubsystem,
30852
31501
  isDraftSubsystem,
30853
31502
  isLogic,
31503
+ isOwnComponentEntry,
30854
31504
  isPattern,
30855
31505
  isProjectInitialized,
30856
31506
  isProvidedBy,
@@ -30904,6 +31554,7 @@ function loadProjectConfig3() {
30904
31554
  pathKey,
30905
31555
  pinInstalledPacksAsSelections,
30906
31556
  projectConfigExists,
31557
+ projectIdentity,
30907
31558
  provisionProject,
30908
31559
  qualifiedTypeId,
30909
31560
  readFileOrNull,
@@ -30929,7 +31580,9 @@ function loadProjectConfig3() {
30929
31580
  resolveExpectedOutputPaths,
30930
31581
  resolveImport,
30931
31582
  resolveInstalledPack,
31583
+ resolveProjectExports,
30932
31584
  resolveSubprojectForNamespace,
31585
+ resolveSubsystemExports,
30933
31586
  resolveVariantGuidance,
30934
31587
  retireSpecialists,
30935
31588
  runWithProjectBinding,