@wairon/cli 5.1.1-dev.96 → 5.1.1-dev.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7162,11 +7162,16 @@ var MethodSignatureSchema = import_zod8.z.object({
7162
7162
  */
7163
7163
  guarantees: import_zod8.z.array(GuaranteeSchema).optional(),
7164
7164
  /**
7165
- * State-effect direction of this method on its component's held state. Required on a
7166
- * durable Store's contract methods so the durability round-trip rule can pair external
7167
- * writes with hydration read-backs (MISSING_HYDRATION); optional elsewhere.
7165
+ * What this method does to its component's held state: `read` observes it, `write`
7166
+ * modifies an entity's domain fields, and `lifecycle` creates, destroys, or
7167
+ * (un)registers an entity's existence or membership without modifying its fields —
7168
+ * closed under composition (a lifecycle method calls only read and lifecycle methods,
7169
+ * LIFECYCLE_CALLS_WRITE). Required on a durable Store's contract methods so the
7170
+ * durability round-trip rule can pair mutations with hydration read-backs
7171
+ * (MISSING_HYDRATION); a Supervisor may call a data component it does not own only
7172
+ * through read and lifecycle methods (SUPERVISOR_WRITE_SHORTCUT). Optional elsewhere.
7168
7173
  */
7169
- effect: import_zod8.z.enum(["read", "write"]).optional(),
7174
+ effect: import_zod8.z.enum(["read", "write", "lifecycle"]).optional(),
7170
7175
  /**
7171
7176
  * Typed acknowledgment of a real caller OUTSIDE the modeled narrative graph
7172
7177
  * (runtime timer/hook, external system, sibling subsystem). Unused-detection
@@ -16150,7 +16155,7 @@ function defaultTargetConfig(type) {
16150
16155
  enabled: true
16151
16156
  };
16152
16157
  }
16153
- var WAIRON_VERSION = "5.1.1-dev.96";
16158
+ var WAIRON_VERSION = "5.1.1-dev.97";
16154
16159
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
16155
16160
  var ARCHITECT_AGENT_ID = "agent-architect";
16156
16161
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -17126,7 +17131,7 @@ function buildImportGraph(index, universe) {
17126
17131
  function buildOwnershipIndex(ctx) {
17127
17132
  const ownedBy2 = /* @__PURE__ */ new Map();
17128
17133
  for (const comp of ctx.components) {
17129
- if (isRetired(comp) || !isPattern(comp)) continue;
17134
+ if (isRetired(comp) || !(isPattern(comp) || comp.componentType === "Supervisor")) continue;
17130
17135
  for (const memberId of comp.owns) {
17131
17136
  const member = ctx.componentMap.get(memberId);
17132
17137
  if (!member || isPattern(member)) continue;
@@ -18423,10 +18428,10 @@ var projectBoundariesRule = {
18423
18428
  var lintAllowsRule = {
18424
18429
  name: "lint-allows",
18425
18430
  judges: "design",
18426
- description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. An allow covers exactly the occurrence it names: a finding that reports a site is silenced only by an allow whose `at` is that site, a finding that reports none only by an allow that names none, and an aggregating finding only by an allow whose `covers` lists every unit it reports \u2014 a unit nobody listed is named back as new instead of inheriting a decision taken about its neighbours. So a coarse allow left on a rule that names sites, and an allow whose site the run no longer reports, are both UNUSED_LINT_ALLOW, and the finding names the sites that did fire. Allows silence warnings and notices; errors always surface.",
18431
+ description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. An allow covers exactly the occurrence it names: a finding that reports a site is silenced only by an allow whose `at` is that site, a finding that reports none only by an allow that names none, and an aggregating finding only by an allow whose `covers` lists every unit it reports \u2014 a unit nobody listed is named back as new instead of inheriting a decision taken about its neighbours. So a coarse allow left on a rule that names sites, and an allow whose site the run no longer reports, are both UNUSED_LINT_ALLOW, and the finding names the sites that did fire. Allows silence warnings and notices; errors always surface \u2014 so an allow naming a code that is an error on its spec (it fired there as one, or its resolved severity is error) covers nothing, and the finding says plainly that an error cannot be allowed rather than that the code never fired.",
18427
18432
  codes: [
18428
18433
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
18429
- { code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014 the code never fired, or it fired at sites this allow does not name" }
18434
+ { code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry covers nothing this run \u2014 the code never fired, it fired at sites this allow does not name, or it is an error, which no allow can cover" }
18430
18435
  ],
18431
18436
  check(ctx) {
18432
18437
  for (const a of ctx.lintAllows) {
@@ -18439,9 +18444,20 @@ var lintAllowsRule = {
18439
18444
  );
18440
18445
  continue;
18441
18446
  }
18442
- if (a.used) continue;
18443
18447
  const reported = ctx.sitesReported(a.specId, a.code);
18444
18448
  const at = a.at ? ` at "${a.at}"` : "";
18449
+ const fired = reported.unsited || reported.sites.length > 0;
18450
+ const isError = reported.errored || !fired && ctx.severityOf(a.code, a.specId) === "error";
18451
+ if (isError) {
18452
+ ctx.addIssue(
18453
+ "warning",
18454
+ "UNUSED_LINT_ALLOW",
18455
+ `Spec "${a.specId}" allows "${a.code}"${at} (reason: ${a.reason}), but "${a.code}" is an error${fired ? " and fired here as one" : ""}, and an error cannot be allowed \u2014 lint.allow silences warnings and notices only. Fix what the finding names, or remove the allow (a project may re-tune the code's severity in rules.sddRuleSeverity).`,
18456
+ a.specId
18457
+ );
18458
+ continue;
18459
+ }
18460
+ if (a.used) continue;
18445
18461
  let why;
18446
18462
  if (a.at && reported.unsited && reported.sites.length === 0) {
18447
18463
  why = `findings of "${a.code}" on this spec name no site at all, so this allow must not name one \u2014 drop the \`at\``;
@@ -19964,18 +19980,47 @@ var dataBlockDepsRule = {
19964
19980
  };
19965
19981
 
19966
19982
  // src/core/rules/doctrine/entrypoint-dependencies.ts
19983
+ function maintainedRegistries(ctx) {
19984
+ const maintained = /* @__PURE__ */ new Map();
19985
+ const add2 = (supervisorId, registryId) => {
19986
+ let set = maintained.get(supervisorId);
19987
+ if (!set) maintained.set(supervisorId, set = /* @__PURE__ */ new Set());
19988
+ set.add(registryId);
19989
+ };
19990
+ for (const comp of ctx.components) {
19991
+ if (comp.componentType !== "Supervisor" || isRetired(comp)) continue;
19992
+ for (const memberId of comp.owns) {
19993
+ if (ctx.componentMap.get(memberId)?.componentType === "Registry") add2(comp.id, memberId);
19994
+ }
19995
+ }
19996
+ for (const impl of ctx.implementations) {
19997
+ const contract = ctx.interfaceMap.get(impl.contract);
19998
+ const supervisor = contract ? ctx.componentMap.get(contract.component) : void 0;
19999
+ if (!supervisor || supervisor.componentType !== "Supervisor" || isRetired(supervisor)) continue;
20000
+ for (const implMethod of impl.methods) {
20001
+ for (const step of implMethod.narrative) {
20002
+ if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
20003
+ if (ctx.componentMap.get(step.targetComponent)?.componentType !== "Registry") continue;
20004
+ const called = ctx.interfaceMethodsOf(step.targetComponent).find((m) => m.name === step.targetMethod);
20005
+ if (called?.effect === "lifecycle") add2(supervisor.id, step.targetComponent);
20006
+ }
20007
+ }
20008
+ }
20009
+ return new Map([...maintained].map(([id, set]) => [id, [...set]]));
20010
+ }
19967
20011
  var entrypointDepsRule = {
19968
20012
  name: "entrypoint-dependencies",
19969
20013
  judges: "design",
19970
- description: "Judges the edges at the system's entry points and its process layer. Portals and Observers are top-level entry points and subscribers, so nothing may depend on them \u2014 and that is the edge's one finding, which is why no other matrix rule judges it. Downward, a Portal dispatches to Orchestrators and may READ through Indexes and Repository facades but never reaches Store/Registry/Query or an Adapter, while an Observer forwards to one Orchestrator or Supervisor over a message-bus Adapter. A View stays a passive presenter. A Supervisor reaches data only through workflows, and a component depending on a live Actor must also depend on a Supervisor that supervises it.",
20014
+ description: "Judges the edges at the system's entry points and its process layer. Portals and Observers are top-level entry points and subscribers, so nothing may depend on them \u2014 and that is the edge's one finding, which is why no other matrix rule judges it. Downward, a Portal dispatches to Orchestrators and may READ through Indexes and Repository facades but never reaches Store/Registry/Query or an Adapter, while an Observer forwards to one Orchestrator or Supervisor over a message-bus Adapter. A View stays a passive presenter. A Supervisor stays out of presentation; what it may do to the data it reaches is judged per call (supervisor-shared-data), and what it owns as its supervision state by the ownership rules. A component depending on a live Actor reaches it through the Actor's supervision: by depending on a Supervisor that supervises it, or on a Registry such a Supervisor maintains (owns, or calls with lifecycle-effect methods) \u2014 the lookup hop callers really take.",
19971
20015
  codes: [
19972
20016
  { code: "ARCHITECTURE_VIOLATION_PORTAL_DEP", defaultSeverity: "error", summary: "Component depending on a Portal/Observer" },
19973
20017
  { code: "ARCHITECTURE_VIOLATION_PORTAL_FORBIDDEN_DEP", defaultSeverity: "error", summary: "Portal/Observer reaching the data layer directly" },
19974
20018
  { code: "ARCHITECTURE_VIOLATION_VIEW_DEP", defaultSeverity: "error", summary: "View depending on persistence layers or on logic that is not pure" },
19975
- { code: "ARCHITECTURE_VIOLATION_SUPERVISOR_DEP", defaultSeverity: "error", summary: "Supervisor depending on anything but Actors, Orchestrators, Adapters or other Supervisors \u2014 it reaches data only through workflows" },
19976
- { code: "ACTOR_REACHED_WITHOUT_SUPERVISOR", defaultSeverity: "error", summary: "Component depending on a live Actor it does not supervise, without also depending on a Supervisor that supervises it \u2014 a live Actor is reached by id through its Supervisor" }
20019
+ { code: "ARCHITECTURE_VIOLATION_SUPERVISOR_DEP", defaultSeverity: "error", summary: "Supervisor depending on a presentation block (View, FeatureComponent, RouterComponent) \u2014 a Supervisor manages live processes; the data it reaches is judged per call by supervisor-shared-data" },
20020
+ { code: "ACTOR_REACHED_WITHOUT_SUPERVISOR", defaultSeverity: "error", summary: "Component depending on a live Actor it does not supervise, reaching it neither through a Supervisor that supervises it nor through a Registry such a Supervisor maintains \u2014 model the real lookup hop" }
19977
20021
  ],
19978
20022
  check(ctx) {
20023
+ const lookups = maintainedRegistries(ctx);
19979
20024
  for (const edge of ctx.dependencyEdges().matrix) {
19980
20025
  const comp = edge.from;
19981
20026
  const depComp = edge.to;
@@ -20011,27 +20056,32 @@ var entrypointDepsRule = {
20011
20056
  edge.draftContext
20012
20057
  );
20013
20058
  }
20014
- if (comp.componentType === "Supervisor" && ["Store", "Registry", "Repository", "Index", "Query", "View", "FeatureComponent", "RouterComponent"].includes(depComp.componentType)) {
20059
+ if (comp.componentType === "Supervisor" && ["View", "FeatureComponent", "RouterComponent"].includes(depComp.componentType)) {
20015
20060
  ctx.addIssue(
20016
20061
  "error",
20017
20062
  "ARCHITECTURE_VIOLATION_SUPERVISOR_DEP",
20018
- `Architectural violation: Supervisor "${comp.id}" cannot depend on ${stereotypeOf(depComp)} "${depComp.id}". A Supervisor reaches data only through workflows \u2014 depend on the Orchestrator, Actor or Adapter that does that work instead.` + storeHint,
20063
+ `Architectural violation: Supervisor "${comp.id}" cannot depend on ${stereotypeOf(depComp)} "${depComp.id}". A Supervisor manages live processes and stays out of presentation \u2014 the UI reaches the system through its Portals and workflows, never through a Supervisor's dependencies.`,
20019
20064
  comp.id,
20020
20065
  edge.draftContext
20021
20066
  );
20022
20067
  }
20023
20068
  if (depComp.componentType === "Actor" && comp.componentType !== "Supervisor") {
20024
- const reachedThroughSupervisor = comp.dependsOn.some((id) => {
20025
- const supervisor = ctx.componentMap.get(id);
20026
- return supervisor?.componentType === "Supervisor" && supervisor.dependsOn.includes(depComp.id);
20027
- });
20028
- if (!reachedThroughSupervisor) {
20029
- const supervisors = ctx.components.filter((c) => c.componentType === "Supervisor" && c.dependsOn.includes(depComp.id)).map((c) => `"${c.id}"`);
20030
- const remedy = supervisors.length > 0 ? `also depend on ${supervisors.length === 1 ? "its Supervisor" : "one of its Supervisors"} (${supervisors.join(", ")})` : `no Supervisor depends on "${depComp.id}" yet, so give the Actor a Supervisor that depends on it and depend on that Supervisor`;
20069
+ const supervisors = ctx.components.filter((c) => c.componentType === "Supervisor" && c.dependsOn.includes(depComp.id));
20070
+ const registries = supervisors.flatMap((s) => (lookups.get(s.id) ?? []).map((registry) => ({ registry, supervisor: s.id })));
20071
+ const reached = supervisors.some((s) => comp.dependsOn.includes(s.id)) || registries.some((r) => comp.dependsOn.includes(r.registry));
20072
+ if (!reached) {
20073
+ let remedy;
20074
+ if (registries.length > 0) {
20075
+ remedy = `depend on ${registries.length === 1 ? "the Registry" : "one of the Registries"} its Supervisor maintains (${registries.map((r) => `"${r.registry}", kept by "${r.supervisor}"`).join("; ")}) and look the Actor up by id there`;
20076
+ } else if (supervisors.length > 0) {
20077
+ remedy = `${supervisors.length === 1 ? "its Supervisor" : "its Supervisors"} (${supervisors.map((s) => `"${s.id}"`).join(", ")}) maintain no Registry of its live handles yet: model that lookup as a Registry the Supervisor keeps through lifecycle-effect methods (registering and unregistering a live handle), and depend on that Registry`;
20078
+ } else {
20079
+ remedy = `no Supervisor depends on "${depComp.id}" yet: give it a Supervisor that depends on it and maintains a Registry of its live handles through lifecycle-effect methods, and depend on that Registry`;
20080
+ }
20031
20081
  ctx.addIssue(
20032
20082
  "error",
20033
20083
  "ACTOR_REACHED_WITHOUT_SUPERVISOR",
20034
- `Architectural violation: ${stereotypeOf(comp)} "${comp.id}" depends on the live Actor "${depComp.id}" without a Supervisor that supervises it. A live Actor is reached by id through a Supervisor that supervises it \u2014 ${remedy}.`,
20084
+ `Architectural violation: ${stereotypeOf(comp)} "${comp.id}" depends on the live Actor "${depComp.id}" without reaching it through its supervision. A live Actor is found by id through a Registry its Supervisor maintains, or messaged through that Supervisor \u2014 ${remedy}.`,
20035
20085
  comp.id,
20036
20086
  edge.draftContext
20037
20087
  );
@@ -20045,9 +20095,9 @@ var entrypointDepsRule = {
20045
20095
  var portalWriteShortcutRule = {
20046
20096
  name: "portal-write-shortcut",
20047
20097
  judges: "design",
20048
- description: "A Portal narrative call step or dispatch-table binding that reaches a write-effect method on a Repository or Index directly is the write shortcut: the Portal\u2192data-facade edge is licensed for reads only, and a write routes through an Orchestrator that owns the workflow. A dispatch step reaches its server only through a table binding, so judging every binding judges each dispatch step that takes it, once, where the route is declared. Methods that carry no effect tag are not judged.",
20098
+ description: "A Portal narrative call step or dispatch-table binding that reaches a write- or lifecycle-effect method on a Repository or Index directly is the write shortcut: the Portal\u2192data-facade edge is licensed for reads only, and a mutation \u2014 a write, or a lifecycle change to what exists \u2014 routes through an Orchestrator that owns the workflow. A dispatch step reaches its server only through a table binding, so judging every binding judges each dispatch step that takes it, once, where the route is declared. Methods that carry no effect tag are not judged.",
20049
20099
  codes: [
20050
- { code: "PORTAL_WRITE_SHORTCUT", defaultSeverity: "error", summary: "Portal narrative call or dispatch-table binding reaches a write-effect method on a Repository/Index directly \u2014 reads may shortcut, writes route through an Orchestrator (judged on effect-tagged facade methods; untagged methods are not yet judged)" }
20100
+ { code: "PORTAL_WRITE_SHORTCUT", defaultSeverity: "error", summary: "Portal narrative call or dispatch-table binding reaches a write- or lifecycle-effect method on a Repository/Index directly \u2014 reads may shortcut, mutations route through an Orchestrator (judged on effect-tagged facade methods; untagged methods are not yet judged)" }
20051
20101
  ],
20052
20102
  check(ctx) {
20053
20103
  for (const impl of ctx.implementations) {
@@ -20061,11 +20111,11 @@ var portalWriteShortcutRule = {
20061
20111
  const target = ctx.componentMap.get(step.targetComponent);
20062
20112
  if (!target || target.componentType !== "Repository" && target.componentType !== "Index") continue;
20063
20113
  const targetMethod = ctx.interfaceMethodsOf(target.id).find((m) => m.name === step.targetMethod);
20064
- if (targetMethod?.effect !== "write") continue;
20114
+ if (targetMethod?.effect !== "write" && targetMethod?.effect !== "lifecycle") continue;
20065
20115
  ctx.addIssue(
20066
20116
  "error",
20067
20117
  "PORTAL_WRITE_SHORTCUT",
20068
- `Portal "${component.id}": step ${step.stepNumber} of "${implMethod.name}" calls write-effect method ${target.id}.${step.targetMethod} directly. The Portal\u2192${target.componentType} shortcut is licensed for READS only \u2014 route the write through an Orchestrator that owns the workflow.`,
20118
+ `Portal "${component.id}": step ${step.stepNumber} of "${implMethod.name}" calls ${targetMethod.effect}-effect method ${target.id}.${step.targetMethod} directly. The Portal\u2192${target.componentType} shortcut is licensed for READS only \u2014 route the ${targetMethod.effect === "write" ? "write" : "lifecycle change"} through an Orchestrator that owns the workflow.`,
20069
20119
  impl.id,
20070
20120
  isDraftCtx || ctx.isComponentDraft(target.id)
20071
20121
  );
@@ -20079,11 +20129,11 @@ var portalWriteShortcutRule = {
20079
20129
  const target = ctx.componentMap.get(binding.component);
20080
20130
  if (!target || target.componentType !== "Repository" && target.componentType !== "Index") continue;
20081
20131
  const targetMethod = ctx.interfaceMethodsOf(target.id).find((m) => m.name === binding.method);
20082
- if (targetMethod?.effect !== "write") continue;
20132
+ if (targetMethod?.effect !== "write" && targetMethod?.effect !== "lifecycle") continue;
20083
20133
  ctx.addIssue(
20084
20134
  "error",
20085
20135
  "PORTAL_WRITE_SHORTCUT",
20086
- `Portal "${comp.id}": dispatch binding "${binding.capability}" routes to write-effect method ${target.id}.${binding.method} directly. The Portal\u2192${target.componentType} shortcut is licensed for READS only \u2014 route the write through an Orchestrator that owns the workflow.`,
20136
+ `Portal "${comp.id}": dispatch binding "${binding.capability}" routes to ${targetMethod.effect}-effect method ${target.id}.${binding.method} directly. The Portal\u2192${target.componentType} shortcut is licensed for READS only \u2014 route the ${targetMethod.effect === "write" ? "write" : "lifecycle change"} through an Orchestrator that owns the workflow.`,
20087
20137
  comp.id,
20088
20138
  isDraftCtx || ctx.isComponentDraft(target.id)
20089
20139
  );
@@ -20092,28 +20142,101 @@ var portalWriteShortcutRule = {
20092
20142
  }
20093
20143
  };
20094
20144
 
20145
+ // src/core/rules/doctrine/supervisor-shared-data.ts
20146
+ var DATA_COMPONENTS = /* @__PURE__ */ new Set(["Store", "Registry", "Repository", "Index", "Query"]);
20147
+ var supervisorSharedDataRule = {
20148
+ name: "supervisor-shared-data",
20149
+ judges: "design",
20150
+ description: "A Supervisor keeps its own supervision state (the Stores and Registries it owns) with full read and write, but data it does not own is shared: a Supervisor narrative call step that reaches a method of a Store, Registry, Repository, Index or Query the Supervisor does not own is legal only when that method declares effect read or lifecycle. A write \u2014 or a method that declares no effect \u2014 goes through a workflow: the Supervisor depends on the Orchestrator that does it. A read on behalf of a request still passes; in practice the paired write is what forces the workflow out, and the read moves with it.",
20151
+ codes: [
20152
+ { code: "SUPERVISOR_WRITE_SHORTCUT", defaultSeverity: "error", summary: "Supervisor narrative call reaches a method of a data component it does not own whose declared effect is neither read nor lifecycle \u2014 writes to shared data route through an Orchestrator" }
20153
+ ],
20154
+ check(ctx) {
20155
+ const ownership = ctx.ownershipIndex();
20156
+ for (const impl of ctx.implementations) {
20157
+ const contract = ctx.interfaceMap.get(impl.contract);
20158
+ const supervisor = contract ? ctx.componentMap.get(contract.component) : void 0;
20159
+ if (!supervisor || supervisor.componentType !== "Supervisor" || isRetired(supervisor)) continue;
20160
+ const isDraftCtx = ctx.isImplementationDraft(impl);
20161
+ for (const implMethod of impl.methods) {
20162
+ for (const step of implMethod.narrative) {
20163
+ if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
20164
+ const target = ctx.componentMap.get(step.targetComponent);
20165
+ if (!target || !DATA_COMPONENTS.has(target.componentType)) continue;
20166
+ if (ownership.ownerOf(target.id) === supervisor.id) continue;
20167
+ const called = ctx.interfaceMethodsOf(target.id).find((m) => m.name === step.targetMethod);
20168
+ if (!called || called.effect === "read" || called.effect === "lifecycle") continue;
20169
+ const what = called.effect === "write" ? "a write-effect method" : "a method that declares no effect";
20170
+ const untaggedHint = called.effect === "write" ? "" : ` If ${target.id}.${called.name} only reads, or only changes what exists, declare its effect (read or lifecycle).`;
20171
+ ctx.addIssue(
20172
+ "error",
20173
+ "SUPERVISOR_WRITE_SHORTCUT",
20174
+ `Supervisor "${supervisor.id}": step ${step.stepNumber} of "${implMethod.name}" calls ${target.id}.${called.name}, ${what} on ${target.componentType} "${target.id}", which the Supervisor does not own. A Supervisor reads shared data and changes what exists in it (read and lifecycle effects); a write to its fields goes through a workflow \u2014 depend on the Orchestrator that does it and call that instead.${untaggedHint}`,
20175
+ impl.id,
20176
+ isDraftCtx || ctx.isComponentDraft(target.id)
20177
+ );
20178
+ }
20179
+ }
20180
+ }
20181
+ }
20182
+ };
20183
+
20184
+ // src/core/rules/doctrine/lifecycle-effect-closure.ts
20185
+ var lifecycleEffectClosureRule = {
20186
+ name: "lifecycle-effect-closure",
20187
+ judges: "design",
20188
+ description: "A lifecycle-effect method may create, destroy, or (un)register an entity's existence or membership, never modify its domain fields \u2014 and the effect is closed under composition: its narrative may call only read- and lifecycle-effect methods besides its construction and local steps. A call step to a write-effect method from a lifecycle-effect method is a declaration error: the method is a write, or the write belongs to a workflow beside it. Called methods that declare no effect are not judged.",
20189
+ codes: [
20190
+ { code: "LIFECYCLE_CALLS_WRITE", defaultSeverity: "error", summary: "Lifecycle-effect method whose narrative calls a write-effect method \u2014 the lifecycle effect is closed under composition" }
20191
+ ],
20192
+ check(ctx) {
20193
+ for (const impl of ctx.implementations) {
20194
+ const contract = ctx.interfaceMap.get(impl.contract);
20195
+ if (!contract) continue;
20196
+ const declared = ctx.interfaceMethodsOf(contract.component);
20197
+ const isDraftCtx = ctx.isImplementationDraft(impl);
20198
+ for (const implMethod of impl.methods) {
20199
+ if (declared.find((m) => m.name === implMethod.name)?.effect !== "lifecycle") continue;
20200
+ for (const step of implMethod.narrative) {
20201
+ if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
20202
+ const called = ctx.interfaceMethodsOf(step.targetComponent).find((m) => m.name === step.targetMethod);
20203
+ if (called?.effect !== "write") continue;
20204
+ ctx.addIssue(
20205
+ "error",
20206
+ "LIFECYCLE_CALLS_WRITE",
20207
+ `"${contract.component}.${implMethod.name}" declares effect lifecycle, but step ${step.stepNumber} calls write-effect method ${step.targetComponent}.${step.targetMethod}. A lifecycle method creates, destroys or (un)registers what exists and never modifies domain fields, and the effect is closed under composition \u2014 it may call only read and lifecycle methods. Declare the method a write, or move the write to a workflow beside it.`,
20208
+ impl.id,
20209
+ isDraftCtx
20210
+ );
20211
+ }
20212
+ }
20213
+ }
20214
+ }
20215
+ };
20216
+
20095
20217
  // src/core/rules/doctrine/pattern-membership.ts
20096
20218
  var patternMembershipRule = {
20097
20219
  name: "pattern-membership",
20098
20220
  judges: "design",
20099
- description: "Only patterns (Repository/FeatureComponent/RouterComponent) own member blocks, and every pattern owns at least one. Each claim must name a component that exists and is itself a building block \u2014 patterns compose at the subsystem (L1) level, never by owning one another \u2014 and a block has exactly one owner, the first pattern to claim it.",
20221
+ description: "Only patterns (Repository/FeatureComponent/RouterComponent) own member blocks, and every pattern owns at least one. The one building block that may own is a Supervisor, which owns its supervision state and may own nothing at all; what it may own is pattern-containment's question. Each claim must name a component that exists and is itself a building block \u2014 patterns compose at the subsystem (L1) level, never by owning one another \u2014 and a block has exactly one owner, the first pattern or Supervisor to claim it.",
20100
20222
  codes: [
20101
20223
  { code: "EMPTY_PATTERN", defaultSeverity: "error", summary: "Pattern with no owned member blocks" },
20102
- { code: "BLOCK_OWNS_MEMBERS", defaultSeverity: "error", summary: "Building block using owns" },
20224
+ { code: "BLOCK_OWNS_MEMBERS", defaultSeverity: "error", summary: "Building block other than a Supervisor using owns" },
20103
20225
  { code: "INVALID_OWNED_MEMBER", defaultSeverity: "error", summary: "owns names a non-existent component" },
20104
20226
  { code: "PATTERN_OWNS_PATTERN", defaultSeverity: "error", summary: "Pattern owning another pattern" },
20105
- { code: "SHARED_OWNED_MEMBER", defaultSeverity: "error", summary: "Block owned by two patterns" }
20227
+ { code: "SHARED_OWNED_MEMBER", defaultSeverity: "error", summary: "Block owned by two owners (patterns or Supervisors)" }
20106
20228
  ],
20107
20229
  check(ctx) {
20108
20230
  for (const comp of ctx.components) {
20109
20231
  if (isRetired(comp)) continue;
20110
20232
  const isDraftCtx = ctx.isComponentDraft(comp.id);
20111
20233
  const pattern = isPattern(comp);
20234
+ const supervisor = comp.componentType === "Supervisor";
20112
20235
  if (pattern && comp.owns.length === 0) {
20113
20236
  ctx.addIssue("error", "EMPTY_PATTERN", `Pattern "${comp.id}" (${comp.componentType}) must own member blocks via "owns".`, comp.id, isDraftCtx);
20114
20237
  }
20115
- if (!pattern && comp.owns.length > 0) {
20116
- ctx.addIssue("error", "BLOCK_OWNS_MEMBERS", `Building block "${comp.id}" (${comp.componentType}) cannot own members; only patterns (${Array.from(PATTERN_TYPES).join("/")}) use "owns".`, comp.id, isDraftCtx);
20238
+ if (!pattern && !supervisor && comp.owns.length > 0) {
20239
+ ctx.addIssue("error", "BLOCK_OWNS_MEMBERS", `Building block "${comp.id}" (${comp.componentType}) cannot own members; only patterns (${Array.from(PATTERN_TYPES).join("/")}) use "owns", and a Supervisor for its supervision state.`, comp.id, isDraftCtx);
20117
20240
  }
20118
20241
  }
20119
20242
  const ownership = ctx.ownershipIndex();
@@ -20121,15 +20244,18 @@ var patternMembershipRule = {
20121
20244
  if (isRetired(comp)) continue;
20122
20245
  const isDraftCtx = ctx.isComponentDraft(comp.id);
20123
20246
  const pattern = isPattern(comp);
20247
+ const supervisor = comp.componentType === "Supervisor";
20124
20248
  for (const memberId of comp.owns) {
20125
20249
  const member = ctx.componentMap.get(memberId);
20126
20250
  if (!member) {
20127
20251
  ctx.addIssue("error", "INVALID_OWNED_MEMBER", `Component "${comp.id}" owns "${memberId}" which does not exist.`, comp.id, isDraftCtx);
20128
20252
  continue;
20129
20253
  }
20130
- if (!pattern) continue;
20254
+ if (!pattern && !supervisor) continue;
20131
20255
  if (isPattern(member)) {
20132
- ctx.addIssue("error", "PATTERN_OWNS_PATTERN", `Pattern "${comp.id}" owns "${memberId}", which is itself a pattern. Patterns own only building blocks \u2014 compose patterns at the subsystem (L1) level.`, comp.id, isDraftCtx);
20256
+ if (pattern) {
20257
+ ctx.addIssue("error", "PATTERN_OWNS_PATTERN", `Pattern "${comp.id}" owns "${memberId}", which is itself a pattern. Patterns own only building blocks \u2014 compose patterns at the subsystem (L1) level.`, comp.id, isDraftCtx);
20258
+ }
20133
20259
  continue;
20134
20260
  }
20135
20261
  const firstOwner = ownership.ownerOf(memberId);
@@ -20143,13 +20269,15 @@ var patternMembershipRule = {
20143
20269
 
20144
20270
  // src/core/rules/doctrine/pattern-containment.ts
20145
20271
  var REPOSITORY_MEMBERS = /* @__PURE__ */ new Set(["Store", "Registry", "Index", "Query", "Adapter"]);
20272
+ var SUPERVISION_STATE = /* @__PURE__ */ new Set(["Store", "Registry"]);
20146
20273
  var patternContainmentRule = {
20147
20274
  name: "pattern-containment",
20148
20275
  judges: "design",
20149
- description: "Holds each pattern to the containment its definition prescribes. A Repository may own only Store, Registry, Index, Query and (optionally) Adapter, judged member by member. A FeatureComponent owns exactly one Orchestrator (the logic side) and one or more Views (its faces \u2014 list, detail, form \u2014 sharing the one logic component), and nothing else. A RouterComponent owns exactly one Portal as its facade and at least one other child to route to. A counting pattern that still owns a retired member is not judged until that member is migrated: its counts change with the migration, and STEREOTYPE_RETIRED is the one finding.",
20276
+ description: "Holds each owner to the containment its definition prescribes. A Repository may own only Store, Registry, Index, Query and (optionally) Adapter, judged member by member. A Supervisor may own only its supervision state \u2014 Stores and Registries that are its own, one hop \u2014 judged member by member too. A FeatureComponent owns exactly one Orchestrator (the logic side) and one or more Views (its faces \u2014 list, detail, form \u2014 sharing the one logic component), and nothing else. A RouterComponent owns exactly one Portal as its facade and at least one other child to route to. A counting pattern that still owns a retired member is not judged until that member is migrated: its counts change with the migration, and STEREOTYPE_RETIRED is the one finding.",
20150
20277
  codes: [
20151
20278
  { code: "REPOSITORY_CONTAINMENT", defaultSeverity: "error", summary: "Repository owning a non Store/Registry/Index/Query/Adapter member" },
20152
20279
  { code: "FEATURE_COMPONENT_CONTAINMENT", defaultSeverity: "error", summary: "FeatureComponent not owning exactly one Orchestrator + one or more Views" },
20280
+ { code: "SUPERVISOR_CONTAINMENT", defaultSeverity: "error", summary: "Supervisor owning anything but a Store or Registry \u2014 a Supervisor owns only its supervision state" },
20153
20281
  { code: "ROUTER_COMPONENT_CONTAINMENT", defaultSeverity: "error", summary: "RouterComponent not owning exactly one Portal facade, or owning no children to route to" }
20154
20282
  ],
20155
20283
  check(ctx) {
@@ -20200,6 +20328,15 @@ var patternContainmentRule = {
20200
20328
  }
20201
20329
  }
20202
20330
  }
20331
+ for (const comp of ctx.components) {
20332
+ if (comp.componentType !== "Supervisor" || isRetired(comp)) continue;
20333
+ const isDraftCtx = ctx.isComponentDraft(comp.id);
20334
+ for (const memberId of comp.owns) {
20335
+ const member = ctx.componentMap.get(memberId);
20336
+ if (!member || isRetired(member) || SUPERVISION_STATE.has(member.componentType)) continue;
20337
+ ctx.addIssue("error", "SUPERVISOR_CONTAINMENT", `Supervisor "${comp.id}" owns "${memberId}" of type ${member.componentType}; a Supervisor owns only its supervision state \u2014 Stores and Registries that are its own. Depend on "${memberId}" as a collaborator instead.`, comp.id, isDraftCtx);
20338
+ }
20339
+ }
20203
20340
  }
20204
20341
  };
20205
20342
 
@@ -20207,7 +20344,7 @@ var patternContainmentRule = {
20207
20344
  var unownedBlocksRule = {
20208
20345
  name: "unowned-blocks",
20209
20346
  judges: "design",
20210
- description: `Judges the data blocks no pattern owns. A Store may stand alone deliberately \u2014 the sanctioned lightweight form for genuinely simple state, acknowledged with a lint.allow \u2014 while the recommended shape stays a Repository; what must never happen is folding the state into a consuming component, where no spec, diagram or conformance check can see it again. A Registry standing alone with no Store to write to is either mistyped (the "file-backed Registry" idiom, a fused persistent store that belongs typed Store) or orphaned. A Query has no standalone form at all: it computes reads over its own Repository's Store.`,
20347
+ description: `Judges the data blocks no owner (a pattern, or a Supervisor keeping its supervision state) owns. A Store may stand alone deliberately \u2014 the sanctioned lightweight form for genuinely simple state, acknowledged with a lint.allow \u2014 while the recommended shape stays a Repository; what must never happen is folding the state into a consuming component, where no spec, diagram or conformance check can see it again. A Registry standing alone with no Store to write to is either mistyped (the "file-backed Registry" idiom, a fused persistent store that belongs typed Store) or orphaned. A Query has no standalone form at all: it computes reads over its own Repository's Store.`,
20211
20348
  codes: [
20212
20349
  { code: "UNOWNED_STORE", defaultSeverity: "warning", summary: "Store not owned by any pattern \u2014 recommended shape is a Repository; a deliberate standalone Store needs a lint.allow" },
20213
20350
  { code: "REGISTRY_WITHOUT_STORE", defaultSeverity: "warning", summary: "Standalone Registry with no Store to write to \u2014 either mistyped (a fused file-backed store belongs typed Store) or orphaned" },
@@ -20253,9 +20390,10 @@ var unownedBlocksRule = {
20253
20390
  var memberVisibilityRule = {
20254
20391
  name: "member-visibility",
20255
20392
  judges: "design",
20256
- description: "A component may depend on a block within its own group (it is the owning pattern, or a sibling member of the same pattern), on any pattern facade, or on a standalone block \u2014 never on a block privately owned by ANOTHER pattern, which must be reached through that pattern's facade. A retired depending component is skipped, as everywhere in this family.",
20393
+ description: "A component may depend on a block within its own group (it is the owner, or a sibling member of the same owner), on any pattern facade, or on a standalone block \u2014 never on a block privately owned by ANOTHER pattern, which must be reached through that pattern's facade. A Store or Registry a Supervisor owns is that Supervisor's supervision state and nobody else depends on it: a component that does is the intruder, and the finding is reported on it, naming the Supervisor and the state. A retired depending component is skipped, as everywhere in this family.",
20257
20394
  codes: [
20258
- { code: "VISIBILITY_VIOLATION", defaultSeverity: "error", summary: "Dependency on a block privately owned by another pattern" }
20395
+ { code: "VISIBILITY_VIOLATION", defaultSeverity: "error", summary: "Dependency on a block privately owned by another pattern" },
20396
+ { code: "SUPERVISION_STATE_INTRUSION", defaultSeverity: "error", summary: "Dependency on a Store or Registry a Supervisor owns as its supervision state \u2014 reported on the intruder, naming the Supervisor and the state" }
20259
20397
  ],
20260
20398
  check(ctx) {
20261
20399
  const ownership = ctx.ownershipIndex();
@@ -20266,7 +20404,12 @@ var memberVisibilityRule = {
20266
20404
  if (!owner) continue;
20267
20405
  if (owner === comp.id) continue;
20268
20406
  if (ownership.ownerOf(comp.id) === owner) continue;
20269
- ctx.addIssue("error", "VISIBILITY_VIOLATION", `Component "${comp.id}" depends on "${depId}", which is privately owned by pattern "${owner}". Depend on the facade "${owner}" instead.`, comp.id, ctx.isComponentDraft(comp.id) || ctx.isComponentDraft(depId));
20407
+ const isDraftCtx = ctx.isComponentDraft(comp.id) || ctx.isComponentDraft(depId);
20408
+ if (ctx.componentMap.get(owner)?.componentType === "Supervisor") {
20409
+ ctx.addIssue("error", "SUPERVISION_STATE_INTRUSION", `"${comp.id}" depends on "${depId}", which Supervisor "${owner}" owns as its supervision state. Supervision state is private to its Supervisor \u2014 nobody else depends on it. If "${comp.id}" needs this data it is shared data: move "${depId}" out of "${owner}"'s owns into a data component both depend on (the Supervisor keeping it through read and lifecycle calls), or reach what "${comp.id}" needs through "${owner}".`, comp.id, isDraftCtx);
20410
+ continue;
20411
+ }
20412
+ ctx.addIssue("error", "VISIBILITY_VIOLATION", `Component "${comp.id}" depends on "${depId}", which is privately owned by pattern "${owner}". Depend on the facade "${owner}" instead.`, comp.id, isDraftCtx);
20270
20413
  }
20271
20414
  }
20272
20415
  }
@@ -21399,9 +21542,9 @@ var durabilityRule = {
21399
21542
  // Stage 8: its verdict needs the whole system's specs, so a part judged alone skips it.
21400
21543
  needsWholeTree: true,
21401
21544
  judges: "design",
21402
- description: "A durable Store (persisted RAM projection) must carry effect-tagged contract methods, and its writes require a hydration read-back reachable from a lifecycle init entrypoint. read-through is exempt (every read IS the read-back), as are ram-projection (rebuilt not restored) and cache (evictable, loss-safe). The flagship semantic check is opt-out by declaration, never silently absent \u2014 the declaration itself is enforced by durability-declaration.",
21545
+ description: "A durable Store (persisted RAM projection) must carry effect-tagged contract methods, and its mutations \u2014 write-effect methods, and lifecycle-effect methods, which change what exists \u2014 require a hydration read-back reachable from a lifecycle init entrypoint. read-through is exempt (every read IS the read-back), as are ram-projection (rebuilt not restored) and cache (evictable, loss-safe). The flagship semantic check is opt-out by declaration, never silently absent \u2014 the declaration itself is enforced by durability-declaration.",
21403
21546
  codes: [
21404
- { code: "MISSING_EFFECT_TAG", defaultSeverity: "warning", summary: "Durable Store contract method lacks an effect: read | write tag" },
21547
+ { code: "MISSING_EFFECT_TAG", defaultSeverity: "warning", summary: "Durable Store contract method lacks an effect: read | write | lifecycle tag" },
21405
21548
  { code: "MISSING_HYDRATION", defaultSeverity: "error", summary: "Durable Store is written but no read-back is reachable from any lifecycle init entrypoint" }
21406
21549
  ],
21407
21550
  check(ctx) {
@@ -21429,7 +21572,7 @@ var durabilityRule = {
21429
21572
  isDraftCtx
21430
21573
  );
21431
21574
  }
21432
- const writes = methods.filter((method) => method.effect === "write");
21575
+ const writes = methods.filter((method) => method.effect === "write" || method.effect === "lifecycle");
21433
21576
  const reads = methods.filter((method) => method.effect === "read");
21434
21577
  if (writes.length === 0) continue;
21435
21578
  const hydrated = initReach !== null && reads.some((method) => initReach.reachesMethod(comp.id, method.name));
@@ -24722,6 +24865,10 @@ var SDD_RULES = [
24722
24865
  dataBlockDepsRule,
24723
24866
  entrypointDepsRule,
24724
24867
  portalWriteShortcutRule,
24868
+ // The process layer's data reach and the lifecycle effect's closure, both
24869
+ // read from narratives against the callee's declared effect.
24870
+ supervisorSharedDataRule,
24871
+ lifecycleEffectClosureRule,
24725
24872
  // Patterns in four questions: who may own and what a claim must name,
24726
24873
  // what each pattern must contain, which data blocks are left standing
24727
24874
  // alone, and who may see a private member.
@@ -26445,7 +26592,16 @@ function buildRuleContext(opts) {
26445
26592
  const sitesSeen = /* @__PURE__ */ new Map();
26446
26593
  const sitesReported = (specId, code) => {
26447
26594
  const seen = sitesSeen.get(`${specId}\0${code}`);
26448
- return { sites: [...seen?.sites ?? []], unsited: seen?.unsited ?? false };
26595
+ return { sites: [...seen?.sites ?? []], unsited: seen?.unsited ?? false, errored: seen?.errored ?? false };
26596
+ };
26597
+ const codeDefaults = new Map(opts.issueCodeSeverities ?? []);
26598
+ for (const a of opts.extensions?.assertions ?? []) {
26599
+ if (!codeDefaults.has(a.fullCode)) codeDefaults.set(a.fullCode, a.severity);
26600
+ }
26601
+ const severityOf = (code, specId) => {
26602
+ const defaultSeverity = codeDefaults.get(code);
26603
+ if (!defaultSeverity) return void 0;
26604
+ return getRuleSeverity(code, defaultSeverity, false, subsystemOfSpec.get(specId));
26449
26605
  };
26450
26606
  for (const s of subsystems) collectAllows(s.id, s.lint);
26451
26607
  for (const c of components) collectAllows(c.id, c.lint);
@@ -26488,9 +26644,10 @@ function buildRuleContext(opts) {
26488
26644
  if (specId) {
26489
26645
  const key = `${specId}\0${code}`;
26490
26646
  let seen = sitesSeen.get(key);
26491
- if (!seen) sitesSeen.set(key, seen = { sites: /* @__PURE__ */ new Set(), unsited: false });
26647
+ if (!seen) sitesSeen.set(key, seen = { sites: /* @__PURE__ */ new Set(), unsited: false, errored: false });
26492
26648
  if (parts) seen.sites.add(parts.at);
26493
26649
  else seen.unsited = true;
26650
+ if (severity === "error") seen.errored = true;
26494
26651
  }
26495
26652
  let allowClaimed = false;
26496
26653
  if (specId) {
@@ -26607,6 +26764,7 @@ function buildRuleContext(opts) {
26607
26764
  roundTripIssues: opts.roundTripIssues,
26608
26765
  lintAllows: lintAllows2,
26609
26766
  sitesReported,
26767
+ severityOf,
26610
26768
  knownIssueCodes: opts.knownIssueCodes,
26611
26769
  carriedFindings,
26612
26770
  carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
@@ -27039,6 +27197,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend") {
27039
27197
  const carryableCodes = new Set(
27040
27198
  knownIssueCodes().filter((rc) => rc.carryable).map((rc) => rc.code)
27041
27199
  );
27200
+ const codeSeverities = new Map(knownIssueCodes().map((rc) => [rc.code, rc.defaultSeverity]));
27042
27201
  const family = graph();
27043
27202
  const memberTables = family.nodes.filter((n) => n.namespace !== "").map((n) => resolveProjectExports(n.namespace));
27044
27203
  const producers = new Set(family.references.filter((r) => r.consumer === "").map((r) => r.producer));
@@ -27081,6 +27240,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend") {
27081
27240
  codeModel,
27082
27241
  roundTripIssues,
27083
27242
  knownIssueCodes: knownCodes,
27243
+ issueCodeSeverities: codeSeverities,
27084
27244
  carryableIssueCodes: carryableCodes,
27085
27245
  issues
27086
27246
  });
@@ -43280,7 +43440,7 @@ ${renderChangeReport(report2)}`,
43280
43440
  ),
43281
43441
  params: import_zod11.z.array(methodParamItem).optional().describe("Structured parameters \u2014 authoritative for type checking, and the signature text is derived from them. Strongly preferred."),
43282
43442
  guarantees: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Semantic guarantees the method promises (combinable); any guarantee a narrative step asserts must be declared here. Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
43283
- effect: import_zod11.z.enum(["read", "write"]).optional().describe("State-effect direction on the component's held state \u2014 required on a durable Store's contract methods so the durability round-trip rule can pair writes with hydration read-backs"),
43443
+ effect: import_zod11.z.enum(["read", "write", "lifecycle"]).optional().describe("What the method does to the component's held state: read observes it; write modifies an entity's domain fields; lifecycle creates, destroys, or (un)registers an entity's existence or membership without modifying its fields, and calls only read and lifecycle methods (LIFECYCLE_CALLS_WRITE). Required on a durable Store's contract methods so the durability round-trip rule can pair mutations with hydration read-backs; a Supervisor may call a data component it does not own only through read and lifecycle methods (SUPERVISOR_WRITE_SHORTCUT)"),
43284
43444
  invokedBy: import_zod11.z.object({
43285
43445
  kind: import_zod11.z.enum(["runtime", "external", "sibling-subsystem"]).describe("Who owns the out-of-graph invocation: runtime (timer/signal/shutdown hook), external (a system outside this project), sibling-subsystem (a modeled sibling whose edge is not narrated here)"),
43286
43446
  caller: import_zod11.z.string().optional().describe("WHO invokes it and when, as reviewable prose \u2014 missing or placeholder-thin prose is INVOKED_BY_UNDESCRIBED")