@tacuchi/agent-workflow-cli 21.16.0 → 22.0.0

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.
Files changed (209) hide show
  1. package/README.md +4 -16
  2. package/dist/adapters/git-cli.js +36 -0
  3. package/dist/adapters/git-cli.js.map +1 -1
  4. package/dist/application/capability/design-handler.js +472 -177
  5. package/dist/application/capability/design-handler.js.map +1 -1
  6. package/dist/application/capability/dispatcher.js +1 -1
  7. package/dist/application/capability/dispatcher.js.map +1 -1
  8. package/dist/application/capability/durable-effect.js +1 -1
  9. package/dist/application/capability/durable-effect.js.map +1 -1
  10. package/dist/application/check-branch-service.js +1 -0
  11. package/dist/application/check-branch-service.js.map +1 -1
  12. package/dist/application/checkpoint-service.js +4 -3
  13. package/dist/application/checkpoint-service.js.map +1 -1
  14. package/dist/application/design/consumer-document.js +179 -0
  15. package/dist/application/design/consumer-document.js.map +1 -0
  16. package/dist/application/design/design-content-gate-service.js +9 -1
  17. package/dist/application/design/design-content-gate-service.js.map +1 -1
  18. package/dist/application/design/design-gate-service.js +18 -1
  19. package/dist/application/design/design-gate-service.js.map +1 -1
  20. package/dist/application/design/design-index-service.js +22 -3
  21. package/dist/application/design/design-index-service.js.map +1 -1
  22. package/dist/application/design/design-publish-service.js +44 -136
  23. package/dist/application/design/design-publish-service.js.map +1 -1
  24. package/dist/application/design/design-simple-service.js +38 -24
  25. package/dist/application/design/design-simple-service.js.map +1 -1
  26. package/dist/application/dev-only-services.js +19 -12
  27. package/dist/application/dev-only-services.js.map +1 -1
  28. package/dist/application/docs-canon-service.js +79 -5
  29. package/dist/application/docs-canon-service.js.map +1 -1
  30. package/dist/application/dsn-reader-service.js +12 -47
  31. package/dist/application/dsn-reader-service.js.map +1 -1
  32. package/dist/application/export-service.js +20 -11
  33. package/dist/application/export-service.js.map +1 -1
  34. package/dist/application/flow/flow-service.js +25 -0
  35. package/dist/application/flow/flow-service.js.map +1 -1
  36. package/dist/application/flow/internal-actions.js +5 -0
  37. package/dist/application/flow/internal-actions.js.map +1 -1
  38. package/dist/application/flow/submit.js +152 -22
  39. package/dist/application/flow/submit.js.map +1 -1
  40. package/dist/application/history-table.js +82 -44
  41. package/dist/application/history-table.js.map +1 -1
  42. package/dist/application/history-update-service.js +19 -9
  43. package/dist/application/history-update-service.js.map +1 -1
  44. package/dist/application/hook-git-commit-advisor.js +5 -4
  45. package/dist/application/hook-git-commit-advisor.js.map +1 -1
  46. package/dist/application/hook-sql-mutation-guard.js +1 -1
  47. package/dist/application/hook-sql-mutation-guard.js.map +1 -1
  48. package/dist/application/lifecycle-target.js +1 -0
  49. package/dist/application/lifecycle-target.js.map +1 -1
  50. package/dist/application/local-proposal.js +47 -23
  51. package/dist/application/local-proposal.js.map +1 -1
  52. package/dist/application/mcp-connections-service.js +37 -2
  53. package/dist/application/mcp-connections-service.js.map +1 -1
  54. package/dist/application/mcp-dbhub-launcher.js +22 -50
  55. package/dist/application/mcp-dbhub-launcher.js.map +1 -1
  56. package/dist/application/mcp-doctor-service.js +14 -23
  57. package/dist/application/mcp-doctor-service.js.map +1 -1
  58. package/dist/application/mcp-remove-service.js +3 -3
  59. package/dist/application/mcp-remove-service.js.map +1 -1
  60. package/dist/application/mcp-setup-service.js +4 -4
  61. package/dist/application/mcp-setup-service.js.map +1 -1
  62. package/dist/application/mcp-test-connection-service.js +4 -6
  63. package/dist/application/mcp-test-connection-service.js.map +1 -1
  64. package/dist/application/parsers/phases.js +17 -5
  65. package/dist/application/parsers/phases.js.map +1 -1
  66. package/dist/application/parsers/project-block.js +5 -11
  67. package/dist/application/parsers/project-block.js.map +1 -1
  68. package/dist/application/parsers/sessions-csv.js +16 -14
  69. package/dist/application/parsers/sessions-csv.js.map +1 -1
  70. package/dist/application/parsers/spec-relation.js +15 -11
  71. package/dist/application/parsers/spec-relation.js.map +1 -1
  72. package/dist/application/parsers/tasks.js +25 -1
  73. package/dist/application/parsers/tasks.js.map +1 -1
  74. package/dist/application/persist-service.js +67 -29
  75. package/dist/application/persist-service.js.map +1 -1
  76. package/dist/application/plugin-doctor/skills.js +1 -40
  77. package/dist/application/plugin-doctor/skills.js.map +1 -1
  78. package/dist/application/plugin-doctor-service.js +1 -3
  79. package/dist/application/plugin-doctor-service.js.map +1 -1
  80. package/dist/application/release-data/artifacts.js +1 -0
  81. package/dist/application/release-data/artifacts.js.map +1 -1
  82. package/dist/application/release-data/bundles.js +15 -6
  83. package/dist/application/release-data/bundles.js.map +1 -1
  84. package/dist/application/release-data/common.js +16 -7
  85. package/dist/application/release-data/common.js.map +1 -1
  86. package/dist/application/release-data/sessions.js +36 -8
  87. package/dist/application/release-data/sessions.js.map +1 -1
  88. package/dist/application/render/project-block.js +2 -2
  89. package/dist/application/render/project-block.js.map +1 -1
  90. package/dist/application/resume-service.js +9 -1
  91. package/dist/application/resume-service.js.map +1 -1
  92. package/dist/application/retirement/graph.js +14 -13
  93. package/dist/application/retirement/graph.js.map +1 -1
  94. package/dist/application/retirement/prepare.js +15 -2
  95. package/dist/application/retirement/prepare.js.map +1 -1
  96. package/dist/application/retirement/resolve.js.map +1 -1
  97. package/dist/application/self/clean-legacy.js +2 -4
  98. package/dist/application/self/clean-legacy.js.map +1 -1
  99. package/dist/application/self/mcp-config.js +8 -9
  100. package/dist/application/self/mcp-config.js.map +1 -1
  101. package/dist/application/session-close-service.js +8 -2
  102. package/dist/application/session-close-service.js.map +1 -1
  103. package/dist/application/session-create-service.js +26 -14
  104. package/dist/application/session-create-service.js.map +1 -1
  105. package/dist/application/session-resolver.js +80 -34
  106. package/dist/application/session-resolver.js.map +1 -1
  107. package/dist/application/session-resume-service.js +4 -3
  108. package/dist/application/session-resume-service.js.map +1 -1
  109. package/dist/application/source-boundary-policy.js +483 -0
  110. package/dist/application/source-boundary-policy.js.map +1 -0
  111. package/dist/application/status-service.js +1 -0
  112. package/dist/application/status-service.js.map +1 -1
  113. package/dist/application/workline-index-service.js +42 -20
  114. package/dist/application/workline-index-service.js.map +1 -1
  115. package/dist/application/workspace-migrate/apply.js +2 -4
  116. package/dist/application/workspace-migrate/apply.js.map +1 -1
  117. package/dist/application/workspace-migrate/plan.js +9 -6
  118. package/dist/application/workspace-migrate/plan.js.map +1 -1
  119. package/dist/application/workspace-migrate/preview.js +3 -3
  120. package/dist/application/workspace-migrate/preview.js.map +1 -1
  121. package/dist/application/worktree-service.js +3 -0
  122. package/dist/application/worktree-service.js.map +1 -1
  123. package/dist/cli/commands/capability.js +124 -5
  124. package/dist/cli/commands/capability.js.map +1 -1
  125. package/dist/cli/commands/designs.js +2 -6
  126. package/dist/cli/commands/designs.js.map +1 -1
  127. package/dist/cli/commands/dev-only.js +1 -1
  128. package/dist/cli/commands/dev-only.js.map +1 -1
  129. package/dist/cli/commands/flow.js +3 -1
  130. package/dist/cli/commands/flow.js.map +1 -1
  131. package/dist/cli/commands/index.js +0 -2
  132. package/dist/cli/commands/index.js.map +1 -1
  133. package/dist/cli/commands/mcp.js +53 -55
  134. package/dist/cli/commands/mcp.js.map +1 -1
  135. package/dist/cli/commands/persist.js +27 -21
  136. package/dist/cli/commands/persist.js.map +1 -1
  137. package/dist/cli/commands/resume.js +2 -2
  138. package/dist/cli/commands/resume.js.map +1 -1
  139. package/dist/cli/commands/session-create.js +2 -1
  140. package/dist/cli/commands/session-create.js.map +1 -1
  141. package/dist/cli/help-groups.js +0 -1
  142. package/dist/cli/help-groups.js.map +1 -1
  143. package/dist/cli/main.js +0 -0
  144. package/dist/cli/main.js.map +1 -1
  145. package/dist/cli/parser.js +1 -0
  146. package/dist/cli/parser.js.map +1 -1
  147. package/dist/cli/tui/data/recommended-skills.js +2 -2
  148. package/dist/cli/tui/data/recommended-skills.js.map +1 -1
  149. package/dist/cli/tui/tabs/mcp-tab-helpers.js +1 -1
  150. package/dist/cli/tui/tabs/mcp-tab.js +2 -2
  151. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  152. package/dist/domain/capability/protocol.js.map +1 -1
  153. package/dist/domain/correlative.js +121 -0
  154. package/dist/domain/correlative.js.map +1 -0
  155. package/dist/domain/design/capability.js +37 -3
  156. package/dist/domain/design/capability.js.map +1 -1
  157. package/dist/domain/design/direct.js.map +1 -1
  158. package/dist/domain/design/simple.js +33 -1
  159. package/dist/domain/design/simple.js.map +1 -1
  160. package/dist/domain/docs-canon.js +31 -0
  161. package/dist/domain/docs-canon.js.map +1 -0
  162. package/dist/domain/flow/answer.js +57 -1
  163. package/dist/domain/flow/answer.js.map +1 -1
  164. package/dist/domain/flow/authority.js +28 -29
  165. package/dist/domain/flow/authority.js.map +1 -1
  166. package/dist/domain/flow/execution-result.js +13 -1
  167. package/dist/domain/flow/execution-result.js.map +1 -1
  168. package/dist/domain/flow/run-state.js.map +1 -1
  169. package/dist/domain/mcp-entry.js +7 -12
  170. package/dist/domain/mcp-entry.js.map +1 -1
  171. package/dist/domain/retirement/selector.js +16 -14
  172. package/dist/domain/retirement/selector.js.map +1 -1
  173. package/dist/domain/source-boundary.js +3 -0
  174. package/dist/domain/source-boundary.js.map +1 -0
  175. package/dist/domain/workline-node.js +12 -5
  176. package/dist/domain/workline-node.js.map +1 -1
  177. package/dist/runtime/namespace-resolver.js +0 -3
  178. package/dist/runtime/namespace-resolver.js.map +1 -1
  179. package/package.json +5 -5
  180. package/skills/w/SKILL.md +2 -2
  181. package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +14 -9
  182. package/skills/w/artifacts/artifacts-research/CONCLUSIONS.md +7 -0
  183. package/skills/w/commands/export-scripts.md +8 -2
  184. package/skills/w/commands/plan-new.md +1 -0
  185. package/skills/w/context/MANIFEST.json +2 -9
  186. package/skills/w/exports/export-scripts/EXPORT.md +13 -9
  187. package/skills/w/loops/CHASSIS.md +1 -1
  188. package/skills/w/loops/plan-exec-loop/LOOP.md +14 -10
  189. package/skills/w/loops/plan-new-loop/LOOP.md +16 -16
  190. package/skills/w/loops/plan-refine-loop/LOOP.md +6 -1
  191. package/skills/w/loops/quick-loop/LOOP.md +2 -2
  192. package/skills/w/loops/spec-refine-loop/LOOP.md +4 -0
  193. package/skills/w/modules/DB-RESEARCH-RULE.md +5 -5
  194. package/skills/w/modules/DB-SCRIPTS-ONLY.md +5 -6
  195. package/skills/w/modules/EXEC-DB-POLICY.md +2 -2
  196. package/skills/w/modules/PLAN-INPUT.md +11 -9
  197. package/skills/w/roles/sql/ROLE.md +9 -9
  198. package/dist/application/decisiones-service.js +0 -31
  199. package/dist/application/decisiones-service.js.map +0 -1
  200. package/dist/application/dev-bootstrap-dsn-service.js +0 -76
  201. package/dist/application/dev-bootstrap-dsn-service.js.map +0 -1
  202. package/dist/application/profile/profile-service.js +0 -199
  203. package/dist/application/profile/profile-service.js.map +0 -1
  204. package/dist/application/tasks-service.js +0 -45
  205. package/dist/application/tasks-service.js.map +0 -1
  206. package/dist/cli/commands/bootstrap-dsn.js +0 -16
  207. package/dist/cli/commands/bootstrap-dsn.js.map +0 -1
  208. package/dist/cli/tui/components/process-list.js +0 -43
  209. package/dist/cli/tui/components/process-list.js.map +0 -1
@@ -20,15 +20,19 @@ import { DESIGN_DESCRIPTOR, DESIGN_OPERATIONS } from "../../domain/design/capabi
20
20
  import { attainedMaturity, isIndexable, resolveOutputRoot, } from "../../domain/design/direct.js";
21
21
  import { deriveStructuralSignals, judgeExpansion, } from "../../domain/design/expansion.js";
22
22
  import { DESIGN_MANIFEST_FILE, DESIGN_MANIFEST_SCHEMA_ID, } from "../../domain/design/manifest.js";
23
+ import { PROJECTIONS } from "../../domain/design/naming.js";
23
24
  import { DESIGN_ADAPTERS } from "../../domain/design/profiles.js";
24
- import { SIMPLE_CORE_SECTIONS, SIMPLE_DESIGN_FILE, SIMPLE_SECTIONS, designFolder, designSlug, nextPackageId, } from "../../domain/design/simple.js";
25
+ import { SIMPLE_CORE_SECTIONS, SIMPLE_DESIGN_FILE, SIMPLE_SECTIONS, designFolder, designSlug, nextPackageId, simpleMaturity, validateSimpleDesign, } from "../../domain/design/simple.js";
25
26
  import { classifySource, reportSources, } from "../../domain/design/sources.js";
26
- import { baseDigest } from "../../domain/proposal.js";
27
+ import { DEFAULT_CORE_DOCS_CANON } from "../../domain/docs-canon.js";
27
28
  import { localDateIso } from "../dates.js";
29
+ import { readConsumerDocument } from "../design/consumer-document.js";
30
+ import { currentEntries, gatePackageContent } from "../design/design-content-gate-service.js";
28
31
  import { readDesignIndex, resolveDesignPackage, } from "../design/design-index-service.js";
29
32
  import { buildPackageCandidate, } from "../design/design-publish-service.js";
30
33
  import { checkRecordPrecondition } from "../design/design-record-service.js";
31
34
  import { buildSimpleProposal, resolveSimpleTarget, } from "../design/design-simple-service.js";
35
+ import { resolveCoreDocsCanon } from "../docs-canon-service.js";
32
36
  import { buildSemanticRequest, parseSemanticResponse } from "../semantic-operation/protocol.js";
33
37
  import { registerCapability } from "./dispatcher.js";
34
38
  /** Artefact ceilings for one authored revision. Generous, and still a ceiling. */
@@ -78,25 +82,42 @@ async function validatePackage(ctx) {
78
82
  },
79
83
  };
80
84
  }
85
+ // The SAME content gate `aw designs` runs. Judging a package with the
86
+ // structural check alone answered `handoff` for a tree the listing then
87
+ // rejected — the verdict and the listing have to be the same verdict.
88
+ const content = await gatePackageContent(ctx.fs, ctx.workspace, found);
89
+ const failures = [...found.failures, ...content];
90
+ const ok = found.ok && content.length === 0;
81
91
  const validations = [
82
92
  {
83
93
  id: "design-manifest",
84
94
  passed: found.ok,
85
95
  detail: found.ok ? null : found.failures.map((f) => `${f.code}: ${f.message}`).join("; "),
86
96
  },
97
+ {
98
+ id: "design-content",
99
+ passed: content.length === 0,
100
+ detail: content.length === 0 ? null : content.map((f) => `${f.code}: ${f.message}`).join("; "),
101
+ },
87
102
  ];
88
103
  const report = reportSources([], `${id}`);
89
104
  const simple = found.mode === "simple";
90
- const maturity = attainedMaturity(requestedMaturity(ctx), found.ok ? "handoff" : "outline", report);
105
+ const gate = ok
106
+ ? await publishedMaturity(ctx.fs, ctx.workspace, found)
107
+ : { attained: "outline", reasons: failures.map((f) => f.message) };
108
+ const maturity = attainedMaturity(requestedMaturity(ctx), gate.attained, report);
91
109
  const fields = {
92
110
  package: found.id,
93
111
  baseline: found.current_baseline === null
94
112
  ? null
95
113
  : { revision: found.current_baseline.revision, digest: found.current_baseline.digest },
114
+ // A verdict publishes nothing, so there is nothing it could have failed to
115
+ // seal: the package's own baseline above is the whole answer.
116
+ unsealed: null,
96
117
  path: found.path,
97
118
  root: "workspace",
98
119
  indexable: true,
99
- maturity: { requested: requestedMaturity(ctx), attained: simple ? null : maturity.attained },
120
+ maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
100
121
  sources: [],
101
122
  renditions: [],
102
123
  // Judging an existing design reports the route it IS, not one this attempt
@@ -108,7 +129,7 @@ async function validatePackage(ctx) {
108
129
  kind: "completed",
109
130
  validations,
110
131
  output: {
111
- value: { design: fields, ok: found.ok, failures: found.failures },
132
+ value: { design: fields, ok, failures, gaps: [...gate.reasons, ...maturity.gaps] },
112
133
  reference: found.id === null || found.current_baseline === null
113
134
  ? null
114
135
  : {
@@ -146,6 +167,59 @@ async function authoring(ctx) {
146
167
  },
147
168
  };
148
169
  }
170
+ // Outside a workspace every destination this route could declare is ABSOLUTE,
171
+ // and the write boundary only admits workspace-relative paths inside the
172
+ // declared ones: an absolute answer is refused for being absolute and a
173
+ // relative one for falling outside them. No answer exists, so publishing the
174
+ // contract burns an authoring round over a question with no valid reply — and
175
+ // `apply` demands a workspace anyway, so it could never land either. The
176
+ // refusal belongs here, where the root is decided, and not one stage later
177
+ // where it reads as the author's mistake.
178
+ if (ctx.workspace === null) {
179
+ return {
180
+ kind: "blocked",
181
+ failure: {
182
+ code: "DESIGN_WORKSPACE_ABSENT",
183
+ message: `fuera de un workspace los destinos de '${ctx.operation.name}' son absolutos y ninguna respuesta puede caer dentro de ellos`,
184
+ action: "corré la operación dentro del workspace donde debe quedar el diseño: 'target' acota la carpeta DENTRO del workspace, no publica fuera de él",
185
+ },
186
+ };
187
+ }
188
+ const workspace = ctx.workspace;
189
+ const consumerInput = inputOf(ctx, "consumer_document");
190
+ let docsCanon = DEFAULT_CORE_DOCS_CANON;
191
+ // A compound design publication changes a spec or plan in the same durable
192
+ // effect. Resolve the shared documentary roots only when this operation has a
193
+ // consumer (or is required to carry one), so a package-only design remains
194
+ // independent while a consumer can never use a layout custody/retirement do
195
+ // not share.
196
+ if (consumerInput !== undefined || requiresConsumerDocument(ctx)) {
197
+ const resolved = await resolveCoreDocsCanon(ctx.fs, ctx.paths);
198
+ if (!resolved.ok) {
199
+ return {
200
+ kind: "blocked",
201
+ failure: {
202
+ code: "DOCS_CANON_INVALID",
203
+ message: resolved.error,
204
+ action: "corregí [docs] para conservar el layout documental canónico antes de publicar",
205
+ },
206
+ };
207
+ }
208
+ docsCanon = resolved.canon;
209
+ }
210
+ const consumer = readConsumerDocument(consumerInput, docsCanon);
211
+ if (!consumer.ok)
212
+ return { kind: "blocked", failure: consumer.failure };
213
+ if (requiresConsumerDocument(ctx) && consumer.value === null) {
214
+ return {
215
+ kind: "blocked",
216
+ failure: {
217
+ code: "DESIGN_CONSUMER_REQUIRED",
218
+ message: "esta publicación compuesta fija un baseline nuevo y necesita el documento consumidor final",
219
+ action: "pasá 'consumer_document' como attachment con los bytes finales, su path de spec/plan y el digest base",
220
+ },
221
+ };
222
+ }
149
223
  const precondition = await operationPrecondition(ctx);
150
224
  if (precondition !== null)
151
225
  return { kind: "blocked", failure: precondition };
@@ -166,7 +240,7 @@ async function authoring(ctx) {
166
240
  // what a valid answer looks like, and where it may land, are different on the
167
241
  // two routes, so asking first and classifying afterwards would publish a
168
242
  // contract for a route the run is not on.
169
- const route = await decideRoute(ctx, sources, root.value);
243
+ const route = await decideRoute(ctx, workspace, sources, root.value);
170
244
  if (!route.ok)
171
245
  return { kind: "blocked", failure: route.failure };
172
246
  const request = buildSemanticRequest({
@@ -204,9 +278,14 @@ async function authoring(ctx) {
204
278
  if (!parsed.ok)
205
279
  return { kind: "blocked", failure: parsed.failure };
206
280
  const answered = parsed.value.artifacts ?? [];
207
- return route.value.target === null
208
- ? packageProposal(ctx, report, route.value, answered)
209
- : simpleProposal(ctx, report, route.value, answered);
281
+ const { target } = route.value;
282
+ if (target.mode === "simple") {
283
+ return simpleProposal(ctx, workspace, report, route.value, target.simple, answered, consumer.value, docsCanon);
284
+ }
285
+ if (target.mode === "package") {
286
+ return packageProposal(ctx, workspace, report, route.value, target.package, answered, consumer.value, docsCanon);
287
+ }
288
+ return projectionProposal(ctx, workspace, report, route.value, target.projection, answered);
210
289
  }
211
290
  /**
212
291
  * Which route this attempt runs on, and everything that follows from it.
@@ -217,13 +296,17 @@ async function authoring(ctx) {
217
296
  * or a structural one somebody typed comes back as a rejection in the verdict
218
297
  * rather than as an expansion nobody can explain.
219
298
  */
220
- async function decideRoute(ctx, sources, root) {
221
- const index = ctx.workspace === null ? null : await readDesignIndex(ctx.fs, ctx.workspace);
299
+ async function decideRoute(ctx, workspace, sources, root) {
300
+ // Always readable, never null: a workspace with no `docs/designs/` yet answers
301
+ // an EMPTY index, and outside a workspace `authoring` already refused. A
302
+ // nullable index here used to carry a third state that every branch below had
303
+ // to restate and that no invocation could reach.
304
+ const index = await readDesignIndex(ctx.fs, workspace);
222
305
  // By IDENTITY and only when one was named. `find(p => p.id === null)` would
223
306
  // match the first package whose manifest does not validate — an entry that has
224
307
  // no identity is not the one this invocation continues.
225
308
  const named = packageInput(ctx);
226
- const targeted = index === null || named === null ? null : (index.packages.find((p) => p.id === named) ?? null);
309
+ const targeted = named === null ? null : (index.packages.find((p) => p.id === named) ?? null);
227
310
  const verdict = judgeExpansion(declaredExpansionSignals(ctx), deriveStructuralSignals({
228
311
  sensitiveSources: ctx.request.policy.sensitive_sources === true,
229
312
  externalTransmission: ctx.request.policy.external_transmission === true,
@@ -232,9 +315,9 @@ async function decideRoute(ctx, sources, root) {
232
315
  (targeted?.manifest?.governance.revocations.length ?? 0),
233
316
  publishedRevisions: targeted?.manifest?.baselines.length ?? 0,
234
317
  }));
235
- // The package route is also the only one available outside a workspace or
236
- // outside `docs/designs/`: a simple design derives its identity from the index,
237
- // and there is no index to derive it from.
318
+ // The package route is also the only one available outside `docs/designs/`: a
319
+ // simple design derives its identity from the index, and a root the index does
320
+ // not cover has none to derive from.
238
321
  //
239
322
  // `render` and `record` are package operations whatever the signals say —
240
323
  // projecting revisions and sealing governance decisions are things a catalog
@@ -242,13 +325,13 @@ async function decideRoute(ctx, sources, root) {
242
325
  // simple design for a maturity and a rendition it never had.
243
326
  if (verdict.mode === "package" ||
244
327
  !AUTHORING_OPERATIONS.includes(ctx.operation.name) ||
245
- index === null ||
246
328
  !isIndexable(root)) {
247
329
  return packageRoute(ctx, verdict, index, root);
248
330
  }
249
331
  const resolved = resolveSimpleTarget(index, ctx.operation.name, {
250
332
  title: textInput(ctx, "title"),
251
333
  packageId: packageInput(ctx),
334
+ root: root.root,
252
335
  });
253
336
  if (!resolved.ok) {
254
337
  const { code, message, action } = resolved.failure;
@@ -269,49 +352,32 @@ async function decideRoute(ctx, sources, root) {
269
352
  // The exact file, not its folder: on the simple route the CLI already knows
270
353
  // the one destination, so anything else is not a design it can publish.
271
354
  destinations: [`${resolved.value.path}/${SIMPLE_DESIGN_FILE}`],
272
- target: resolved.value,
273
- packageTarget: null,
355
+ target: { mode: "simple", simple: resolved.value },
274
356
  root,
275
357
  },
276
358
  };
277
359
  }
278
360
  /**
279
- * The package route: sealed when the target can be derived, verbatim when not.
361
+ * The package route. It seals, or it does not run.
280
362
  *
281
- * The compare-and-swap base is the caller's claim on this route, and a safety
282
- * check that can be omitted is one nobody performs. It is demanded HERE rather
283
- * than in the descriptor because the simple route derives it instead.
363
+ * There used to be a third outcome, declared in the code as a known limitation:
364
+ * when the target could not be derived the answer was written EXACTLY as
365
+ * authored no baseline, no manifest, no gate and the receipt said
366
+ * `completed` anyway. The measured effect was a tree `aw designs` refuses right
367
+ * afterwards, which only a hand check ever discovers. An operation that did not
368
+ * happen is a better outcome than a dossier nobody can read, so what used to be
369
+ * a silent half-write is now either a sealed publication or a refusal that names
370
+ * what is missing.
284
371
  */
285
372
  function packageRoute(ctx, verdict, index, root) {
286
- if (ctx.operation.name === "update" && textInput(ctx, "base") === null) {
287
- return {
288
- ok: false,
289
- failure: {
290
- code: "DESIGN_FIELD_INVALID",
291
- message: "actualizar un package declara sobre qué revisión se preparó",
292
- action: "pasá 'base' con la revisión vigente (por ejemplo DES-001@r3), o null si el package no publicó ninguna",
293
- },
294
- };
373
+ // `render` and `record` author no normative content: a projection is derived
374
+ // from the manifest and a governance decision decides ON a baseline. Minting
375
+ // no revision is their NATURE, not a defect, so they get their own route
376
+ // instead of the refusal that briefly made both unreachable.
377
+ if (!AUTHORING_OPERATIONS.includes(ctx.operation.name)) {
378
+ return projectionRoute(ctx, verdict, index, root);
295
379
  }
296
- // KNOWN LIMITATION: `render`/`record` — and a create/update with no index to
297
- // derive from (outside a workspace or outside `docs/designs/`) — still run
298
- // the verbatim path: the answer is published as authored, with no derived
299
- // manifest, baseline or gate. Only create/update over the index seal.
300
- if (index === null || !AUTHORING_OPERATIONS.includes(ctx.operation.name) || !isIndexable(root)) {
301
- return {
302
- ok: true,
303
- value: {
304
- verdict,
305
- contract: contractFor(ctx.operation.name),
306
- inventory: { root: root.root, mode: root.kind },
307
- destinations: [root.root],
308
- target: null,
309
- packageTarget: null,
310
- root,
311
- },
312
- };
313
- }
314
- const resolved = resolvePackageTarget(ctx, index);
380
+ const resolved = resolvePackageTarget(ctx, index, root);
315
381
  if (!resolved.ok)
316
382
  return { ok: false, failure: resolved.failure };
317
383
  const target = resolved.value;
@@ -330,67 +396,161 @@ function packageRoute(ctx, verdict, index, root) {
330
396
  // The package folder, not the taxonomy root: the destination check is
331
397
  // segment-based, so every artifact lands INSIDE this package or nowhere.
332
398
  destinations: [target.path],
333
- target: null,
334
- packageTarget: target,
399
+ target: { mode: "package", package: target },
335
400
  root,
336
401
  },
337
402
  };
338
403
  }
404
+ /** Why each non-authoring operation mints no revision, said in its own receipt. */
405
+ const UNSEALED_CAUSE = {
406
+ render: "'render' regenera proyecciones: las deriva el CLI del manifest y ningún baseline las selecciona, así que no hay revisión que acuñar",
407
+ record: "'record' decide SOBRE una revisión que ya existe: sella una decisión de gobierno y no acuña una línea base nueva",
408
+ };
339
409
  /**
340
- * The package route's target, derived BEFORE the contract is published.
410
+ * The route of an operation that publishes WITHOUT minting a revision.
341
411
  *
342
- * `create` mints the identity and the folder from the title, over an initial
343
- * manifest that exists only to give the candidate builder a line to start from.
344
- * `update` locates the package BY IDENTITY and checks the declared base against
345
- * the line in force — the same compare-and-swap `publishDesignRevision` runs,
346
- * moved to the moment the contract is fixed instead of discovered mid-publish.
412
+ * It writes inside a package the index already carries, and refuses when there
413
+ * is none. The two halves are one rule: with a manifest already there the tree
414
+ * stays readable `aw designs` accepts afterwards exactly what it accepted
415
+ * before, because nothing sealed moves and without one the files would land in
416
+ * a folder the listing then rejects for having no manifest, which is the
417
+ * illegible tree this plan exists to stop.
347
418
  */
348
- function resolvePackageTarget(ctx, index) {
349
- if (ctx.operation.name === "create") {
350
- const title = textInput(ctx, "title");
351
- if (title === null) {
352
- return {
353
- ok: false,
354
- failure: {
355
- code: "DESIGN_FIELD_INVALID",
356
- message: "un package nuevo necesita un título",
357
- action: "pasá 'title' con el nombre humano del diseño: de ahí salen la carpeta y el id",
358
- },
359
- };
360
- }
361
- const packageId = nextPackageId(index.packages.map((p) => p.id ?? p.declared_id));
419
+ function projectionRoute(ctx, verdict, index, root) {
420
+ const operation = ctx.operation.name;
421
+ const named = packageInput(ctx);
422
+ if (named === null) {
362
423
  return {
363
- ok: true,
364
- value: {
365
- packageId,
366
- path: designFolder(index.root, packageId, designSlug(title)),
367
- revision: 1,
368
- manifest: initialPackageManifest(packageId, title, localDateIso(new Date())),
424
+ ok: false,
425
+ failure: {
426
+ code: "DESIGN_FIELD_INVALID",
427
+ message: `'${operation}' escribe dentro de un package que ya existe y no se declaró cuál`,
428
+ action: "pasá 'package' con su id, por ejemplo DES-007",
369
429
  },
370
430
  };
371
431
  }
372
- const named = packageInput(ctx);
373
- if (named === null) {
432
+ const located = locatePackage(index, named);
433
+ if (!located.ok)
434
+ return { ok: false, failure: located.failure };
435
+ const entry = located.value;
436
+ const target = {
437
+ entry,
438
+ unsealed: UNSEALED_CAUSE[operation] ?? `'${operation}' no acuña una revisión`,
439
+ };
440
+ return {
441
+ ok: true,
442
+ value: {
443
+ verdict,
444
+ contract: projectionContract(operation, target),
445
+ inventory: {
446
+ root: root.root,
447
+ mode: entry.manifest.mode,
448
+ package: entry.manifest.id,
449
+ path: entry.path,
450
+ // Consultative and load-bearing: an author who does not know the answer
451
+ // will not be sealed writes a revision nobody asked for.
452
+ seals: false,
453
+ },
454
+ // The package folder the INDEX reports, not the root the invocation named:
455
+ // this operation writes into a package that already exists, wherever it is.
456
+ destinations: [entry.path],
457
+ target: { mode: "projection", projection: target },
458
+ root,
459
+ },
460
+ };
461
+ }
462
+ function resolvePackageTarget(ctx, index, root) {
463
+ return ctx.operation.name === "create"
464
+ ? mintPackageTarget(ctx, index, root)
465
+ : continuePackageTarget(ctx, index);
466
+ }
467
+ /** A brand-new package: the identity and the folder, from the title and the root. */
468
+ function mintPackageTarget(ctx, index, root) {
469
+ const title = textInput(ctx, "title");
470
+ if (title === null) {
374
471
  return {
375
472
  ok: false,
376
473
  failure: {
377
474
  code: "DESIGN_FIELD_INVALID",
378
- message: "actualizar un package necesita la identidad del que se continúa",
379
- action: "pasá 'package' con su id, por ejemplo DES-007",
475
+ message: "un package nuevo necesita un título",
476
+ action: "pasá 'title' con el nombre humano del diseño: de ahí salen la carpeta y el id",
477
+ },
478
+ };
479
+ }
480
+ const packageId = nextPackageId(index.packages.map((p) => p.id ?? p.declared_id));
481
+ return {
482
+ ok: true,
483
+ value: {
484
+ packageId,
485
+ // The DECLARED root, not the index's: a `target` that narrows where the
486
+ // package lands has to be where it lands, or the folder and the
487
+ // destination allowlist the request publishes disagree.
488
+ path: designFolder(root.root, packageId, designSlug(title)),
489
+ revision: 1,
490
+ manifest: initialPackageManifest(packageId, title, localDateIso(new Date())),
491
+ manifest_base: null,
492
+ },
493
+ };
494
+ }
495
+ function locatePackage(index, named) {
496
+ // By identity, and never by the FIRST match. Two packages claiming one id
497
+ // break every reference to it, and picking whichever the walk reached first
498
+ // would write into one of them at random.
499
+ const claiming = index.packages.filter((p) => p.id === named || p.declared_id === named);
500
+ if (claiming.length > 1) {
501
+ return {
502
+ ok: false,
503
+ failure: {
504
+ code: "DESIGN_REFERENCE_AMBIGUOUS",
505
+ message: `${named} está declarado por ${claiming.length} packages: ${claiming.map((p) => p.path).join(", ")}`,
506
+ action: "dos packages no pueden reclamar la misma identidad: renombrá uno y volvé a intentar",
380
507
  },
381
508
  };
382
509
  }
383
- const found = index.packages.find((p) => p.id === named) ?? null;
384
- if (found === null || found.manifest === null) {
510
+ const found = claiming[0];
511
+ if (found === undefined) {
385
512
  return {
386
513
  ok: false,
387
514
  failure: {
388
515
  code: "DESIGN_PACKAGE_NOT_FOUND",
389
- message: `no hay ningún package ${named} legible bajo ${index.root}/`,
516
+ message: `no hay ningún package ${named} bajo ${index.root}/`,
390
517
  action: `revisá 'aw designs' para ver las identidades publicadas bajo ${index.root}/`,
391
518
  },
392
519
  };
393
520
  }
521
+ // A BROKEN package is not a missing one: they are very different problems for
522
+ // whoever has to fix one, so what comes back is the manifest's own diagnosis
523
+ // and not «no existe».
524
+ if (found.manifest === null) {
525
+ const first = found.failures[0];
526
+ return {
527
+ ok: false,
528
+ failure: {
529
+ code: first?.code ?? "DESIGN_MANIFEST_MISSING",
530
+ message: `${found.manifest_path}: ${first?.message ?? "el package no tiene un manifest legible"}`,
531
+ action: first?.action ?? "reparalo antes de publicar sobre él",
532
+ },
533
+ };
534
+ }
535
+ return { ok: true, value: { ...found, manifest: found.manifest } };
536
+ }
537
+ /** The next revision of a package that exists: located by identity, base checked. */
538
+ function continuePackageTarget(ctx, index) {
539
+ const named = packageInput(ctx);
540
+ if (named === null) {
541
+ return {
542
+ ok: false,
543
+ failure: {
544
+ code: "DESIGN_FIELD_INVALID",
545
+ message: "actualizar un package necesita la identidad del que se continúa",
546
+ action: "pasá 'package' con su id, por ejemplo DES-007",
547
+ },
548
+ };
549
+ }
550
+ const located = locatePackage(index, named);
551
+ if (!located.ok)
552
+ return { ok: false, failure: located.failure };
553
+ const found = located.value;
394
554
  const manifest = found.manifest;
395
555
  const current = manifest.current_baseline;
396
556
  const actual = current === null ? null : `${manifest.id}@r${current.revision}`;
@@ -427,6 +587,7 @@ function resolvePackageTarget(ctx, index) {
427
587
  path: found.path,
428
588
  revision: (current?.revision ?? 0) + 1,
429
589
  manifest,
590
+ manifest_base: found.manifest_base,
430
591
  },
431
592
  };
432
593
  }
@@ -452,8 +613,7 @@ function initialPackageManifest(packageId, title, created) {
452
613
  };
453
614
  }
454
615
  /** The simple route's durable step: one authored document, everything else derived. */
455
- async function simpleProposal(ctx, report, route, answered) {
456
- const target = route.target;
616
+ async function simpleProposal(ctx, workspace, report, route, target, answered, consumer, docsCanon) {
457
617
  const documentPath = `${target.path}/${SIMPLE_DESIGN_FILE}`;
458
618
  const document = answered.find((a) => a.path === documentPath);
459
619
  if (answered.length !== 1 || document === undefined) {
@@ -466,12 +626,12 @@ async function simpleProposal(ctx, report, route, answered) {
466
626
  },
467
627
  };
468
628
  }
469
- // A target only exists when `decideRoute` read the index, which it only does
470
- // inside a workspace: the simple route and a null workspace cannot coexist.
471
- const built = await buildSimpleProposal(ctx.fs, ctx.workspace, {
629
+ const built = await buildSimpleProposal(ctx.fs, workspace, {
472
630
  target,
473
631
  document: document.content,
474
632
  published: localDateIso(new Date()),
633
+ consumer_document: consumer,
634
+ docs_canon: docsCanon,
475
635
  });
476
636
  if (!built.ok) {
477
637
  const first = built.failures[0];
@@ -484,14 +644,19 @@ async function simpleProposal(ctx, report, route, answered) {
484
644
  },
485
645
  };
486
646
  }
647
+ // The gate's verdict over the document, exactly as the package route takes it
648
+ // from the gate over its own: what the design attains cannot depend on which
649
+ // route wrote it.
650
+ const gate = built.value.maturity;
651
+ const maturity = attainedMaturity(requestedMaturity(ctx), gate.attained, report);
487
652
  const fields = {
488
653
  package: built.value.packageId,
489
654
  baseline: { revision: built.value.revision, digest: built.value.digest },
655
+ unsealed: null,
490
656
  path: target.path,
491
657
  root: route.root.kind,
492
658
  indexable: true,
493
- // No ladder to climb: see `DesignReceiptFields.maturity`.
494
- maturity: { requested: requestedMaturity(ctx), attained: null },
659
+ maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
495
660
  sources: report.sources,
496
661
  renditions: [],
497
662
  route: routeOf(route.verdict),
@@ -507,56 +672,23 @@ async function simpleProposal(ctx, report, route, answered) {
507
672
  value: {
508
673
  design: fields,
509
674
  artifacts: built.value.artifacts.map((a) => a.path),
510
- gaps: [],
675
+ gaps: [...gate.reasons, ...maturity.gaps],
511
676
  },
512
677
  reference: null,
513
678
  completeness: "partial",
514
679
  },
515
- base: built.value.base,
680
+ bases: proposalBases(built.value.base, consumer?.base ?? null),
516
681
  };
517
682
  }
518
683
  /**
519
- * The expanded route's durable step.
684
+ * The expanded route's durable step. The CLI owns the seal, always.
520
685
  *
521
- * With a derived target (create/update over the index) the CLI owns the seal:
522
- * the authored artifacts are candidate files, and the manifest, the baseline
523
- * and `PACKAGE.md` are DERIVED here the same candidate `publishDesignRevision`
524
- * publishes, so the gate verdict the `012` computes runs NOW, inside `validate`,
525
- * and an invalid tree is blocked before the first byte moves.
526
- *
527
- * Without one (`render`/`record`, or no index to derive from) the package is
528
- * published exactly as authored. That verbatim path is the known limitation
529
- * `decideRoute` documents, not a second contract.
686
+ * The authored artifacts are candidate files, and the manifest, the baseline
687
+ * and `PACKAGE.md` are DERIVED here the one candidate the whole system
688
+ * publishes, so the gate verdict runs NOW, inside `validate`, and an invalid
689
+ * tree is blocked before the first byte moves.
530
690
  */
531
- async function packageProposal(ctx, report, route, answered) {
532
- const target = route.packageTarget;
533
- if (target === null) {
534
- const artifacts = answered.map((a) => ({ path: a.path, content: a.content }));
535
- const maturity = attainedMaturity(requestedMaturity(ctx), "outline", report);
536
- const fields = {
537
- package: packageInput(ctx),
538
- baseline: null,
539
- path: route.root.root,
540
- root: route.root.kind,
541
- indexable: isIndexable(route.root),
542
- maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
543
- sources: report.sources,
544
- renditions: [],
545
- route: routeOf(route.verdict),
546
- };
547
- return {
548
- kind: "durable",
549
- artifacts,
550
- output: {
551
- value: { design: fields, artifacts: artifacts.map((a) => a.path), gaps: maturity.gaps },
552
- reference: null,
553
- // The durable step has not run yet, so nothing is published. Claiming
554
- // `complete` here would let a gate accept a proposal as a package.
555
- completeness: "partial",
556
- },
557
- base: null,
558
- };
559
- }
691
+ async function packageProposal(ctx, workspace, report, route, target, answered, consumer, docsCanon) {
560
692
  // From workspace-relative to package-relative, which is the vocabulary the
561
693
  // candidate builder speaks. The destination check already confined every
562
694
  // answer to the package folder, so the prefix always strips.
@@ -564,7 +696,7 @@ async function packageProposal(ctx, report, route, answered) {
564
696
  const files = [];
565
697
  for (const artifact of answered) {
566
698
  const relative = artifact.path.slice(prefix.length);
567
- if (CLI_DERIVED_FILES.includes(relative) || relative.startsWith("baselines/")) {
699
+ if (owns(DERIVED_PATHS, relative)) {
568
700
  return {
569
701
  kind: "blocked",
570
702
  failure: {
@@ -576,13 +708,13 @@ async function packageProposal(ctx, report, route, answered) {
576
708
  }
577
709
  files.push({ path: relative, content: artifact.content });
578
710
  }
579
- // The target only exists when `decideRoute` read the index, which it only
580
- // does inside a workspace.
581
- const candidate = await buildPackageCandidate(ctx.fs, ctx.workspace, {
711
+ const candidate = await buildPackageCandidate(ctx.fs, workspace, {
582
712
  manifest: target.manifest,
583
713
  packagePath: target.path,
584
714
  files,
585
715
  published: localDateIso(new Date()),
716
+ consumer_document: consumer,
717
+ docs_canon: docsCanon,
586
718
  });
587
719
  if (!candidate.ok) {
588
720
  // The gate's own verdict, with its real code and next action: this is where
@@ -597,13 +729,17 @@ async function packageProposal(ctx, report, route, answered) {
597
729
  },
598
730
  };
599
731
  }
600
- // The gate passed over the merged catalog, so the verdict a proposal may
601
- // claim is what THIS revision's flows and screens claim for themselves — the
602
- // ones already sealed were judged when they were published.
603
- const maturity = attainedMaturity(requestedMaturity(ctx), claimedMaturity(candidate.value.manifest, files), report);
732
+ // The verdict over the catalog this revision LEAVES, not over the files it
733
+ // brings. A revision of a single token introduces no document that could
734
+ // object, and judging only what it introduces answered `handoff` for a package
735
+ // whose current flow was still `outline` — a receipt the `validate` right
736
+ // afterwards contradicted about the same tree.
737
+ const gate = catalogMaturity(candidate.value.manifest);
738
+ const maturity = attainedMaturity(requestedMaturity(ctx), gate.attained, report);
604
739
  const fields = {
605
740
  package: target.packageId,
606
741
  baseline: { revision: candidate.value.revision, digest: candidate.value.baseline.digest },
742
+ unsealed: null,
607
743
  path: target.path,
608
744
  root: route.root.kind,
609
745
  indexable: isIndexable(route.root),
@@ -619,42 +755,178 @@ async function packageProposal(ctx, report, route, answered) {
619
755
  value: {
620
756
  design: fields,
621
757
  artifacts: candidate.value.artifacts.map((a) => a.path),
622
- gaps: maturity.gaps,
758
+ gaps: [...gate.reasons, ...maturity.gaps],
623
759
  },
624
760
  reference: null,
625
761
  // The durable step has not run yet, so nothing is published. Claiming
626
762
  // `complete` here would let a gate accept a proposal as a package.
627
763
  completeness: "partial",
628
764
  },
629
- base: await packageManifestBase(ctx, target),
765
+ // `target.manifest_base` was captured with the exact bytes parsed into
766
+ // `target.manifest`. A concurrent change therefore fails at apply instead
767
+ // of being accidentally adopted as this candidate's CAS base.
768
+ bases: proposalBases(target.manifest_base, consumer?.base ?? null),
630
769
  };
631
770
  }
632
- /** Package-relative file paths the CLI derives and seals — authoring one is rejected. */
633
- const CLI_DERIVED_FILES = [DESIGN_MANIFEST_FILE, "PACKAGE.md"];
634
771
  /**
635
- * The maturity this revision's own flows and screens claim, as the merged
636
- * catalog recorded it: `outline` as soon as one of them claims no more, and
637
- * `handoff` when none says otherwise the kinds without a maturity ladder
638
- * have no vote.
772
+ * The durable step of a publication that mints NO revision.
773
+ *
774
+ * What it refuses is the only way one of these operations can leave the package
775
+ * unreadable: a hand-authored manifest or baseline seals the tree with something
776
+ * nobody derived, and `aw designs` rejects it right afterwards. Everything else
777
+ * lands as authored — a projection replaces the one it regenerates, anything
778
+ * else is additive — and the receipt says, in words, that nothing was sealed.
639
779
  */
640
- function claimedMaturity(manifest, files) {
641
- const introduced = new Set(files.map((f) => f.path));
642
- const claims = [...manifest.catalog.flows, ...manifest.catalog.screens]
643
- .filter((entry) => introduced.has(entry.path))
644
- .map((entry) => entry.maturity);
645
- return claims.includes("outline") ? "outline" : "handoff";
780
+ async function projectionProposal(ctx, workspace, report, route, target, answered) {
781
+ const entry = target.entry;
782
+ const prefix = `${entry.path}/`;
783
+ const artifacts = [];
784
+ for (const artifact of answered) {
785
+ const relative = artifact.path.slice(prefix.length);
786
+ if (owns(SEALED_PATHS, relative)) {
787
+ return {
788
+ kind: "blocked",
789
+ failure: {
790
+ code: "DESIGN_FIELD_INVALID",
791
+ message: `'${artifact.path}' es lo que sella el package, y '${ctx.operation.name}' no acuña revisión`,
792
+ action: `quitalo de 'artifacts': ${DESIGN_MANIFEST_FILE} y 'baselines/' los deriva y sella una publicación de contenido normativo, con 'create' o 'update'`,
793
+ },
794
+ };
795
+ }
796
+ artifacts.push({
797
+ path: artifact.path,
798
+ content: artifact.content,
799
+ // Regenerating a projection REPLACES it — that is what regenerating means,
800
+ // and `render` declares `mutate_overwrite` for exactly this. Nothing else
801
+ // is regenerable: a governance record decides on bytes that already exist,
802
+ // so publishing over one would rewrite a decision somebody made.
803
+ overwrite: PROJECTIONS.includes(relative),
804
+ });
805
+ }
806
+ // The design's own maturity, unchanged: this publication catalogues nothing,
807
+ // so reporting anything else would credit or blame it for a verdict it did
808
+ // not move. Through the same function `validate` uses, which is what keeps a
809
+ // simple design judged by its document instead of by an empty catalog.
810
+ const gate = await publishedMaturity(ctx.fs, workspace, entry);
811
+ const maturity = attainedMaturity(requestedMaturity(ctx), gate.attained, report);
812
+ const fields = {
813
+ package: entry.manifest.id,
814
+ // Null, and SAID: `unsealed` is what turns "no hay línea base" from an
815
+ // omission into a declaration.
816
+ baseline: null,
817
+ unsealed: target.unsealed,
818
+ path: entry.path,
819
+ root: route.root.kind,
820
+ // Resolved FROM the index, so it is indexed whatever root the invocation
821
+ // happened to name.
822
+ indexable: true,
823
+ maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
824
+ sources: report.sources,
825
+ renditions: [],
826
+ route: routeOf(route.verdict),
827
+ };
828
+ return {
829
+ kind: "durable",
830
+ artifacts,
831
+ output: {
832
+ value: {
833
+ design: fields,
834
+ artifacts: artifacts.map((a) => a.path),
835
+ gaps: [...gate.reasons, ...maturity.gaps],
836
+ },
837
+ reference: null,
838
+ // Nothing is on disk until the approval lands, here as everywhere else.
839
+ completeness: "partial",
840
+ },
841
+ // Nothing to compare and swap: this publication reads no manifest to derive
842
+ // its output, so there is no state it could have been computed against.
843
+ bases: [],
844
+ };
845
+ }
846
+ /**
847
+ * Package-relative paths the CLI SEALS. Authoring one is refused on every route:
848
+ * a hand-written manifest or baseline is precisely the tree the listing rejects.
849
+ */
850
+ const SEALED_PATHS = [DESIGN_MANIFEST_FILE, "baselines"];
851
+ /** What a SEALING publication also derives for itself: the projections it renders. */
852
+ const DERIVED_PATHS = [...SEALED_PATHS, ...PROJECTIONS];
853
+ /** Is this package-relative path one of `owned` — the entry itself, or under it? */
854
+ function owns(owned, relative) {
855
+ return owned.some((path) => relative === path || relative.startsWith(`${path}/`));
856
+ }
857
+ /**
858
+ * The maturity a catalog attains as a whole.
859
+ *
860
+ * `handoff` is a property of the WHOLE thing being published: a package is
861
+ * consumed as one dossier, so the weakest CURRENT document is what an
862
+ * implementer hits. The empty case is vacuously `handoff`, and that is only
863
+ * sound because every caller derives its claims from {@link currentEntries},
864
+ * which yields exactly one entry per catalogued id: no claims means the catalog
865
+ * has no flow and no screen — no ladder to climb — rather than a filter having
866
+ * eaten the ones it has.
867
+ */
868
+ function ceilingOf(claims) {
869
+ const holding = claims.filter((c) => c.maturity !== "handoff");
870
+ if (holding.length === 0)
871
+ return { attained: "handoff", reasons: [] };
872
+ return {
873
+ attained: "outline",
874
+ reasons: holding.map((c) => `${c.subject} alcanza '${c.maturity}': una publicación vale lo que vale su artefacto más flojo`),
875
+ };
646
876
  }
647
877
  /**
648
- * The compare-and-swap base of a package proposal: the manifest as it stood
649
- * when the candidate was computed. Null when there is nothing on disk to have
650
- * moved — a create is protected by its destinations not existing.
878
+ * The maturity a package attains — ONE function, over the manifest that IS its
879
+ * catalog.
880
+ *
881
+ * The same question for the tree a publication will LEAVE (the candidate's
882
+ * manifest) and for the one already published (the entry's), so the receipt and
883
+ * the `validate` right after it cannot answer differently about the same tree.
884
+ * Which revision of each artifact answers is the content gate's own
885
+ * `currentEntries`, and reusing it is the point: reading `currentness` again
886
+ * here dropped every artifact it did not enumerate, and a manifest is allowed
887
+ * not to enumerate one.
888
+ *
889
+ * Reading the catalog rather than the files is not a shortcut: the manifest
890
+ * records the maturity each revision was sealed with — the publication gate
891
+ * refused it otherwise — and that IS the verdict in force.
651
892
  */
652
- async function packageManifestBase(ctx, target) {
653
- const path = `${target.path}/${DESIGN_MANIFEST_FILE}`;
654
- const absolute = join(ctx.workspace, path);
655
- if (!(await ctx.fs.exists(absolute)))
656
- return null;
657
- return { path, digest: baseDigest(await ctx.fs.readText(absolute)) };
893
+ function catalogMaturity(manifest) {
894
+ const claims = [...currentEntries(manifest, "flows"), ...currentEntries(manifest, "screens")].map((entry) => ({
895
+ subject: `${entry.id}@r${entry.revision}`,
896
+ maturity: entry.maturity ?? "outline",
897
+ }));
898
+ return ceilingOf(claims);
899
+ }
900
+ /**
901
+ * The gate's verdict over a design that is ALREADY published.
902
+ *
903
+ * Two shapes, one question: a simple design is judged by its own document, a
904
+ * package by its catalog. This only runs once the content gate came back clean,
905
+ * which is what makes the catalog's recorded maturities trustworthy here.
906
+ */
907
+ async function publishedMaturity(fs, workspace, entry) {
908
+ const manifest = entry.manifest;
909
+ if (manifest === null) {
910
+ return {
911
+ attained: "outline",
912
+ reasons: [`'${entry.manifest_path}' no valida: sin manifest no hay catálogo que juzgar`],
913
+ };
914
+ }
915
+ if (manifest.mode !== "simple")
916
+ return catalogMaturity(manifest);
917
+ const absolute = join(workspace, entry.path, SIMPLE_DESIGN_FILE);
918
+ if (!(await fs.exists(absolute))) {
919
+ return {
920
+ attained: "outline",
921
+ reasons: [`'${entry.path}/${SIMPLE_DESIGN_FILE}' no está: no hay documento que juzgar`],
922
+ };
923
+ }
924
+ const parsed = validateSimpleDesign(await fs.readText(absolute), SIMPLE_DESIGN_FILE);
925
+ if (!parsed.ok || parsed.value === null) {
926
+ return { attained: "outline", reasons: parsed.failures.map((f) => f.message) };
927
+ }
928
+ const verdict = simpleMaturity(parsed.value);
929
+ return { attained: verdict.attained, reasons: verdict.reasons };
658
930
  }
659
931
  /** The verdict as the receipt states it: mode, signals and the one-line cause. */
660
932
  function routeOf(verdict) {
@@ -765,18 +1037,6 @@ function simpleContract(target) {
765
1037
  "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' y 'artifacts': [{path, content}] con ese único archivo.",
766
1038
  ].join(" ");
767
1039
  }
768
- function contractFor(operation) {
769
- const shared = "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' " +
770
- "y 'artifacts': [{path, content}]. Cada 'path' es relativo al workspace y cae dentro de los " +
771
- "destinos permitidos. Ningún artefacto inventa un formato: los del UI Design Package v1 son los únicos aceptados.";
772
- const perOperation = {
773
- create: "Autorá la PRIMERA revisión del package a partir de las fuentes declaradas.",
774
- update: "Autorá la revisión SIGUIENTE sobre la base declarada. No reescribas revisiones ya selladas.",
775
- render: "Regenerá las proyecciones de la revisión indicada. Una proyección no es normativa y nunca se sella.",
776
- record: "Sellá la decisión de gobierno sobre la revisión indicada, sin tocar el contenido del package.",
777
- };
778
- return `${perOperation[operation] ?? ""} ${shared}`.trim();
779
- }
780
1040
  /**
781
1041
  * What a valid answer is on the SEALED package route: the normative artifacts,
782
1042
  * and nothing the CLI derives.
@@ -801,8 +1061,43 @@ function packageContract(operation, target) {
801
1061
  .join(" ")
802
1062
  .trim();
803
1063
  }
1064
+ /**
1065
+ * What a valid answer is on the route that mints NO revision.
1066
+ *
1067
+ * It states that first, and states it before anything else: an author who thinks
1068
+ * the answer will be sealed writes a revision, and a revision is exactly what
1069
+ * this route does not publish.
1070
+ */
1071
+ function projectionContract(operation, target) {
1072
+ const perOperation = {
1073
+ render: "Regenerá las proyecciones de la revisión vigente. Una proyección no es normativa: sale del manifest y ningún baseline la sella.",
1074
+ record: "Escribí la decisión de gobierno sobre la revisión indicada, sin tocar el contenido normativo del package.",
1075
+ };
1076
+ return [
1077
+ perOperation[operation] ?? "",
1078
+ `Esta operación NO acuña una revisión: ${target.unsealed}.`,
1079
+ `Se escribe DENTRO de '${target.entry.path}', el package ${target.entry.manifest.id} que ya está indexado.`,
1080
+ `NO autores '${DESIGN_MANIFEST_FILE}' ni nada bajo 'baselines/': son lo que sella el package y solo los deriva una publicación de contenido normativo.`,
1081
+ "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' y 'artifacts': [{path, content}]. Cada 'path' es relativo al workspace y cae dentro de ese package.",
1082
+ ]
1083
+ .join(" ")
1084
+ .trim();
1085
+ }
1086
+ function inputOf(ctx, name) {
1087
+ return ctx.request.inputs.find((i) => i.name === name);
1088
+ }
804
1089
  function inputValue(ctx, name) {
805
- return ctx.request.inputs.find((i) => i.name === name)?.value;
1090
+ return inputOf(ctx, name)?.value;
1091
+ }
1092
+ /** A composed refine replaces its consumer in the same approved publication. */
1093
+ function requiresConsumerDocument(ctx) {
1094
+ return (AUTHORING_OPERATIONS.includes(ctx.operation.name) &&
1095
+ ctx.request.caller.route === "compose" &&
1096
+ (ctx.request.caller.flow === "spec-refine" || ctx.request.caller.flow === "plan-refine"));
1097
+ }
1098
+ /** One proposal owns every non-null compare-and-swap base it depends on. */
1099
+ function proposalBases(...candidates) {
1100
+ return candidates.filter((candidate) => candidate !== null);
806
1101
  }
807
1102
  /** The five operations this floor answers — the descriptor's, not a second list. */
808
1103
  export const DESIGN_FLOOR_OPERATIONS = DESIGN_OPERATIONS;