@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/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.79";
68
+ WAIRON_VERSION = "5.1.1-dev.80";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -616,7 +616,40 @@ function declaredPackNames(config) {
616
616
  function declaredProfileIds(config) {
617
617
  return [...new Set(config.profileSelection?.profileIds ?? [])];
618
618
  }
619
- var path5, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema, PACK_EXT_RE;
619
+ function slugifyProjectName(name) {
620
+ const slug = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "");
621
+ return slug === "" ? null : slug;
622
+ }
623
+ function effectiveProjectId(config) {
624
+ if (config.id !== void 0) return config.id;
625
+ return slugifyProjectName(config.name);
626
+ }
627
+ function projectIdentity(config, lockedProjectId) {
628
+ const id = effectiveProjectId(config);
629
+ const source = config.id !== void 0 ? "declared" : id !== null ? "defaulted" : "none";
630
+ const problems = [];
631
+ if (source === "defaulted") {
632
+ problems.push({ kind: "defaulted", detail: `no id is declared; the name "${config.name}" yields "${id}"` });
633
+ } else if (source === "none") {
634
+ problems.push({ kind: "ambiguous", detail: `no id is declared, and the name "${config.name}" yields no id` });
635
+ } else if (!PROJECT_ID_RE.test(config.id)) {
636
+ problems.push({ kind: "ambiguous", detail: `the declared id "${config.id}" breaks the project-id grammar` });
637
+ }
638
+ if (lockedProjectId !== void 0 && id !== lockedProjectId) {
639
+ problems.push({
640
+ kind: "changed",
641
+ 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}"`
642
+ });
643
+ }
644
+ return {
645
+ ...id !== null ? { id } : {},
646
+ source,
647
+ name: config.name,
648
+ ...lockedProjectId !== void 0 ? { lockedId: lockedProjectId } : {},
649
+ problems
650
+ };
651
+ }
652
+ var path5, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema, PACK_EXT_RE, PROJECT_ID_RE;
620
653
  var init_project = __esm({
621
654
  "src/models/project.ts"() {
622
655
  "use strict";
@@ -938,7 +971,16 @@ var init_project = __esm({
938
971
  * CLI versions.
939
972
  */
940
973
  schemaVersion: import_zod3.z.string().default("1.0.0"),
941
- /** Human-readable project name */
974
+ /**
975
+ * The project's stable identity: a slug of [a-z0-9-_.] that starts and ends
976
+ * alphanumeric (PROJECT_ID_RE). Optional in the file: a project that declares
977
+ * none answers to its name slugified (effectiveProjectId) until a deliberate
978
+ * writer sets one — an ordinary save never writes the default. Deliberately a plain
979
+ * string here rather than the grammar: a malformed id must be REPORTED
980
+ * (PROJECT_ID_AMBIGUOUS), not make the whole configuration unreadable.
981
+ */
982
+ id: import_zod3.z.string().optional(),
983
+ /** Human-readable display name. Not an identity — `id` is. */
942
984
  name: import_zod3.z.string(),
943
985
  /**
944
986
  * The type/profile of the project, which configures targeted guidelines, rules,
@@ -1032,6 +1074,7 @@ var init_project = __esm({
1032
1074
  updatedAt: import_zod3.z.string().datetime()
1033
1075
  });
1034
1076
  PACK_EXT_RE = /\.(ya?ml|cjs|js)$/i;
1077
+ PROJECT_ID_RE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
1035
1078
  }
1036
1079
  });
1037
1080
 
@@ -1319,18 +1362,19 @@ function registryOver(store, root) {
1319
1362
  if (!config) throw new ProjectNotInitializedError();
1320
1363
  return config;
1321
1364
  };
1365
+ const save4 = (config) => store.write(config);
1322
1366
  return {
1323
1367
  create(config) {
1324
1368
  if (store.exists()) {
1325
1369
  throw new WaironError(`A project configuration already exists at ${root}; creating one never overwrites it.`);
1326
1370
  }
1327
- store.write(config);
1371
+ save4(config);
1328
1372
  },
1329
1373
  upsertPackSelection(selection) {
1330
1374
  const config = current2();
1331
1375
  const existing = packsOf(config);
1332
1376
  const without = existing.filter((entry) => typeof entry === "string" || entry.name !== selection.name);
1333
- store.write(withPacks(config, [...without, selection]));
1377
+ save4(withPacks(config, [...without, selection]));
1334
1378
  return without.length !== existing.length;
1335
1379
  },
1336
1380
  removePackSelection(packName) {
@@ -1338,24 +1382,24 @@ function registryOver(store, root) {
1338
1382
  const existing = packsOf(config);
1339
1383
  const remaining = existing.filter((entry) => typeof entry === "string" || entry.name !== packName);
1340
1384
  if (remaining.length === existing.length) return false;
1341
- store.write(withPacks(config, remaining));
1385
+ save4(withPacks(config, remaining));
1342
1386
  return true;
1343
1387
  },
1344
1388
  setProjectType(projectType) {
1345
- store.write({ ...current2(), projectType });
1389
+ save4({ ...current2(), projectType });
1346
1390
  },
1347
1391
  recordProfileSelection(selection) {
1348
- store.write({ ...current2(), profileSelection: selection });
1392
+ save4({ ...current2(), profileSelection: selection });
1349
1393
  },
1350
1394
  setExecutionTier(tier) {
1351
1395
  const config = current2();
1352
- store.write({ ...config, execution: { ...config.execution, tier } });
1396
+ save4({ ...config, execution: { ...config.execution, tier } });
1353
1397
  },
1354
1398
  registerPackRef(ref) {
1355
1399
  const config = current2();
1356
1400
  const packs = packsOf(config);
1357
1401
  if (packs.includes(ref)) return false;
1358
- store.write(withPacks(config, [...packs, ref]));
1402
+ save4(withPacks(config, [...packs, ref]));
1359
1403
  return true;
1360
1404
  },
1361
1405
  deregisterPackRef(ref) {
@@ -1363,16 +1407,16 @@ function registryOver(store, root) {
1363
1407
  const packs = packsOf(config);
1364
1408
  const remaining = packs.filter((entry) => entry !== ref);
1365
1409
  if (remaining.length === packs.length) return false;
1366
- store.write(withPacks(config, remaining));
1410
+ save4(withPacks(config, remaining));
1367
1411
  return true;
1368
1412
  },
1369
1413
  markSelectionsBundled(bundled) {
1370
1414
  const config = current2();
1371
- store.write(withPacks(config, bundleInPlace(packsOf(config), bundled)));
1415
+ save4(withPacks(config, bundleInPlace(packsOf(config), bundled)));
1372
1416
  },
1373
1417
  pinGlobalPacksAsSelections(selections) {
1374
1418
  const config = current2();
1375
- store.write({
1419
+ save4({
1376
1420
  ...config,
1377
1421
  extensions: { ...config.extensions, packs: [...packsOf(config), ...selections], useGlobalPacks: false }
1378
1422
  });
@@ -1906,6 +1950,9 @@ function splitNamespace(qualifiedId2) {
1906
1950
  const localId = parts.pop();
1907
1951
  return { prefix: parts.join("::"), localId };
1908
1952
  }
1953
+ function isOwnComponentEntry(pi) {
1954
+ return pi.from === void 0 && pi.typeDef === void 0;
1955
+ }
1909
1956
  function isDraftSubsystem(subsystem) {
1910
1957
  return subsystem.status === "draft" || subsystem.status === "design";
1911
1958
  }
@@ -2019,7 +2066,7 @@ function typeSourceFiles(type) {
2019
2066
  for (const method2 of type.methods ?? []) add(method2.sourcePath);
2020
2067
  return files;
2021
2068
  }
2022
- var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
2069
+ var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceTypeExportSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
2023
2070
  var init_specs = __esm({
2024
2071
  "src/models/specs.ts"() {
2025
2072
  "use strict";
@@ -2055,15 +2102,21 @@ var init_specs = __esm({
2055
2102
  SURFACE_AUDIENCES = ["project", "department", "instance", "partner", "external"];
2056
2103
  SurfaceAudienceSchema = import_zod8.z.enum(SURFACE_AUDIENCES);
2057
2104
  SystemPublicInterfaceSchema = import_zod8.z.object({
2058
- /** Stable public interface id within the system. */
2105
+ /** Legacy public name, read as `as` when `as` is absent. */
2059
2106
  id: import_zod8.z.string().optional(),
2060
2107
  name: import_zod8.z.string().optional(),
2061
- /** Subsystem publishing the backing L1 public interface. */
2108
+ /** Legacy source subsystem, read as `from` when `from` is absent. */
2062
2109
  subsystem: import_zod8.z.string().optional(),
2063
- /** Portal (or compatible published component) backing this entry. */
2110
+ /** The component re-exported one to one; without it and without `typeDef`, a wildcard. */
2064
2111
  component: import_zod8.z.string().optional(),
2065
- /** Optional L3 interface id backing the surface. */
2112
+ /** The L3 interface the export narrows to. */
2066
2113
  interface: import_zod8.z.string().optional(),
2114
+ /** The source subsystem the entry re-exports from. */
2115
+ from: import_zod8.z.string().optional(),
2116
+ /** A type exported as is, giving it a public name. */
2117
+ typeDef: import_zod8.z.string().optional(),
2118
+ /** The public name: defaults to `id`, then `interface`, then `component` or `typeDef`. */
2119
+ as: import_zod8.z.string().optional(),
2067
2120
  /** Surface kind: REST, GraphQL, MessageBus, RPC, or Custom. */
2068
2121
  type: import_zod8.z.string().optional(),
2069
2122
  details: import_zod8.z.string().optional(),
@@ -2104,18 +2157,29 @@ var init_specs = __esm({
2104
2157
  });
2105
2158
  PublicInterfaceTypeSchema = import_zod8.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]);
2106
2159
  PublicInterfaceSchema = import_zod8.z.object({
2107
- type: PublicInterfaceTypeSchema,
2108
- details: import_zod8.z.string(),
2109
- /** The L2 component that realizes this public interface (the subsystem's published surface). */
2160
+ /** Transport kind of an own item; a re-export inherits its target's. */
2161
+ type: PublicInterfaceTypeSchema.optional(),
2162
+ /** Consumer-facing description of an own item; a re-export inherits its target's. */
2163
+ details: import_zod8.z.string().optional(),
2164
+ /** For an own item, the L2 component that realizes it; for a re-export, the component re-exported from `from`. */
2110
2165
  component: SpecIdSchema.optional(),
2111
- /** Optional L3 interface on that component backing this entry. */
2166
+ /** Optional L3 interface the entry narrows to. */
2112
2167
  interface: SpecIdSchema.optional(),
2168
+ /** Re-export source: another subsystem of the same project. */
2169
+ from: SpecIdSchema.optional(),
2170
+ /** A type exported as is: owned by this subsystem, or re-exported from `from`. */
2171
+ typeDef: SpecIdSchema.optional(),
2172
+ /** The public name; defaults to the narrowed interface, then the component or type id. */
2173
+ as: import_zod8.z.string().optional(),
2113
2174
  /**
2114
2175
  * The subsystems this surface is published to. Absent, anyone may depend on
2115
2176
  * it; present, only these subsystems may — when every entry publishing the
2116
2177
  * component names consumers, the union of those lists is the whole set.
2117
2178
  */
2118
2179
  consumers: import_zod8.z.array(SpecIdSchema).optional()
2180
+ }).refine((pi) => pi.from !== void 0 || pi.typeDef !== void 0 || pi.type !== void 0 && pi.details !== void 0, {
2181
+ message: "An own component entry states its `type` and `details`; only a re-export (`from`) or a type export (`typeDef`) may leave them out.",
2182
+ path: ["type"]
2119
2183
  });
2120
2184
  TrustedLinkSchema = import_zod8.z.object({
2121
2185
  /** The peer subsystem id this link sanctions tight coupling with. */
@@ -2392,8 +2456,8 @@ var init_specs = __esm({
2392
2456
  FindingDeclarationSchema = import_zod8.z.object({
2393
2457
  /** UPPER_SNAKE and unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>). */
2394
2458
  code: import_zod8.z.string().regex(/^[A-Z][A-Z0-9_]*$/, "Finding code must be UPPER_SNAKE, e.g. UNREALIZED_FINDING"),
2395
- /** The default before project severity overrides and draft-context downgrades. */
2396
- severity: import_zod8.z.enum(["error", "warning"]),
2459
+ /** The default before project severity overrides and draft-context downgrades; a notice is reported but never fails the gate. */
2460
+ severity: import_zod8.z.enum(["error", "warning", "notice"]),
2397
2461
  /** One line saying what the finding means. */
2398
2462
  summary: import_zod8.z.string().min(1, "Finding summary must say what the finding means")
2399
2463
  });
@@ -2865,6 +2929,14 @@ var init_specs = __esm({
2865
2929
  optional: import_zod8.z.boolean().optional()
2866
2930
  })).default([])
2867
2931
  });
2932
+ SurfaceTypeExportSchema = import_zod8.z.object({
2933
+ /** The type's public name in the producer's export table. */
2934
+ id: import_zod8.z.string(),
2935
+ /** The id of the type's definition in the snapshot's type closure. */
2936
+ type: import_zod8.z.string(),
2937
+ /** Exposure level from the L0 entry. */
2938
+ audience: import_zod8.z.string().default("instance")
2939
+ });
2868
2940
  SurfaceContractEntrySchema = import_zod8.z.object({
2869
2941
  id: import_zod8.z.string(),
2870
2942
  name: import_zod8.z.string(),
@@ -2885,7 +2957,11 @@ var init_specs = __esm({
2885
2957
  * emits it as OpenAPI securitySchemes/security. */
2886
2958
  auth: PortalAuthSchema.optional(),
2887
2959
  /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
2888
- basePath: import_zod8.z.string().optional()
2960
+ basePath: import_zod8.z.string().optional(),
2961
+ /** Stereotype of the backing component; absent on snapshots written before exports carried it. */
2962
+ componentType: import_zod8.z.string().optional(),
2963
+ /** The L3 interface the export narrows to, when it narrows. */
2964
+ interface: import_zod8.z.string().optional()
2889
2965
  });
2890
2966
  SurfaceSnapshotSchema = import_zod8.z.object({
2891
2967
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -2897,8 +2973,12 @@ var init_specs = __esm({
2897
2973
  version: import_zod8.z.string().optional(),
2898
2974
  generatedAt: import_zod8.z.string(),
2899
2975
  interfaces: import_zod8.z.array(SurfaceContractEntrySchema).default([]),
2900
- /** Transitive type closure of every exported signature — self-contained. */
2901
- types: import_zod8.z.array(SurfaceTypeDefSchema).default([])
2976
+ /** Transitive type closure of every exported signature and exported type — self-contained. */
2977
+ types: import_zod8.z.array(SurfaceTypeDefSchema).default([]),
2978
+ /** The producing project's effective id, beside the display-oriented projectName. */
2979
+ projectId: import_zod8.z.string().optional(),
2980
+ /** The types the producer exports by name, listed apart from the contract entries. */
2981
+ exportedTypes: import_zod8.z.array(SurfaceTypeExportSchema).optional()
2902
2982
  });
2903
2983
  NamedOpenApiSpecSchema = import_zod8.z.object({
2904
2984
  portalId: import_zod8.z.string(),
@@ -3464,6 +3544,18 @@ var init_surface_references = __esm({
3464
3544
  }
3465
3545
  });
3466
3546
 
3547
+ // src/models/exports.ts
3548
+ function exportTargetKey(e) {
3549
+ return e.kind === "type" ? `type:${e.typeDef}` : `component:${e.component}${e.interface ? `#${e.interface}` : ""}`;
3550
+ }
3551
+ var PUBLIC_NAME_RE;
3552
+ var init_exports = __esm({
3553
+ "src/models/exports.ts"() {
3554
+ "use strict";
3555
+ PUBLIC_NAME_RE = /^[a-z0-9_-]+$/;
3556
+ }
3557
+ });
3558
+
3467
3559
  // src/models/index.ts
3468
3560
  var init_models = __esm({
3469
3561
  "src/models/index.ts"() {
@@ -3479,6 +3571,7 @@ var init_models = __esm({
3479
3571
  init_step_config();
3480
3572
  init_code_model();
3481
3573
  init_surface_references();
3574
+ init_exports();
3482
3575
  }
3483
3576
  });
3484
3577
 
@@ -3493,7 +3586,7 @@ function resolveNarrativeLabels(methodName, steps) {
3493
3586
  else labels.set(s.label, s.stepNumber);
3494
3587
  }
3495
3588
  }
3496
- const lookup = (ref, where) => {
3589
+ const lookup2 = (ref, where) => {
3497
3590
  if (typeof ref !== "string" || !ref.length) {
3498
3591
  errors.push(`${where} is not a label string`);
3499
3592
  return void 0;
@@ -3507,7 +3600,7 @@ function resolveNarrativeLabels(methodName, steps) {
3507
3600
  for (const s of steps) {
3508
3601
  for (const [labelField, stepField] of SCALAR_REFS) {
3509
3602
  if (s[labelField] === void 0) continue;
3510
- const n = lookup(s[labelField], `step ${s.stepNumber} ${labelField}`);
3603
+ const n = lookup2(s[labelField], `step ${s.stepNumber} ${labelField}`);
3511
3604
  if (n !== void 0) {
3512
3605
  if (typeof s[stepField] === "number" && s[stepField] !== n) {
3513
3606
  errors.push(`step ${s.stepNumber} sets both ${stepField}=${s[stepField]} and ${labelField}="${s[labelField]}" (= step ${n}) \u2014 they disagree`);
@@ -3523,7 +3616,7 @@ function resolveNarrativeLabels(methodName, steps) {
3523
3616
  for (const entry of arr) {
3524
3617
  if (!entry || typeof entry !== "object" || entry.label === void 0) continue;
3525
3618
  const e = entry;
3526
- const n = lookup(e.label, `step ${s.stepNumber} ${key} entry`);
3619
+ const n = lookup2(e.label, `step ${s.stepNumber} ${key} entry`);
3527
3620
  if (n !== void 0) {
3528
3621
  if (typeof e.step === "number" && e.step !== n) {
3529
3622
  errors.push(`step ${s.stepNumber} ${key} entry sets both step=${e.step} and label="${e.label}" (= step ${n}) \u2014 they disagree`);
@@ -3984,13 +4077,17 @@ function buildCanvasModel(issues = []) {
3984
4077
  const componentIds = new Set(components.map((c) => c.id));
3985
4078
  const publicComponents = /* @__PURE__ */ new Set();
3986
4079
  for (const sub of subsystems) {
3987
- for (const pi of sub.publicInterfaces) {
4080
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
3988
4081
  if (pi.component) publicComponents.add(pi.component);
3989
4082
  }
3990
4083
  }
3991
4084
  const apiTagOf = /* @__PURE__ */ new Map();
3992
- for (const pi of system?.publicInterfaces ?? []) {
3993
- if (pi.component && pi.id) apiTagOf.set(pi.component, pi.id);
4085
+ if (system) {
4086
+ for (const entry of resolveProjectExports().entries) {
4087
+ if (entry.kind === "component" && entry.component && !apiTagOf.has(entry.component)) {
4088
+ apiTagOf.set(entry.component, entry.publicName);
4089
+ }
4090
+ }
3994
4091
  }
3995
4092
  const patternTypes = /* @__PURE__ */ new Set(["Repository", "FeatureComponent", "RouterComponent"]);
3996
4093
  const ownerOf2 = /* @__PURE__ */ new Map();
@@ -4156,7 +4253,7 @@ function buildCanvasModel(issues = []) {
4156
4253
  }
4157
4254
  const portalOf = /* @__PURE__ */ new Map();
4158
4255
  for (const sub of subsystems) {
4159
- for (const pi of sub.publicInterfaces) {
4256
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
4160
4257
  if (pi.component && !portalOf.has(sub.id)) portalOf.set(sub.id, pi.component);
4161
4258
  }
4162
4259
  }
@@ -8811,19 +8908,25 @@ function stateIdString() {
8811
8908
  const s = computeStateId();
8812
8909
  return `${s.algorithm}:${s.digest}`;
8813
8910
  }
8814
- function computeTypeClosure(entries, types) {
8911
+ function qualifiedTypeId2(spec) {
8912
+ return spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
8913
+ }
8914
+ function computeTypeClosure(entries, types, exported = []) {
8815
8915
  const included = /* @__PURE__ */ new Map();
8816
8916
  const queue = [];
8917
+ const include = (spec) => {
8918
+ const key = qualifiedTypeId2(spec);
8919
+ if (included.has(key)) return;
8920
+ included.set(key, spec);
8921
+ queue.push(key);
8922
+ };
8817
8923
  const enqueueRef = (ref) => {
8818
8924
  if (BUILTIN_TYPES.has(ref.toLowerCase())) return;
8819
8925
  for (const spec of types) {
8820
- const qualifiedId2 = spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
8821
- if (matchTypeRef(ref, qualifiedId2) && !included.has(spec.id)) {
8822
- included.set(spec.id, spec);
8823
- queue.push(spec.id);
8824
- }
8926
+ if (matchTypeRef(ref, qualifiedTypeId2(spec))) include(spec);
8825
8927
  }
8826
8928
  };
8929
+ for (const spec of exported) include(spec);
8827
8930
  for (const entry of entries) {
8828
8931
  for (const m of entry.methods) {
8829
8932
  for (const ref of methodTypeRefs(m)) enqueueRef(ref);
@@ -8835,63 +8938,87 @@ function computeTypeClosure(entries, types) {
8835
8938
  for (const ref of extractTypeIdentifiers(field.type)) enqueueRef(ref);
8836
8939
  }
8837
8940
  }
8838
- return [...included.values()].map((t) => ({
8839
- id: t.id,
8840
- name: t.name,
8841
- kind: t.kind,
8842
- fields: t.fields.map((f) => ({
8843
- name: f.name,
8844
- type: f.type,
8845
- ...f.description ? { description: f.description } : {},
8846
- ...f.optional ? { optional: true } : {}
8847
- }))
8848
- }));
8941
+ const usedIds = /* @__PURE__ */ new Set();
8942
+ return [...included.entries()].map(([qualified, t]) => {
8943
+ const id = usedIds.has(t.id) ? qualified : t.id;
8944
+ usedIds.add(id);
8945
+ return {
8946
+ id,
8947
+ name: t.name,
8948
+ kind: t.kind,
8949
+ fields: t.fields.map((f) => ({
8950
+ name: f.name,
8951
+ type: f.type,
8952
+ ...f.description ? { description: f.description } : {},
8953
+ ...f.optional ? { optional: true } : {}
8954
+ }))
8955
+ };
8956
+ });
8957
+ }
8958
+ function boundProjectId() {
8959
+ try {
8960
+ const config = loadProjectConfig();
8961
+ return config ? effectiveProjectId(config) ?? void 0 : void 0;
8962
+ } catch {
8963
+ return void 0;
8964
+ }
8965
+ }
8966
+ function contractEntry(entry, comp, interfaces) {
8967
+ const methods = interfaces.filter((i) => i.component === comp.id && (!entry.interface || i.id === entry.interface)).flatMap((i) => i.methods);
8968
+ return {
8969
+ id: entry.publicName,
8970
+ name: entry.name ?? comp.name,
8971
+ audience: entry.audience ?? "instance",
8972
+ type: entry.type ?? "Custom",
8973
+ component: comp.id,
8974
+ methods,
8975
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
8976
+ // Project the backing Portal's auth + basePath so the codec can emit
8977
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
8978
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
8979
+ ...comp.basePath ? { basePath: comp.basePath } : {},
8980
+ details: entry.details ?? "",
8981
+ ...entry.version ? { version: entry.version } : {},
8982
+ ...entry.stability ? { stability: entry.stability } : {},
8983
+ componentType: comp.componentType,
8984
+ ...entry.interface ? { interface: entry.interface } : {}
8985
+ };
8849
8986
  }
8850
8987
  function projectOwnSurface(maxAudience) {
8851
8988
  const system = loadSystemSpec();
8852
8989
  if (!system) {
8853
8990
  throw new Error("Cannot project a surface: the L0 system spec is missing.");
8854
8991
  }
8855
- const subsystems = loadSubsystemSpecs();
8992
+ const table = resolveProjectExports();
8856
8993
  const components = loadComponentSpecs();
8857
8994
  const interfaces = loadInterfaceSpecs();
8858
8995
  const types = loadTypeSpecs();
8859
8996
  const floor = audienceRank(maxAudience);
8860
- const rawEntries = system.publicInterfaces ?? [];
8997
+ const visible = table.entries.filter((e) => audienceRank(e.audience ?? "instance") >= floor);
8861
8998
  const entries = [];
8862
- for (const raw of rawEntries) {
8863
- const audience = raw.audience ?? "instance";
8864
- if (audienceRank(audience) < floor) continue;
8865
- if (!raw.component) continue;
8866
- const comp = components.find((c) => c.id === raw.component);
8867
- if (!comp) continue;
8868
- const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!raw.interface || i.id === raw.interface));
8869
- const methods = compInterfaces.flatMap((i) => i.methods);
8870
- const subsystemType = subsystems.find((s) => s.id === comp.subsystem)?.publicInterfaces.find((pi) => pi.component === comp.id)?.type;
8871
- entries.push({
8872
- id: raw.id ?? raw.interface ?? comp.id,
8873
- name: raw.name ?? comp.name,
8874
- audience,
8875
- type: raw.type ?? subsystemType ?? "Custom",
8876
- component: comp.id,
8877
- methods,
8878
- ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
8879
- // Project the backing Portal's auth + basePath so the codec can emit
8880
- // OpenAPI security + per-portal servers self-contained from the snapshot.
8881
- ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
8882
- ...comp.basePath ? { basePath: comp.basePath } : {},
8883
- details: raw.details ?? "",
8884
- ...raw.version ? { version: raw.version } : {},
8885
- ...raw.stability ? { stability: raw.stability } : {}
8886
- });
8887
- }
8999
+ for (const entry of visible) {
9000
+ if (entry.kind !== "component") continue;
9001
+ const comp = components.find((c) => c.id === entry.component);
9002
+ if (comp) entries.push(contractEntry(entry, comp, interfaces));
9003
+ }
9004
+ const exportedTypes = visible.filter((e) => e.kind === "type");
9005
+ const exportedSpecs = exportedTypes.map((e) => types.find((t) => t.id === e.typeDef && (t.subsystem ?? e.source) === e.source)).filter((t) => !!t);
9006
+ const closure = computeTypeClosure(entries, types, exportedSpecs);
9007
+ const closureIdOf = (e) => {
9008
+ const spec = types.find((t) => t.id === e.typeDef && (t.subsystem ?? e.source) === e.source);
9009
+ if (!spec) return String(e.typeDef);
9010
+ return closure.some((d) => d.id === qualifiedTypeId2(spec)) ? qualifiedTypeId2(spec) : spec.id;
9011
+ };
9012
+ const projectId = boundProjectId();
8888
9013
  return SurfaceSnapshotSchema.parse({
8889
9014
  projectName: system.name,
9015
+ ...projectId ? { projectId } : {},
8890
9016
  origin: "generated",
8891
9017
  stateId: stateIdString(),
8892
9018
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8893
9019
  interfaces: entries,
8894
- types: computeTypeClosure(entries, types)
9020
+ types: closure,
9021
+ ...exportedTypes.length ? { exportedTypes: exportedTypes.map((e) => ({ id: e.publicName, type: closureIdOf(e), audience: e.audience ?? "instance" })) } : {}
8895
9022
  });
8896
9023
  }
8897
9024
  function projectChildSurface() {
@@ -9113,7 +9240,8 @@ function listExternalInterfaces() {
9113
9240
  ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
9114
9241
  ...snapshot.version ? { version: snapshot.version } : {},
9115
9242
  freshness,
9116
- interfaceIds: snapshot.interfaces.map((e) => e.id)
9243
+ interfaceIds: snapshot.interfaces.map((e) => e.id),
9244
+ ...snapshot.projectId ? { projectId: snapshot.projectId } : {}
9117
9245
  };
9118
9246
  });
9119
9247
  }
@@ -10092,18 +10220,18 @@ function buildImportGraph(index, universe) {
10092
10220
  };
10093
10221
  }
10094
10222
  function buildOwnershipIndex(ctx) {
10095
- const ownedBy = /* @__PURE__ */ new Map();
10223
+ const ownedBy2 = /* @__PURE__ */ new Map();
10096
10224
  for (const comp of ctx.components) {
10097
10225
  if (isRetired(comp) || !isPattern(comp)) continue;
10098
10226
  for (const memberId of comp.owns) {
10099
10227
  const member = ctx.componentMap.get(memberId);
10100
10228
  if (!member || isPattern(member)) continue;
10101
- if (!ownedBy.has(memberId)) ownedBy.set(memberId, comp.id);
10229
+ if (!ownedBy2.has(memberId)) ownedBy2.set(memberId, comp.id);
10102
10230
  }
10103
10231
  }
10104
10232
  return {
10105
- ownedMembers: new Set(ownedBy.keys()),
10106
- ownerOf: (memberId) => ownedBy.get(memberId)
10233
+ ownedMembers: new Set(ownedBy2.keys()),
10234
+ ownerOf: (memberId) => ownedBy2.get(memberId)
10107
10235
  };
10108
10236
  }
10109
10237
  function buildDependencyEdges(ctx) {
@@ -10323,6 +10451,51 @@ var init_namespace_shadowing = __esm({
10323
10451
  }
10324
10452
  });
10325
10453
 
10454
+ // src/core/rules/integrity/project-identity.ts
10455
+ var projectIdentityRule;
10456
+ var init_project_identity = __esm({
10457
+ "src/core/rules/integrity/project-identity.ts"() {
10458
+ "use strict";
10459
+ projectIdentityRule = {
10460
+ name: "project-identity",
10461
+ 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).",
10462
+ codes: [
10463
+ { code: "PROJECT_ID_DEFAULTED", defaultSeverity: "notice", summary: "Project declares no id; it answers to one derived from its display name" },
10464
+ { code: "PROJECT_ID_AMBIGUOUS", defaultSeverity: "warning", summary: "Project has no usable id: its name yields no slug, or its declared id breaks the grammar" },
10465
+ { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" }
10466
+ ],
10467
+ check(ctx) {
10468
+ const identity = ctx.projectIdentity;
10469
+ if (!identity) return;
10470
+ const problem = (kind) => identity.problems.find((p) => p.kind === kind);
10471
+ if (problem("defaulted")) {
10472
+ ctx.addIssue(
10473
+ "notice",
10474
+ "PROJECT_ID_DEFAULTED",
10475
+ `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).`
10476
+ );
10477
+ }
10478
+ const ambiguous = problem("ambiguous");
10479
+ if (ambiguous) {
10480
+ ctx.addIssue(
10481
+ "warning",
10482
+ "PROJECT_ID_AMBIGUOUS",
10483
+ `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.`
10484
+ );
10485
+ }
10486
+ if (problem("changed")) {
10487
+ const now = identity.id === void 0 ? "no id at all" : `"${identity.id}"`;
10488
+ ctx.addIssue(
10489
+ "error",
10490
+ "PROJECT_ID_CHANGED",
10491
+ `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.`
10492
+ );
10493
+ }
10494
+ }
10495
+ };
10496
+ }
10497
+ });
10498
+
10326
10499
  // src/core/rules/integrity/roundtrip-serialization.ts
10327
10500
  var roundtripRule;
10328
10501
  var init_roundtrip_serialization = __esm({
@@ -10479,7 +10652,7 @@ var init_public_surface_binding = __esm({
10479
10652
  check(ctx) {
10480
10653
  for (const sub of ctx.subsystems) {
10481
10654
  const isDraftCtx = isDraftSubsystem(sub);
10482
- for (const pi of sub.publicInterfaces) {
10655
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
10483
10656
  if (!pi.component) {
10484
10657
  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);
10485
10658
  continue;
@@ -10580,13 +10753,15 @@ var init_public_surface_declared_type = __esm({
10580
10753
  check(ctx) {
10581
10754
  for (const sub of ctx.subsystems) {
10582
10755
  const isDraftCtx = isDraftSubsystem(sub);
10583
- for (const pi of sub.publicInterfaces) {
10756
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
10584
10757
  const backing = pi.component ? ctx.componentMap.get(pi.component) : void 0;
10585
10758
  if (!backing) continue;
10586
- if (!pubTypeMatches(pi.type, backing.componentType, backing.portalType)) {
10587
- 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);
10759
+ const piType = pi.type ?? "Custom";
10760
+ const piDetails = pi.details ?? "";
10761
+ if (!pubTypeMatches(piType, backing.componentType, backing.portalType)) {
10762
+ 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);
10588
10763
  }
10589
- if (pi.type === "Custom" && EVENT_VOCAB.test(pi.details)) {
10764
+ if (piType === "Custom" && EVENT_VOCAB.test(piDetails)) {
10590
10765
  const eventCapable = backing.componentType === "Observer" || backing.componentType === "Portal" && backing.portalType === "MessageBus";
10591
10766
  if (!eventCapable) {
10592
10767
  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);
@@ -10614,7 +10789,7 @@ var init_public_surface_bound_contract = __esm({
10614
10789
  check(ctx) {
10615
10790
  for (const sub of ctx.subsystems) {
10616
10791
  const isDraftCtx = isDraftSubsystem(sub);
10617
- for (const pi of sub.publicInterfaces) {
10792
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
10618
10793
  if (!pi.interface) continue;
10619
10794
  if (!pi.component || !ctx.componentMap.has(pi.component)) continue;
10620
10795
  const intf = ctx.interfaceMap.get(pi.interface);
@@ -10630,6 +10805,54 @@ var init_public_surface_bound_contract = __esm({
10630
10805
  }
10631
10806
  });
10632
10807
 
10808
+ // src/core/rules/integrity/export-tables.ts
10809
+ var exportTablesRule;
10810
+ var init_export_tables = __esm({
10811
+ "src/core/rules/integrity/export-tables.ts"() {
10812
+ "use strict";
10813
+ init_models();
10814
+ exportTablesRule = {
10815
+ name: "export-tables",
10816
+ 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.",
10817
+ codes: [
10818
+ { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
10819
+ { 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)" },
10820
+ { 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" },
10821
+ { code: "EXPORT_UNCONSUMABLE", defaultSeverity: "notice", summary: "An exported component no boundary caller may reach: neither a Portal nor an Observer" }
10822
+ ],
10823
+ check(ctx) {
10824
+ const tables = ctx.exportTables;
10825
+ if (!tables) return;
10826
+ for (const table of tables) {
10827
+ const sub = table.level === "subsystem" ? ctx.subsystems.find((s) => s.id === table.owner) : void 0;
10828
+ const draft = sub ? isDraftSubsystem(sub) : false;
10829
+ const where = table.level === "subsystem" ? `Subsystem "${table.owner}"` : `The project's L0 export table`;
10830
+ for (const problem of table.problems) {
10831
+ const name = problem.publicName ? `"${problem.publicName}"` : "an entry";
10832
+ switch (problem.kind) {
10833
+ case "duplicate":
10834
+ 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);
10835
+ break;
10836
+ case "named-cycle":
10837
+ ctx.addIssue("error", "EXPORT_CYCLE", `${where} ${problem.detail}. Export the item from the level that owns it.`, table.owner, draft);
10838
+ break;
10839
+ case "wildcard-cycle":
10840
+ ctx.addIssue("warning", "EXPORT_CYCLE", `${where}: ${problem.detail}. Re-export in one direction only.`, table.owner, draft);
10841
+ break;
10842
+ case "invalid":
10843
+ ctx.addIssue("notice", "EXPORT_INVALID", `${where} ${problem.detail}.`, table.owner, draft);
10844
+ break;
10845
+ case "unconsumable":
10846
+ ctx.addIssue("notice", "EXPORT_UNCONSUMABLE", `${where}: ${problem.detail}.`, table.owner, draft);
10847
+ break;
10848
+ }
10849
+ }
10850
+ }
10851
+ }
10852
+ };
10853
+ }
10854
+ });
10855
+
10633
10856
  // src/core/rules/integrity/lint-allows.ts
10634
10857
  var lintAllowsRule;
10635
10858
  var init_lint_allows = __esm({
@@ -12066,7 +12289,7 @@ var init_auth_source_wiring = __esm({
12066
12289
 
12067
12290
  // src/core/rules/doctrine/subsystem-boundary-dependencies.ts
12068
12291
  function restrictedConsumers(ctx, subsystemId, componentId) {
12069
- const entries = (ctx.subsystems.find((s) => s.id === subsystemId)?.publicInterfaces ?? []).filter((pi) => pi.component === componentId);
12292
+ const entries = (ctx.subsystems.find((s) => s.id === subsystemId)?.publicInterfaces ?? []).filter((pi) => isOwnComponentEntry(pi) && pi.component === componentId);
12070
12293
  if (entries.length === 0 || entries.some((pi) => pi.consumers === void 0)) return null;
12071
12294
  return [...new Set(entries.flatMap((pi) => pi.consumers ?? []))];
12072
12295
  }
@@ -12246,6 +12469,27 @@ var init_logic_dependency_class = __esm({
12246
12469
  });
12247
12470
 
12248
12471
  // src/core/rules/doctrine/data-block-dependencies.ts
12472
+ function indexReaches(ctx, from, target, seen = /* @__PURE__ */ new Set()) {
12473
+ for (const dep of from.dependsOn) {
12474
+ if (dep === target) return true;
12475
+ const next = ctx.componentMap.get(dep);
12476
+ if (!next || next.componentType !== "Index" || seen.has(dep)) continue;
12477
+ seen.add(dep);
12478
+ if (indexReaches(ctx, next, target, seen)) return true;
12479
+ }
12480
+ return false;
12481
+ }
12482
+ function derivedIndexBreach(ctx, from, to) {
12483
+ const ownership = ctx.ownershipIndex();
12484
+ const owner = ownership.ownerOf(from.id);
12485
+ if (!owner || owner !== ownership.ownerOf(to.id)) {
12486
+ return "An Index may project another Index only when one Repository owns both";
12487
+ }
12488
+ if (indexReaches(ctx, to, from.id)) {
12489
+ return `"${to.id}" depends back on "${from.id}" through Index edges \u2014 derived Indexes never form a cycle`;
12490
+ }
12491
+ return void 0;
12492
+ }
12249
12493
  var dataBlockDepsRule;
12250
12494
  var init_data_block_dependencies = __esm({
12251
12495
  "src/core/rules/doctrine/data-block-dependencies.ts"() {
@@ -12253,12 +12497,12 @@ var init_data_block_dependencies = __esm({
12253
12497
  init_stereotype_terms();
12254
12498
  dataBlockDepsRule = {
12255
12499
  name: "data-block-dependencies",
12256
- 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.",
12500
+ 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.",
12257
12501
  codes: [
12258
12502
  { code: "ARCHITECTURE_VIOLATION_STORE_DEP", defaultSeverity: "error", summary: "Store depending on anything but another Store, a backend Adapter or pure logic" },
12259
12503
  { 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)" },
12260
12504
  { code: "ARCHITECTURE_VIOLATION_ADAPTER_DEP", defaultSeverity: "error", summary: "Adapter depending on a Store or on an Orchestrator that is not pure" },
12261
- { code: "ARCHITECTURE_VIOLATION_INDEX_DEP", defaultSeverity: "error", summary: "Index depending on anything but its Store, an Adapter or pure logic" },
12505
+ { 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)" },
12262
12506
  { code: "ARCHITECTURE_VIOLATION_QUERY_DEP", defaultSeverity: "error", summary: "Query depending on anything but its Store, a backend Adapter or pure logic" }
12263
12507
  ],
12264
12508
  check(ctx) {
@@ -12295,11 +12539,13 @@ var init_data_block_dependencies = __esm({
12295
12539
  edge.draftContext
12296
12540
  );
12297
12541
  }
12298
- if (comp.componentType === "Index" && ownStoreOnly) {
12542
+ const indexBreach = comp.componentType === "Index" && depComp.componentType === "Index" ? derivedIndexBreach(ctx, comp, depComp) : void 0;
12543
+ const derivedIndexOk = comp.componentType === "Index" && depComp.componentType === "Index" && !indexBreach;
12544
+ if (comp.componentType === "Index" && ownStoreOnly && !derivedIndexOk) {
12299
12545
  ctx.addIssue(
12300
12546
  "error",
12301
12547
  "ARCHITECTURE_VIOLATION_INDEX_DEP",
12302
- `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.`,
12548
+ `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}.` : ""}`,
12303
12549
  comp.id,
12304
12550
  edge.draftContext
12305
12551
  );
@@ -17131,6 +17377,7 @@ var init_repository = __esm({
17131
17377
  init_hierarchy_integrity();
17132
17378
  init_reserved_id_segments();
17133
17379
  init_namespace_shadowing();
17380
+ init_project_identity();
17134
17381
  init_roundtrip_serialization();
17135
17382
  init_type_declarations();
17136
17383
  init_field_type_references();
@@ -17139,6 +17386,7 @@ var init_repository = __esm({
17139
17386
  init_public_surface_consumers();
17140
17387
  init_public_surface_declared_type();
17141
17388
  init_public_surface_bound_contract();
17389
+ init_export_tables();
17142
17390
  init_lint_allows();
17143
17391
  init_contract_symmetry();
17144
17392
  init_narrative_target_references();
@@ -17232,6 +17480,9 @@ var init_repository = __esm({
17232
17480
  // that would anchor a bare reference to the root instead.
17233
17481
  reservedIdSegmentsRule,
17234
17482
  namespaceShadowingRule,
17483
+ // The project's own id, beside the spec ids: declared, well-formed, and the
17484
+ // one its lock approved.
17485
+ projectIdentityRule,
17235
17486
  roundtripRule,
17236
17487
  // The type vocabulary in three questions: what a type declares about
17237
17488
  // itself, then the identifiers its fields name, then the ones its
@@ -17323,6 +17574,8 @@ var init_repository = __esm({
17323
17574
  publicSurfaceConsumersRule,
17324
17575
  publicSurfaceDeclaredTypeRule,
17325
17576
  publicSurfaceBoundContractRule,
17577
+ // The export tables the resolver settled: what the published names bind.
17578
+ exportTablesRule,
17326
17579
  cyclesRule,
17327
17580
  // Semantic-edge family: dispatch/lifecycle validity BEFORE reachability so a
17328
17581
  // reader sees the broken edge finding next to the unused-detection fallout
@@ -18467,7 +18720,7 @@ function buildRuleContext(opts) {
18467
18720
  for (const sub of subsystems) {
18468
18721
  publicSet.set(
18469
18722
  sub.id,
18470
- new Set(sub.publicInterfaces.map((pi) => pi.component).filter((c) => !!c))
18723
+ new Set(sub.publicInterfaces.filter(isOwnComponentEntry).map((pi) => pi.component).filter((c) => !!c))
18471
18724
  );
18472
18725
  }
18473
18726
  const surfaceSnapshots = opts.surfaceSnapshots ?? [];
@@ -18571,7 +18824,7 @@ function buildRuleContext(opts) {
18571
18824
  const candidates = [];
18572
18825
  for (const snapshot of pool) {
18573
18826
  if (provider !== void 0 && !isProvidedBy(snapshot, provider)) continue;
18574
- const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
18827
+ 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));
18575
18828
  if (entry) candidates.push({ snapshot, entry });
18576
18829
  }
18577
18830
  if (candidates.length === 0) continue;
@@ -18810,6 +19063,8 @@ function buildRuleContext(opts) {
18810
19063
  ext: { profiles: extensions.profiles, languages: extensions.languages, patterns: extensions.patterns, guarantees: extensions.guarantees, assertions: extensions.assertions, packSelections: opts.packSelections ?? [], selectionFailures: extensions.selectionFailures ?? [] },
18811
19064
  variants: opts.variants ?? [],
18812
19065
  surfaceSnapshots,
19066
+ ...opts.projectIdentity ? { projectIdentity: opts.projectIdentity } : {},
19067
+ ...opts.exportTables ? { exportTables: opts.exportTables } : {},
18813
19068
  mountSurfaceSnapshots,
18814
19069
  codeModel: opts.codeModel ?? emptyCodeModel(),
18815
19070
  roundTripIssues: opts.roundTripIssues,
@@ -19046,6 +19301,17 @@ function projectPackSelections() {
19046
19301
  return [];
19047
19302
  }
19048
19303
  }
19304
+ function boundProjectIdentity() {
19305
+ let config;
19306
+ try {
19307
+ config = loadProjectConfig();
19308
+ } catch {
19309
+ return void 0;
19310
+ }
19311
+ if (!config) return void 0;
19312
+ const lockedProjectId = readLockRecord()?.projectId;
19313
+ return projectIdentity(config, lockedProjectId);
19314
+ }
19049
19315
  function findTestsReferencing2(methods, projectRoot2, testRoots = []) {
19050
19316
  return findTestsReferencing(methods, projectRoot2, testRoots);
19051
19317
  }
@@ -19262,8 +19528,17 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
19262
19528
  scopeSubsystem,
19263
19529
  extensions,
19264
19530
  variants: loadProjectVariants(),
19531
+ // Every subsystem's resolved export table, then the project's: the
19532
+ // export-tables rule judges the problems the resolver met.
19533
+ exportTables: [
19534
+ ...subsystems.map((s) => resolveSubsystemExports(s.id)),
19535
+ resolveProjectExports()
19536
+ ],
19265
19537
  // By-name selections only: a legacy path ref pins nothing to check.
19266
19538
  packSelections: projectPackSelections(),
19539
+ // The run that IS the parent's verdict on a chained child judges the
19540
+ // child's specs, not the parent's identity, so it carries none.
19541
+ projectIdentity: crossTree === "off" ? void 0 : boundProjectIdentity(),
19267
19542
  surfaceSnapshots,
19268
19543
  mountSurfaceSnapshots,
19269
19544
  codeModel,
@@ -19429,6 +19704,7 @@ var init_validation = __esm({
19429
19704
  init_rules();
19430
19705
  init_repository();
19431
19706
  init_candidate();
19707
+ init_project();
19432
19708
  init_gate_identity();
19433
19709
  init_types();
19434
19710
  init_source_analysis();
@@ -19564,7 +19840,7 @@ function loadSpecGraph() {
19564
19840
  const implementations = loadImplementationSpecs();
19565
19841
  const publicComponents = /* @__PURE__ */ new Set();
19566
19842
  for (const sub of subsystems) {
19567
- for (const pi of sub.publicInterfaces) {
19843
+ for (const pi of sub.publicInterfaces.filter(isOwnComponentEntry)) {
19568
19844
  if (pi.component) publicComponents.add(pi.component);
19569
19845
  }
19570
19846
  }
@@ -19985,6 +20261,358 @@ var init_diagram = __esm({
19985
20261
  }
19986
20262
  });
19987
20263
 
20264
+ // src/core/exports.ts
20265
+ function localName2(id) {
20266
+ return id.split("::").pop() ?? id;
20267
+ }
20268
+ function ownedBy(subsystemId, declaringSubsystem) {
20269
+ return declaringSubsystem !== void 0 && (declaringSubsystem === subsystemId || declaringSubsystem.startsWith(`${subsystemId}::`));
20270
+ }
20271
+ function worldOf(subsystems, components, interfaces, types) {
20272
+ return {
20273
+ subsystems: new Map(subsystems.map((s) => [s.id, s])),
20274
+ components: new Map(components.map((c) => [c.id, c])),
20275
+ interfaces: new Map(interfaces.map((i) => [i.id, i])),
20276
+ types: types.reduce((m, t) => m.set(t.id, [...m.get(t.id) ?? [], t]), /* @__PURE__ */ new Map())
20277
+ };
20278
+ }
20279
+ function lookup(map, ref, namespace) {
20280
+ if (!ref) return void 0;
20281
+ return map.get(ref) ?? (namespace ? map.get(`${namespace}::${ref}`) : void 0);
20282
+ }
20283
+ function lookupType(world, ref, subsystemId) {
20284
+ const candidates = lookup(world.types, ref, subsystemId);
20285
+ return candidates?.find((t) => ownedBy(subsystemId, t.subsystem)) ?? candidates?.[0];
20286
+ }
20287
+ function checkPublicName(name, owner, problems) {
20288
+ if (!PUBLIC_NAME_RE.test(name)) {
20289
+ problems.push({
20290
+ kind: "invalid",
20291
+ owner,
20292
+ publicName: name,
20293
+ detail: `public name "${name}" breaks [a-z0-9-_]+ \u2014 rename it with \`as\``
20294
+ });
20295
+ }
20296
+ }
20297
+ function checkConsumable(entry, owner, problems) {
20298
+ if (entry.kind === "component" && entry.componentType && !CONSUMABLE.has(entry.componentType)) {
20299
+ problems.push({
20300
+ kind: "unconsumable",
20301
+ owner,
20302
+ publicName: entry.publicName,
20303
+ targets: [entry.component],
20304
+ 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`
20305
+ });
20306
+ }
20307
+ }
20308
+ function findInSource(world, source, table, request) {
20309
+ if (request.typeDef) {
20310
+ const type = lookupType(world, request.typeDef, source.id);
20311
+ if (!type) return {};
20312
+ const item2 = { publicName: localName2(type.id), kind: "type", source: type.subsystem ?? source.id, typeDef: type.id, via: [] };
20313
+ return { item: item2, found: table.entries.find((e) => e.kind === "type" && e.typeDef === type.id && e.source === item2.source) };
20314
+ }
20315
+ const intf = lookup(world.interfaces, request.interface, source.id);
20316
+ const comp = lookup(world.components, request.component ?? intf?.component, source.id);
20317
+ if (!comp) return {};
20318
+ const narrowed = intf?.id ?? request.interface;
20319
+ const item = {
20320
+ publicName: localName2(narrowed ?? comp.id),
20321
+ kind: "component",
20322
+ source: comp.subsystem,
20323
+ component: comp.id,
20324
+ ...narrowed ? { interface: narrowed } : {},
20325
+ componentType: comp.componentType,
20326
+ via: []
20327
+ };
20328
+ const exported = table.entries.filter((e) => e.kind === "component" && e.component === comp.id);
20329
+ 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];
20330
+ return { item, found: found && narrowed ? { ...found, interface: narrowed } : found };
20331
+ }
20332
+ function bindNamed(world, owner, source, sourceTable, request, publicName, inCycle, problems) {
20333
+ const { found, item } = findInSource(world, source, sourceTable, request);
20334
+ const what = request.typeDef ? `type "${request.typeDef}"` : `component "${request.component}"${request.interface ? ` (interface "${request.interface}")` : ""}`;
20335
+ if (!item) {
20336
+ problems.push({ kind: "invalid", owner, detail: `re-exports ${what} from "${source.id}", which has no such item` });
20337
+ return void 0;
20338
+ }
20339
+ const name = publicName(item);
20340
+ if (found) return { ...found, publicName: name, via: [source.id, ...found.via] };
20341
+ if (inCycle) {
20342
+ 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` });
20343
+ return void 0;
20344
+ }
20345
+ const declaring = item.source;
20346
+ if (!ownedBy(source.id, declaring)) {
20347
+ problems.push({ kind: "invalid", owner, publicName: name, detail: `re-exports ${what} from "${source.id}", which neither exports nor owns it` });
20348
+ return void 0;
20349
+ }
20350
+ problems.push({ kind: "invalid", owner, publicName: name, detail: `re-exports ${what} from "${source.id}", which does not export it \u2014 publish it there first` });
20351
+ return { ...item, publicName: name, via: [source.id] };
20352
+ }
20353
+ function bindOwn(world, sub, pi, problems) {
20354
+ if (pi.typeDef) {
20355
+ const type = lookupType(world, pi.typeDef, sub.id);
20356
+ if (!type || !ownedBy(sub.id, type.subsystem)) {
20357
+ 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"}` });
20358
+ return void 0;
20359
+ }
20360
+ const name2 = pi.as ?? localName2(type.id);
20361
+ checkPublicName(name2, sub.id, problems);
20362
+ return { publicName: name2, kind: "type", source: type.subsystem ?? sub.id, typeDef: type.id, via: [] };
20363
+ }
20364
+ const comp = lookup(world.components, pi.component, sub.id);
20365
+ if (!comp || !ownedBy(sub.id, comp.subsystem)) return void 0;
20366
+ const intf = lookup(world.interfaces, pi.interface, sub.id);
20367
+ const narrowed = intf?.id ?? pi.interface;
20368
+ const name = pi.as ?? localName2(narrowed ?? comp.id);
20369
+ checkPublicName(name, sub.id, problems);
20370
+ return {
20371
+ publicName: name,
20372
+ kind: "component",
20373
+ source: sub.id,
20374
+ component: comp.id,
20375
+ ...narrowed ? { interface: narrowed } : {},
20376
+ componentType: comp.componentType,
20377
+ ...pi.type ? { type: pi.type } : {},
20378
+ ...pi.details !== void 0 ? { details: pi.details } : {},
20379
+ via: []
20380
+ };
20381
+ }
20382
+ function subsystemCandidates(world, sub, tables, group, problems) {
20383
+ const out = [];
20384
+ sub.publicInterfaces.forEach((pi, index) => {
20385
+ if (pi.from === void 0) {
20386
+ const own = bindOwn(world, sub, pi, problems);
20387
+ if (own) out.push({ entry: own, explicit: true, from: index });
20388
+ return;
20389
+ }
20390
+ const source = world.subsystems.get(pi.from);
20391
+ if (!source || source.id === sub.id) {
20392
+ 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` });
20393
+ return;
20394
+ }
20395
+ const sourceTable = tables.get(source.id) ?? emptyTable(source.id, "subsystem");
20396
+ if (pi.component !== void 0 || pi.typeDef !== void 0 || pi.interface !== void 0) {
20397
+ const bound2 = bindNamed(
20398
+ world,
20399
+ sub.id,
20400
+ source,
20401
+ sourceTable,
20402
+ { component: pi.component, interface: pi.interface, typeDef: pi.typeDef },
20403
+ (item) => pi.as ?? localName2(pi.interface ?? item.typeDef ?? item.component ?? ""),
20404
+ group.has(source.id),
20405
+ problems
20406
+ );
20407
+ if (!bound2) return;
20408
+ checkPublicName(bound2.publicName, sub.id, problems);
20409
+ const entry = { ...bound2, ...pi.type ? { type: pi.type } : {}, ...pi.details !== void 0 ? { details: pi.details } : {} };
20410
+ checkConsumable(entry, sub.id, problems);
20411
+ out.push({ entry, explicit: true, from: index });
20412
+ return;
20413
+ }
20414
+ for (const e of sourceTable.entries) {
20415
+ const entry = { ...e, via: [source.id, ...e.via] };
20416
+ checkConsumable(entry, sub.id, problems);
20417
+ out.push({ entry, explicit: false, from: index });
20418
+ }
20419
+ });
20420
+ return out;
20421
+ }
20422
+ function emptyTable(owner, level) {
20423
+ return { owner, level, entries: [], problems: [] };
20424
+ }
20425
+ function settle(owner, level, candidates, problems) {
20426
+ const byName = /* @__PURE__ */ new Map();
20427
+ for (const c of candidates) {
20428
+ const list2 = byName.get(c.entry.publicName) ?? [];
20429
+ list2.push(c);
20430
+ byName.set(c.entry.publicName, list2);
20431
+ }
20432
+ const entries = [];
20433
+ for (const [name, list2] of byName) {
20434
+ const explicit = list2.filter((c) => c.explicit);
20435
+ const pool = explicit.length ? explicit : list2;
20436
+ const targets = [...new Set(pool.map((c) => exportTargetKey(c.entry)))];
20437
+ if (targets.length === 1) {
20438
+ entries.push(pool[0].entry);
20439
+ continue;
20440
+ }
20441
+ problems.push({
20442
+ kind: "duplicate",
20443
+ owner,
20444
+ publicName: name,
20445
+ targets,
20446
+ 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`
20447
+ });
20448
+ }
20449
+ return { owner, level, entries, problems };
20450
+ }
20451
+ function groupsInDependencyOrder(world) {
20452
+ const edges = /* @__PURE__ */ new Map();
20453
+ for (const sub of world.subsystems.values()) {
20454
+ edges.set(sub.id, sub.publicInterfaces.map((pi) => pi.from).filter((f) => f !== void 0 && f !== sub.id && world.subsystems.has(f)));
20455
+ }
20456
+ const index = /* @__PURE__ */ new Map();
20457
+ const low = /* @__PURE__ */ new Map();
20458
+ const stack = [];
20459
+ const onStack = /* @__PURE__ */ new Set();
20460
+ const groups = [];
20461
+ let next = 0;
20462
+ const visit = (v) => {
20463
+ index.set(v, next);
20464
+ low.set(v, next);
20465
+ next++;
20466
+ stack.push(v);
20467
+ onStack.add(v);
20468
+ for (const w of edges.get(v) ?? []) {
20469
+ if (!index.has(w)) {
20470
+ visit(w);
20471
+ low.set(v, Math.min(low.get(v), low.get(w)));
20472
+ } else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
20473
+ }
20474
+ if (low.get(v) === index.get(v)) {
20475
+ const group = [];
20476
+ let w;
20477
+ do {
20478
+ w = stack.pop();
20479
+ onStack.delete(w);
20480
+ group.push(w);
20481
+ } while (w !== v);
20482
+ groups.push(group.sort());
20483
+ }
20484
+ };
20485
+ for (const id of [...world.subsystems.keys()].sort()) if (!index.has(id)) visit(id);
20486
+ return groups;
20487
+ }
20488
+ function fingerprint(table) {
20489
+ return table.entries.map((e) => `${e.publicName}=${exportTargetKey(e)}`).sort().join("|");
20490
+ }
20491
+ function resolveSubsystemTables(subsystems, components, interfaces, types) {
20492
+ const world = worldOf(subsystems, components, interfaces, types);
20493
+ const tables = /* @__PURE__ */ new Map();
20494
+ for (const group of groupsInDependencyOrder(world)) {
20495
+ const members = new Set(group);
20496
+ const cyclic = group.length > 1;
20497
+ for (let pass = 0, changed = true; changed && pass <= (cyclic ? group.length * 4 + 4 : 0); pass++) {
20498
+ changed = false;
20499
+ for (const id of group) {
20500
+ const problems = [];
20501
+ const table = settle(id, "subsystem", subsystemCandidates(world, world.subsystems.get(id), tables, cyclic ? members : /* @__PURE__ */ new Set(), problems), problems);
20502
+ const before = tables.get(id);
20503
+ if (!before || fingerprint(before) !== fingerprint(table)) changed = true;
20504
+ tables.set(id, table);
20505
+ }
20506
+ }
20507
+ 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)));
20508
+ if (wildcardLoop) {
20509
+ tables.get(group[0]).problems.push({
20510
+ kind: "wildcard-cycle",
20511
+ owner: group[0],
20512
+ targets: group,
20513
+ 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`
20514
+ });
20515
+ }
20516
+ }
20517
+ return tables;
20518
+ }
20519
+ function sourceOf(world, e) {
20520
+ if (e.from ?? e.subsystem) return e.from ?? e.subsystem;
20521
+ if (e.component) return world.components.get(e.component)?.subsystem;
20522
+ if (e.interface) {
20523
+ const intf = world.interfaces.get(e.interface);
20524
+ return intf ? world.components.get(intf.component)?.subsystem : void 0;
20525
+ }
20526
+ if (e.typeDef) return world.types.get(e.typeDef)?.[0]?.subsystem;
20527
+ return void 0;
20528
+ }
20529
+ function withEntryMetadata(entry, e) {
20530
+ return {
20531
+ ...entry,
20532
+ audience: e.audience ?? "instance",
20533
+ ...e.name ? { name: e.name } : {},
20534
+ ...e.type ? { type: e.type } : {},
20535
+ ...e.details !== void 0 ? { details: e.details } : {},
20536
+ ...e.authPolicy ? { authPolicy: e.authPolicy } : {},
20537
+ ...e.version ? { version: e.version } : {},
20538
+ ...e.stability ? { stability: e.stability } : {}
20539
+ };
20540
+ }
20541
+ function resolveProjectTable(system, subsystems, components, interfaces, types, subsystemTables) {
20542
+ const world = worldOf(subsystems, components, interfaces, types);
20543
+ const owner = system.name;
20544
+ const problems = [];
20545
+ const candidates = [];
20546
+ (system.publicInterfaces ?? []).forEach((e, index) => {
20547
+ const sourceId = sourceOf(world, e);
20548
+ const source = sourceId ? world.subsystems.get(sourceId) : void 0;
20549
+ if (!source) {
20550
+ const label = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
20551
+ 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` });
20552
+ return;
20553
+ }
20554
+ const sourceTable = subsystemTables.get(source.id) ?? emptyTable(source.id, "subsystem");
20555
+ if (e.component !== void 0 || e.typeDef !== void 0 || e.interface !== void 0) {
20556
+ const bound2 = bindNamed(
20557
+ world,
20558
+ owner,
20559
+ source,
20560
+ sourceTable,
20561
+ { component: e.component, interface: e.interface, typeDef: e.typeDef },
20562
+ () => e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef,
20563
+ false,
20564
+ problems
20565
+ );
20566
+ if (!bound2) return;
20567
+ checkPublicName(bound2.publicName, owner, problems);
20568
+ const entry = withEntryMetadata(bound2, e);
20569
+ checkConsumable(entry, owner, problems);
20570
+ candidates.push({ entry, explicit: true, from: index });
20571
+ return;
20572
+ }
20573
+ for (const found of sourceTable.entries) {
20574
+ const entry = withEntryMetadata({ ...found, via: [source.id, ...found.via] }, e);
20575
+ checkConsumable(entry, owner, problems);
20576
+ candidates.push({ entry, explicit: false, from: index });
20577
+ }
20578
+ });
20579
+ return settle(owner, "project", candidates, problems);
20580
+ }
20581
+ function scanTables(subsystems) {
20582
+ const memo = subsystemMemo.get(subsystems);
20583
+ if (memo) return memo;
20584
+ const tables = resolveSubsystemTables(subsystems, loadComponentSpecs(), loadInterfaceSpecs(), loadTypeSpecs());
20585
+ subsystemMemo.set(subsystems, tables);
20586
+ return tables;
20587
+ }
20588
+ function resolveSubsystemExportTable(subsystemId) {
20589
+ const tables = scanTables(loadSubsystemSpecs());
20590
+ return tables.get(subsystemId) ?? emptyTable(subsystemId, "subsystem");
20591
+ }
20592
+ function resolveProjectExportTable() {
20593
+ const system = loadSystemSpec();
20594
+ if (!system) return emptyTable("", "project");
20595
+ const subsystems = loadSubsystemSpecs();
20596
+ const key = JSON.stringify([system.name, system.publicInterfaces ?? []]);
20597
+ const memo = projectMemo.get(subsystems);
20598
+ if (memo && memo.key === key) return memo.table;
20599
+ const subsystemTables = new Map(subsystems.map((sub) => [sub.id, resolveSubsystemExportTable(sub.id)]));
20600
+ const table = resolveProjectTable(system, subsystems, loadComponentSpecs(), loadInterfaceSpecs(), loadTypeSpecs(), subsystemTables);
20601
+ projectMemo.set(subsystems, { key, table });
20602
+ return table;
20603
+ }
20604
+ var CONSUMABLE, subsystemMemo, projectMemo;
20605
+ var init_exports2 = __esm({
20606
+ "src/core/exports.ts"() {
20607
+ "use strict";
20608
+ init_exports();
20609
+ init_specs2();
20610
+ CONSUMABLE = /* @__PURE__ */ new Set(["Portal", "Observer"]);
20611
+ subsystemMemo = /* @__PURE__ */ new WeakMap();
20612
+ projectMemo = /* @__PURE__ */ new WeakMap();
20613
+ }
20614
+ });
20615
+
19988
20616
  // src/core/specs.ts
19989
20617
  function emptyIndex() {
19990
20618
  return {
@@ -20232,13 +20860,18 @@ function mergeMountRealizations(subs) {
20232
20860
  function stripNamespaceFromSubsystem(spec, prefix) {
20233
20861
  const memberPrefix = spec.projectPath ? spec.id : prefix;
20234
20862
  const relMember = (id) => id.includes("::") ? relativizeId(id, memberPrefix) : id;
20863
+ const relPeer = (id) => id.includes("::") ? relativizeId(id, prefix) : id;
20235
20864
  return {
20236
20865
  ...spec,
20237
20866
  id: relativizeId(spec.id, prefix),
20238
20867
  publicInterfaces: spec.publicInterfaces.map((pi) => ({
20239
20868
  ...pi,
20240
- component: pi.component ? relMember(pi.component) : void 0,
20241
- interface: pi.interface ? relMember(pi.interface) : void 0,
20869
+ // A re-export names a peer's item, written relative to the namespace the
20870
+ // subsystem is declared in (the loader's inverse), not its members'.
20871
+ component: pi.component ? pi.from ? relPeer(pi.component) : relMember(pi.component) : void 0,
20872
+ interface: pi.interface ? pi.from ? relPeer(pi.interface) : relMember(pi.interface) : void 0,
20873
+ ...pi.typeDef ? { typeDef: pi.from ? relPeer(pi.typeDef) : relMember(pi.typeDef) } : {},
20874
+ ...pi.from ? { from: relPeer(pi.from) } : {},
20242
20875
  // consumers name SUBSYSTEMS — peers of this one, so they are relative to
20243
20876
  // the namespace the subsystem itself is declared in, never its members'.
20244
20877
  ...pi.consumers ? { consumers: pi.consumers.map((c) => c.includes("::") ? relativizeId(c, prefix) : c) } : {}
@@ -20513,6 +21146,14 @@ function identityKeyOf(field, item) {
20513
21146
  }
20514
21147
  function publicInterfaceKey(o) {
20515
21148
  const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
21149
+ const from = str(o.from);
21150
+ const typeDef = str(o.typeDef);
21151
+ if (from || typeDef) {
21152
+ const item = typeDef ? `type ${typeDef}` : str(o.component) ?? "*";
21153
+ const narrowed = str(o.interface) ? `.${String(o.interface)}` : "";
21154
+ const renamed = str(o.as) ? ` as ${String(o.as)}` : "";
21155
+ return `${from ? `${from}:` : ""}${item}${narrowed}${renamed}`;
21156
+ }
20516
21157
  const component = str(o.component);
20517
21158
  if (component) return str(o.interface) ? `${component}.${String(o.interface)}` : component;
20518
21159
  if (typeof o.type === "string" && typeof o.details === "string") return `${o.type} ${o.details}`;
@@ -20717,8 +21358,9 @@ function identityFieldsOf(field) {
20717
21358
  case "dispatch":
20718
21359
  return ["capability"];
20719
21360
  // Bound: component + interface; not yet bound: type + details.
21361
+ // Re-exports and type exports: from + item + interface + as.
20720
21362
  case "publicInterfaces":
20721
- return ["component", "interface", "type", "details"];
21363
+ return ["component", "interface", "type", "details", "from", "typeDef", "as"];
20722
21364
  case "lifecycle":
20723
21365
  return ["phase", "component", "method"];
20724
21366
  case "emits":
@@ -21057,6 +21699,12 @@ function loadTypeSpecs() {
21057
21699
  function loadTypeSpec(id) {
21058
21700
  return current().loadTypeSpec(id);
21059
21701
  }
21702
+ function resolveSubsystemExports(subsystemId) {
21703
+ return resolveSubsystemExportTable(subsystemId);
21704
+ }
21705
+ function resolveProjectExports() {
21706
+ return resolveProjectExportTable();
21707
+ }
21060
21708
  function saveTypeSpec(spec) {
21061
21709
  return current().saveTypeSpec(spec);
21062
21710
  }
@@ -21170,6 +21818,7 @@ var init_specs2 = __esm({
21170
21818
  init_models();
21171
21819
  init_narrative_labels();
21172
21820
  init_diagram();
21821
+ init_exports2();
21173
21822
  NO_ROOT_SUBSYSTEMS = /* @__PURE__ */ new Set();
21174
21823
  SIGNATURE_TTL_MS = 2e3;
21175
21824
  COMPONENT_AUTH_SOURCE = "component:";
@@ -21348,14 +21997,19 @@ var init_specs2 = __esm({
21348
21997
  return {
21349
21998
  ...sub,
21350
21999
  id: qualifiedSubId,
21351
- publicInterfaces: sub.publicInterfaces.map((p) => ({
21352
- ...p,
21353
- component: p.component ? qualifyId(p.component, componentPrefix, this.rootSubsystems) : void 0,
21354
- interface: p.interface ? qualifyId(p.interface, componentPrefix, this.rootSubsystems) : void 0,
21355
- // Each consumer is a subsystem reference: qualified into the namespace
21356
- // this subsystem is declared in, like a component's `subsystem` field.
21357
- ...p.consumers ? { consumers: p.consumers.map((c) => qualifySubsystemRef(c, namespacePrefix, this.rootSubsystems)) } : {}
21358
- })),
22000
+ publicInterfaces: sub.publicInterfaces.map((p) => {
22001
+ const itemPrefix = p.from ? namespacePrefix : componentPrefix;
22002
+ return {
22003
+ ...p,
22004
+ component: p.component ? qualifyId(p.component, itemPrefix, this.rootSubsystems) : void 0,
22005
+ interface: p.interface ? qualifyId(p.interface, itemPrefix, this.rootSubsystems) : void 0,
22006
+ ...p.typeDef ? { typeDef: qualifyId(p.typeDef, itemPrefix, this.rootSubsystems) } : {},
22007
+ ...p.from ? { from: qualifySubsystemRef(p.from, namespacePrefix, this.rootSubsystems) } : {},
22008
+ // Each consumer is a subsystem reference: qualified into the namespace
22009
+ // this subsystem is declared in, like a component's `subsystem` field.
22010
+ ...p.consumers ? { consumers: p.consumers.map((c) => qualifySubsystemRef(c, namespacePrefix, this.rootSubsystems)) } : {}
22011
+ };
22012
+ }),
21359
22013
  lifecycle: sub.lifecycle?.map((le) => ({
21360
22014
  ...le,
21361
22015
  component: qualifyId(le.component, componentPrefix, this.rootSubsystems)
@@ -22967,8 +23621,12 @@ var init_specs2 = __esm({
22967
23621
  if (Array.isArray(out.publicInterfaces)) {
22968
23622
  out.publicInterfaces = out.publicInterfaces.map((pi) => ({
22969
23623
  ...pi,
22970
- ...typeof pi?.component === "string" ? { component: qm(pi.component) } : {},
22971
- ...typeof pi?.interface === "string" ? { interface: qm(pi.interface) } : {},
23624
+ // A re-export names a peer's item: qualified into the declaring
23625
+ // namespace (q), not this subsystem's members' (qm).
23626
+ ...typeof pi?.component === "string" ? { component: (typeof pi?.from === "string" ? q2 : qm)(pi.component) } : {},
23627
+ ...typeof pi?.interface === "string" ? { interface: (typeof pi?.from === "string" ? q2 : qm)(pi.interface) } : {},
23628
+ ...typeof pi?.typeDef === "string" ? { typeDef: (typeof pi?.from === "string" ? q2 : qm)(pi.typeDef) } : {},
23629
+ ...typeof pi?.from === "string" ? { from: qualifySubsystemRef(pi.from, prefix, this.rootSubsystems) } : {},
22972
23630
  ...Array.isArray(pi?.consumers) ? { consumers: pi.consumers.map((c) => typeof c === "string" ? qualifySubsystemRef(c, prefix, this.rootSubsystems) : c) } : {}
22973
23631
  }));
22974
23632
  }
@@ -23662,9 +24320,11 @@ var init_domain_curator = __esm({
23662
24320
  });
23663
24321
 
23664
24322
  // src/core/provision.ts
23665
- function defaultProjectConfig(name, now) {
24323
+ function defaultProjectConfig(name, now, id) {
24324
+ const declared = id ?? effectiveProjectId({ name });
23666
24325
  return {
23667
24326
  schemaVersion: "1.0.0",
24327
+ ...declared !== null ? { id: declared } : {},
23668
24328
  name,
23669
24329
  projectType: "backend",
23670
24330
  targets: [{ type: "claude", outputDir: ".claude/agents", enabled: true }],
@@ -23706,7 +24366,7 @@ function provisionProject(name) {
23706
24366
  projectConfigRepository.create(config);
23707
24367
  saveSystemSpec(bootstrapSystemSpec(name, now));
23708
24368
  }
23709
- function ensureProjectInitialized(fallbackName) {
24369
+ function ensureProjectInitialized(fallbackName, id) {
23710
24370
  const now = (/* @__PURE__ */ new Date()).toISOString();
23711
24371
  const paths = aiPathsAt(getProjectRoot());
23712
24372
  const hasSystem = fs14.existsSync(paths.specsSystem());
@@ -23718,7 +24378,7 @@ function ensureProjectInitialized(fallbackName) {
23718
24378
  let wroteConfig = false;
23719
24379
  let wroteSystem = false;
23720
24380
  if (!projectConfigRepository.exists()) {
23721
- projectConfigRepository.create(defaultProjectConfig(name, now));
24381
+ projectConfigRepository.create(defaultProjectConfig(name, now, id));
23722
24382
  wroteConfig = true;
23723
24383
  }
23724
24384
  if (!hasSystem) {
@@ -23799,7 +24459,7 @@ function backfillChainedSubprojectConfigs(projectRoot2) {
23799
24459
  walkChainedSubprojects(projectRoot2, (childDir, subsystemId) => {
23800
24460
  if (childHasSpecsButNoConfig(childDir)) {
23801
24461
  runWithProjectRoot(childDir, () => {
23802
- ensureProjectInitialized(subsystemId);
24462
+ ensureProjectInitialized(subsystemId, localSegment(subsystemId));
23803
24463
  });
23804
24464
  backfilled.push(childDir);
23805
24465
  }
@@ -23815,7 +24475,7 @@ function createChainedSubsystem(subsystem, projectName) {
23815
24475
  const childDir = assertContainedProjectPath(getProjectRoot(), projectPath);
23816
24476
  runWithProjectRoot(childDir, () => {
23817
24477
  ensureDir(aiPathsAt(childDir).specsDir());
23818
- ensureProjectInitialized(projectName);
24478
+ ensureProjectInitialized(projectName, localSegment(subsystem.id));
23819
24479
  });
23820
24480
  invalidateSpecCache();
23821
24481
  }
@@ -23846,6 +24506,9 @@ function moveSubsystemProject(subsystemId, newProjectPath) {
23846
24506
  saveSubsystemSpec({ ...sub, projectPath: nextPath, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
23847
24507
  invalidateSpecCache();
23848
24508
  }
24509
+ function localSegment(subsystemId) {
24510
+ return subsystemId.split("::").pop();
24511
+ }
23849
24512
  function toPosixPath(p) {
23850
24513
  return p.replace(/\\/g, "/");
23851
24514
  }
@@ -23882,7 +24545,7 @@ function externalizeSubsystem(subsystemId, projectPath) {
23882
24545
  const childSystemName = foo.name || subsystemId;
23883
24546
  runWithProjectRoot(childDir, () => {
23884
24547
  const now = (/* @__PURE__ */ new Date()).toISOString();
23885
- projectConfigRepository.create(defaultProjectConfig(childSystemName, now));
24548
+ projectConfigRepository.create(defaultProjectConfig(childSystemName, now, subsystemId));
23886
24549
  ensureDir(path19.join(childDir, ".wai", "specs"));
23887
24550
  saveSystemSpec(bootstrapSystemSpec(childSystemName, now));
23888
24551
  });
@@ -24292,6 +24955,7 @@ var init_provision = __esm({
24292
24955
  init_fs();
24293
24956
  init_yaml();
24294
24957
  init_errors();
24958
+ init_project();
24295
24959
  init_identity_rename();
24296
24960
  init_models();
24297
24961
  METHOD_NAME = /^[a-z][a-zA-Z0-9]*$/;
@@ -30788,7 +31452,13 @@ function qualifiedComponentId(id) {
30788
31452
  return chained.length === 1 ? chained[0] : id;
30789
31453
  }
30790
31454
  function publicInterfacesReplacement(subsystem, stored, list2) {
30791
- const identity = (pi) => pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
31455
+ const identity = (pi) => {
31456
+ if (pi.from || pi.typeDef) {
31457
+ const item = pi.typeDef ? `type ${pi.typeDef}` : pi.component ?? "*";
31458
+ return `${pi.from ? `${pi.from}:` : ""}${item}${pi.interface ? `.${pi.interface}` : ""}${pi.as ? ` as ${pi.as}` : ""}`;
31459
+ }
31460
+ return pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
31461
+ };
30792
31462
  const restated = /* @__PURE__ */ new Map();
30793
31463
  for (const pi of list2) restated.set(identity(pi), (restated.get(identity(pi)) ?? 0) + 1);
30794
31464
  const twice = [...restated].filter(([, n]) => n > 1).map(([key]) => `"${key}"`);
@@ -30801,7 +31471,7 @@ function publicInterfacesReplacement(subsystem, stored, list2) {
30801
31471
  const key = identity(pi);
30802
31472
  seen.set(key, (seen.get(key) ?? 0) + 1);
30803
31473
  if (seen.get(key) > (restated.get(key) ?? 0)) {
30804
- deletes.push(pi.component ? { component: pi.component, interface: pi.interface, action: "delete" } : { type: pi.type, details: pi.details, action: "delete" });
31474
+ 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" });
30805
31475
  }
30806
31476
  }
30807
31477
  const restatedEntries = list2.map((pi) => {
@@ -31266,17 +31936,21 @@ function createMcpServer(options = {}) {
31266
31936
  }
31267
31937
  }
31268
31938
  );
31939
+ const publicInterfaceItemInput = import_zod11.z.object({
31940
+ 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"),
31941
+ 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"),
31942
+ 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`"),
31943
+ interface: import_zod11.z.string().optional().describe("Optional L3 interface id the entry narrows to"),
31944
+ 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)."),
31945
+ 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)"),
31946
+ typeDef: import_zod11.z.string().optional().describe("A type exported as is: owned by this subsystem, or re-exported from `from`"),
31947
+ 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)")
31948
+ }).strict();
31269
31949
  const subsystemInput = {
31270
31950
  id: import_zod11.z.string().describe("Lowercase identifier for the subsystem"),
31271
31951
  name: import_zod11.z.string().describe("Human-readable display name"),
31272
31952
  description: import_zod11.z.string().describe("Purpose and details of the subsystem"),
31273
- publicInterfaces: import_zod11.z.array(import_zod11.z.object({
31274
- type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
31275
- details: import_zod11.z.string(),
31276
- component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface (this subsystem's published surface)"),
31277
- interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component backing this entry"),
31278
- 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).")
31279
- }).strict()).optional().describe("Public entrypoints exposed by this subsystem, each bound to a realizing component"),
31953
+ 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"),
31280
31954
  projectPath: import_zod11.z.string().optional().describe("Relative path to external project root for subsystem chaining"),
31281
31955
  targetLanguage: import_zod11.z.string().optional().describe("Override of the system-level targetLanguage for this subsystem"),
31282
31956
  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)"),
@@ -31338,16 +32012,10 @@ function createMcpServer(options = {}) {
31338
32012
  server,
31339
32013
  "sdd_set_public_interfaces",
31340
32014
  {
31341
- 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.",
32015
+ 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.",
31342
32016
  inputSchema: {
31343
32017
  subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
31344
- publicInterfaces: import_zod11.z.array(import_zod11.z.object({
31345
- type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
31346
- details: import_zod11.z.string(),
31347
- component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface"),
31348
- interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component"),
31349
- 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).")
31350
- }).strict()).describe("The full replacement list of public interfaces for this subsystem")
32018
+ publicInterfaces: import_zod11.z.array(publicInterfaceItemInput).describe("The full replacement list of the subsystem's export table: own items and re-exports")
31351
32019
  },
31352
32020
  outputSchema: specChangeReportOutput
31353
32021
  },
@@ -31613,7 +32281,7 @@ ${renderChangeReport(report2)}`,
31613
32281
  }).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."),
31614
32282
  findings: import_zod11.z.array(import_zod11.z.object({
31615
32283
  code: import_zod11.z.string().describe("UPPER_SNAKE finding code, unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>)"),
31616
- severity: import_zod11.z.enum(["error", "warning"]).describe("Default severity, before project severity overrides and draft-context downgrades"),
32284
+ 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"),
31617
32285
  summary: import_zod11.z.string().describe("One line saying what the finding means")
31618
32286
  }).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."),
31619
32287
  ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim")
@@ -32057,9 +32725,9 @@ ${testsBlock}` : ""}`, deletion);
32057
32725
  {
32058
32726
  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.",
32059
32727
  inputSchema: {
32060
- 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)"),
32728
+ 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})"),
32061
32729
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
32062
- 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.`),
32730
+ 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.`),
32063
32731
  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.")
32064
32732
  },
32065
32733
  outputSchema: specChangeReportOutput
@@ -33331,8 +33999,10 @@ async function executeInit(projectName, projectConfig, guidePlan) {
33331
33999
  logger.blank();
33332
34000
  }
33333
34001
  function buildProjectConfig(name, targets, now, projectType) {
34002
+ const id = effectiveProjectId({ name });
33334
34003
  return {
33335
34004
  schemaVersion: "1.0.0",
34005
+ ...id !== null ? { id } : {},
33336
34006
  name,
33337
34007
  projectType,
33338
34008
  targets,
@@ -33534,7 +34204,7 @@ async function runGenerate(options = {}) {
33534
34204
  logger.blank();
33535
34205
  logger.info(`\u21B3 Chained subproject "${child.subsystemId}" \u2014 generating its layer in ${path36.relative(getProjectRoot(), child.dir) || "."}/`);
33536
34206
  await runWithProjectRoot(child.dir, async () => {
33537
- ensureProjectInitialized(child.subsystemId);
34207
+ ensureProjectInitialized(child.subsystemId, child.subsystemId);
33538
34208
  await runGenerate(options);
33539
34209
  });
33540
34210
  }
@@ -33640,6 +34310,7 @@ init_logger();
33640
34310
  init_defaults();
33641
34311
  var path37 = __toESM(require("path"));
33642
34312
  init_core();
34313
+ init_project();
33643
34314
  init_lock();
33644
34315
  init_fs();
33645
34316
  init_validation();
@@ -33716,6 +34387,8 @@ async function runLock(options = {}, gate) {
33716
34387
  specPathsInScope(options.subsystem).map((p) => path37.relative(root, p).split(path37.sep).join("/"))
33717
34388
  )
33718
34389
  } : void 0;
34390
+ const config = loadProjectConfig();
34391
+ const projectId = config ? effectiveProjectId(config) : null;
33719
34392
  const record2 = {
33720
34393
  stateId: computeGateStateId(),
33721
34394
  lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -33728,6 +34401,7 @@ async function runLock(options = {}, gate) {
33728
34401
  notices: gate ? gate.issues.filter((i) => i.severity === "notice").length : 0
33729
34402
  },
33730
34403
  status: "ready",
34404
+ ...projectId !== null ? { projectId } : {},
33731
34405
  specs: captureApprovedSpecs(root, scope),
33732
34406
  children: currentChildPins(loadSubsystemSpecs(), root)
33733
34407
  };
@@ -36106,6 +36780,7 @@ init_errors();
36106
36780
  var crypto11 = __toESM(require("crypto"));
36107
36781
  init_fs();
36108
36782
  init_defaults();
36783
+ init_project();
36109
36784
 
36110
36785
  // src/server/auth.ts
36111
36786
  var crypto9 = __toESM(require("crypto"));
@@ -36440,14 +37115,14 @@ function registrySetStatus(dataDir, id, status2) {
36440
37115
  }
36441
37116
  function registryRemapUnitReferences(dataDir, remap, removedScopeIds) {
36442
37117
  if (remap.length === 0 && removedScopeIds.length === 0) return;
36443
- const lookup = new Map(remap.map((r) => [r.oldId, r.newId]));
37118
+ const lookup2 = new Map(remap.map((r) => [r.oldId, r.newId]));
36444
37119
  const removed = new Set(removedScopeIds);
36445
37120
  const records = loadStore(dataDir);
36446
37121
  let changed = false;
36447
37122
  const rewritten = records.map((record2) => {
36448
37123
  const next = { ...record2 };
36449
37124
  if (next.unitId) {
36450
- const moved = lookup.get(next.unitId);
37125
+ const moved = lookup2.get(next.unitId);
36451
37126
  if (moved) {
36452
37127
  next.unitId = moved;
36453
37128
  changed = true;
@@ -36457,7 +37132,7 @@ function registryRemapUnitReferences(dataDir, remap, removedScopeIds) {
36457
37132
  }
36458
37133
  }
36459
37134
  if (next.roleBindings && next.roleBindings.length > 0) {
36460
- const bindings = next.roleBindings.filter((b) => !(b.scopeId && removed.has(b.scopeId))).map((b) => b.scopeId && lookup.has(b.scopeId) ? { ...b, scopeId: lookup.get(b.scopeId) } : b);
37135
+ const bindings = next.roleBindings.filter((b) => !(b.scopeId && removed.has(b.scopeId))).map((b) => b.scopeId && lookup2.has(b.scopeId) ? { ...b, scopeId: lookup2.get(b.scopeId) } : b);
36461
37136
  if (bindings.length !== next.roleBindings.length || bindings.some((b, i) => b.scopeId !== next.roleBindings?.[i]?.scopeId)) {
36462
37137
  next.roleBindings = bindings;
36463
37138
  changed = true;
@@ -37337,11 +38012,11 @@ function registryRemove(dataDir, assignmentId) {
37337
38012
  }
37338
38013
  function registryRemapScope(dataDir, remap) {
37339
38014
  if (remap.length === 0) return;
37340
- const lookup = new Map(remap.map((r) => [r.oldId, r.newId]));
38015
+ const lookup2 = new Map(remap.map((r) => [r.oldId, r.newId]));
37341
38016
  const assignments = load4(dataDir);
37342
38017
  let changed = false;
37343
38018
  const rewritten = assignments.map((a) => {
37344
- const next = a.scopeId ? lookup.get(a.scopeId) : void 0;
38019
+ const next = a.scopeId ? lookup2.get(a.scopeId) : void 0;
37345
38020
  if (!next) return a;
37346
38021
  changed = true;
37347
38022
  return { ...a, scopeId: next };
@@ -38109,12 +38784,19 @@ function systemBody(system, mermaid, diagramUrl) {
38109
38784
  if (mermaid) parts.push("```mermaid", mermaid.trim(), "```", "");
38110
38785
  return parts.join("\n");
38111
38786
  }
38787
+ function publicInterfaceLine(p) {
38788
+ const item = p.typeDef ? `type \`${p.typeDef}\`` : p.component ? `\`${p.component}\`` : "everything";
38789
+ const name = p.as ? ` as \`${p.as}\`` : "";
38790
+ if (p.from) return `- re-exports ${item}${name} from \`${p.from}\`${p.details ? ` \u2014 ${p.details}` : ""}`;
38791
+ if (p.typeDef) return `- ${item}${name}`;
38792
+ return `- **${p.type}** via \`${p.component ?? "?"}\`${name} \u2014 ${p.details}`;
38793
+ }
38112
38794
  function subsystemBody(sub) {
38113
38795
  const parts = [`# ${sub.name || sub.id}`, ""];
38114
38796
  if (sub.description) parts.push(sub.description, "");
38115
38797
  if (sub.publicInterfaces?.length) {
38116
38798
  parts.push("## Public surface");
38117
- for (const p of sub.publicInterfaces) parts.push(`- **${p.type}** via \`${p.component ?? "?"}\` \u2014 ${p.details}`);
38799
+ for (const p of sub.publicInterfaces) parts.push(publicInterfaceLine(p));
38118
38800
  parts.push("");
38119
38801
  }
38120
38802
  return parts.join("\n");
@@ -38510,6 +39192,8 @@ function executeApprovedLock(cfg, projectId, approver, subproject) {
38510
39192
  const stateId = computeGateStateId();
38511
39193
  const specs = captureApprovedSpecs();
38512
39194
  const children = currentChildPins(loadSubsystemSpecs());
39195
+ const config = loadProjectConfig();
39196
+ const projectId2 = config ? effectiveProjectId(config) : null;
38513
39197
  const record2 = {
38514
39198
  stateId,
38515
39199
  lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -38522,11 +39206,12 @@ function executeApprovedLock(cfg, projectId, approver, subproject) {
38522
39206
  notices: result.issues.filter((i) => i.severity === "notice").length
38523
39207
  },
38524
39208
  status: "ready",
39209
+ ...projectId2 !== null ? { projectId: projectId2 } : {},
38525
39210
  specs,
38526
39211
  children
38527
39212
  };
38528
39213
  writeLockRecord(record2);
38529
- const publish2 = publish(`wairon lock: ${projectId} @ ${stateId}`);
39214
+ const publish2 = publish(`wairon lock: ${projectId2} @ ${stateId}`);
38530
39215
  if (!publish2.published) return record2;
38531
39216
  const published = {
38532
39217
  ...record2,
@@ -41036,8 +41721,6 @@ function applyEndpointOverrides(config) {
41036
41721
 
41037
41722
  // src/server/landscape.ts
41038
41723
  init_fs();
41039
- init_yaml();
41040
- init_paths();
41041
41724
 
41042
41725
  // src/server/relations.ts
41043
41726
  var fs45 = __toESM(require("fs"));
@@ -41390,6 +42073,7 @@ function openApiIndexDocument(specs) {
41390
42073
  }
41391
42074
 
41392
42075
  // src/server/landscape.ts
42076
+ init_models();
41393
42077
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
41394
42078
  var PROJECT_READ_CAPABILITY3 = "project:read";
41395
42079
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
@@ -41460,66 +42144,36 @@ function tryAppendAudit4(cfg, event) {
41460
42144
  );
41461
42145
  }
41462
42146
  }
41463
- function firstString(...vals) {
41464
- for (const v of vals) {
41465
- if (typeof v === "string" && v.trim().length > 0) return v;
41466
- }
41467
- return void 0;
42147
+ function endpointPath(m) {
42148
+ return m.endpoint?.transport === "HTTP" ? m.endpoint.path : void 0;
41468
42149
  }
41469
- function toNameList(v) {
41470
- if (!Array.isArray(v)) return [];
41471
- const out = [];
41472
- for (const item of v) {
41473
- if (typeof item === "string") {
41474
- if (item.trim().length > 0) out.push(item);
41475
- } else if (item && typeof item === "object") {
41476
- const rec = item;
41477
- const name = firstString(rec.name, rec.id);
41478
- if (name) out.push(name);
42150
+ function referencedTypeNames(methods) {
42151
+ const names = /* @__PURE__ */ new Set();
42152
+ for (const m of methods) {
42153
+ for (const ref of methodTypeRefs(m)) {
42154
+ if (!BUILTIN_TYPES.has(ref.toLowerCase())) names.add(ref.split(/::|\./).pop());
41479
42155
  }
41480
42156
  }
41481
- return out;
42157
+ return [...names];
41482
42158
  }
41483
- function readRawSystemPublicInterfaces() {
41484
- const raw = readYamlFile(AI_PATHS.specsSystem());
41485
- const list2 = raw?.publicInterfaces;
41486
- return Array.isArray(list2) ? list2 : [];
41487
- }
41488
- function resolveSubsystemInterface(entry, subsystems) {
41489
- const subId = firstString(entry.subsystem);
41490
- const interfaceId = firstString(entry.interface);
41491
- const componentId = firstString(entry.component);
41492
- const pools = subId ? subsystems.filter((s) => s.id === subId).map((s) => s.publicInterfaces) : subsystems.map((s) => s.publicInterfaces);
41493
- for (const pool of pools) {
41494
- const match = pool.find(
41495
- (pi) => interfaceId !== void 0 && pi.interface === interfaceId || componentId !== void 0 && pi.component === componentId
41496
- );
41497
- if (match) return match;
41498
- }
41499
- return void 0;
41500
- }
41501
- function buildRedactedSummaries(rawList, subsystems) {
42159
+ function buildRedactedSummaries(table, interfaces) {
41502
42160
  const out = [];
41503
- for (const entry of rawList) {
41504
- if (!entry || typeof entry !== "object") continue;
41505
- const id = firstString(entry.id, entry.systemInterfaceId, entry.interface, entry.component);
41506
- if (!id) continue;
41507
- const matched = resolveSubsystemInterface(entry, subsystems);
42161
+ for (const entry of table.entries) {
42162
+ if (entry.kind !== "component") continue;
42163
+ const methods = interfaces.filter((i) => i.component === entry.component && (!entry.interface || i.id === entry.interface)).flatMap((i) => i.methods);
41508
42164
  const summary = {
41509
- id,
41510
- name: firstString(entry.name) ?? id,
41511
- type: firstString(entry.type, matched?.type) ?? "Custom",
41512
- audience: firstString(entry.audience) ?? "public",
41513
- methods: toNameList(entry.methods),
41514
- details: firstString(entry.details, matched?.details) ?? ""
41515
- };
41516
- const version = firstString(entry.version);
41517
- if (version) summary.version = version;
41518
- const stability = firstString(entry.stability);
41519
- if (stability) summary.stability = stability;
41520
- const endpoints = toNameList(entry.endpoints);
42165
+ id: entry.publicName,
42166
+ name: entry.name ?? entry.publicName,
42167
+ type: entry.type ?? "Custom",
42168
+ audience: entry.audience ?? "instance",
42169
+ methods: methods.map((m) => m.name),
42170
+ details: entry.details ?? ""
42171
+ };
42172
+ if (entry.version) summary.version = entry.version;
42173
+ if (entry.stability) summary.stability = entry.stability;
42174
+ const endpoints = methods.map(endpointPath).filter((e) => !!e);
41521
42175
  if (endpoints.length > 0) summary.endpoints = endpoints;
41522
- const publicTypes = toNameList(entry.publicTypes);
42176
+ const publicTypes = referencedTypeNames(methods);
41523
42177
  if (publicTypes.length > 0) summary.publicTypes = publicTypes;
41524
42178
  out.push(summary);
41525
42179
  }
@@ -41680,9 +42334,8 @@ function refreshPublicSurface(cfg, credential, projectId) {
41680
42334
  const snapshot = runWithProjectRoot(root, () => {
41681
42335
  const stateId = computeStateId();
41682
42336
  const system = loadSystemSpec();
41683
- const subsystems = loadSubsystemSpecs();
41684
- const rawInterfaces = readRawSystemPublicInterfaces();
41685
- const interfaces = buildRedactedSummaries(rawInterfaces, subsystems);
42337
+ const exports2 = resolveProjectExports();
42338
+ const interfaces = buildRedactedSummaries(exports2, loadInterfaceSpecs());
41686
42339
  return {
41687
42340
  projectId,
41688
42341
  stateId: stateIdToString(stateId),
@@ -42120,14 +42773,14 @@ function migratePermissionModel(dataDir, apply) {
42120
42773
  }
42121
42774
  }
42122
42775
  if (apply) {
42123
- const lookup = new Map(remap.map((r) => [r.oldId, r.newId]));
42776
+ const lookup2 = new Map(remap.map((r) => [r.oldId, r.newId]));
42124
42777
  for (const unit of units) {
42125
- unit.id = lookup.get(unit.id) ?? unit.id;
42126
- if (unit.parentId !== void 0) unit.parentId = lookup.get(unit.parentId) ?? unit.parentId;
42127
- if (unit.exposeTo) unit.exposeTo = unit.exposeTo.map((e) => lookup.get(e) ?? e);
42778
+ unit.id = lookup2.get(unit.id) ?? unit.id;
42779
+ if (unit.parentId !== void 0) unit.parentId = lookup2.get(unit.parentId) ?? unit.parentId;
42780
+ if (unit.exposeTo) unit.exposeTo = unit.exposeTo.map((e) => lookup2.get(e) ?? e);
42128
42781
  }
42129
42782
  for (const placement of org.placements ?? []) {
42130
- placement.unitId = lookup.get(placement.unitId) ?? placement.unitId;
42783
+ placement.unitId = lookup2.get(placement.unitId) ?? placement.unitId;
42131
42784
  }
42132
42785
  writeRaw(orgPath, { units, placements: org.placements ?? [] });
42133
42786
  if (remap.length > 0) {