@tacuchi/agent-workflow-cli 21.6.0 → 21.7.1
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/application/artifacts-service.js +10 -0
- package/dist/application/artifacts-service.js.map +1 -1
- package/dist/application/capability/design-handler.js +532 -22
- package/dist/application/capability/design-handler.js.map +1 -1
- package/dist/application/capability/dispatcher.js +20 -6
- package/dist/application/capability/dispatcher.js.map +1 -1
- package/dist/application/capability/durable-effect.js +23 -157
- package/dist/application/capability/durable-effect.js.map +1 -1
- package/dist/application/checkpoint-service.js +49 -3
- package/dist/application/checkpoint-service.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +5 -0
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/design/design-content-gate-service.js +134 -0
- package/dist/application/design/design-content-gate-service.js.map +1 -0
- package/dist/application/design/design-gate-service.js +18 -0
- package/dist/application/design/design-gate-service.js.map +1 -1
- package/dist/application/design/design-index-service.js +2 -0
- package/dist/application/design/design-index-service.js.map +1 -1
- package/dist/application/design/design-publish-service.js +91 -19
- package/dist/application/design/design-publish-service.js.map +1 -1
- package/dist/application/design/design-resolver-service.js +18 -1
- package/dist/application/design/design-resolver-service.js.map +1 -1
- package/dist/application/design/design-simple-service.js +217 -0
- package/dist/application/design/design-simple-service.js.map +1 -0
- package/dist/application/flow/advance.js +96 -11
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/flow-service.js +12 -1
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +221 -0
- package/dist/application/flow/internal-actions.js.map +1 -0
- package/dist/application/flow/internal-drive.js +293 -0
- package/dist/application/flow/internal-drive.js.map +1 -0
- package/dist/application/flow/submit.js +242 -84
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/local-proposal.js +171 -0
- package/dist/application/local-proposal.js.map +1 -0
- package/dist/application/parsers/decisiones.js +13 -0
- package/dist/application/parsers/decisiones.js.map +1 -1
- package/dist/application/paths-service.js +11 -1
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/resume-service.js +22 -26
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/semantic-operation/protocol.js +14 -4
- package/dist/application/semantic-operation/protocol.js.map +1 -1
- package/dist/application/session-artifacts.js +53 -0
- package/dist/application/session-artifacts.js.map +1 -1
- package/dist/application/session-close-service.js +5 -0
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-narrative.js +294 -0
- package/dist/application/session-narrative.js.map +1 -0
- package/dist/application/status-service.js +1 -0
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/workline-index-service.js +2 -0
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/cli/commands/capability.js +1 -1
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/designs.js +29 -5
- package/dist/cli/commands/designs.js.map +1 -1
- package/dist/cli/commands/flow.js +14 -0
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/session-artifacts.js +55 -2
- package/dist/cli/commands/session-artifacts.js.map +1 -1
- package/dist/cli/parser.js +1 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/domain/design/capability.js +35 -4
- package/dist/domain/design/capability.js.map +1 -1
- package/dist/domain/design/direct.js.map +1 -1
- package/dist/domain/design/expansion.js +137 -0
- package/dist/domain/design/expansion.js.map +1 -0
- package/dist/domain/design/manifest.js +61 -8
- package/dist/domain/design/manifest.js.map +1 -1
- package/dist/domain/design/reference.js +57 -10
- package/dist/domain/design/reference.js.map +1 -1
- package/dist/domain/design/simple.js +164 -0
- package/dist/domain/design/simple.js.map +1 -0
- package/dist/domain/flow/answer.js +90 -12
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +266 -60
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/authorization.js +36 -15
- package/dist/domain/flow/authorization.js.map +1 -1
- package/dist/domain/flow/directive.js +10 -6
- package/dist/domain/flow/directive.js.map +1 -1
- package/dist/domain/flow/execution-result.js +72 -0
- package/dist/domain/flow/execution-result.js.map +1 -0
- package/dist/domain/flow/run-state.js +165 -13
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/host-verification.js +8 -8
- package/dist/domain/host-verification.js.map +1 -1
- package/dist/domain/proposal.js +91 -0
- package/dist/domain/proposal.js.map +1 -0
- package/dist/domain/session/narrative.js +126 -0
- package/dist/domain/session/narrative.js.map +1 -0
- package/package.json +1 -1
- package/skills/w/SKILL.md +1 -1
- package/skills/w/loops/CHASSIS.md +1 -1
- package/skills/w/loops/plan-new-loop/LOOP.md +5 -6
- package/skills/w/loops/plan-refine-loop/LOOP.md +7 -7
- package/skills/w/loops/spec-refine-loop/LOOP.md +6 -4
- package/skills/w/modules/DESIGN-REFERENCES.md +50 -56
- package/skills/w/roles/design/CONTRACT.md +73 -6
- package/skills/w/roles/design/ROLE.md +21 -3
- package/skills/w/schemas/design/design-manifest.v1.schema.json +4 -0
|
@@ -14,17 +14,27 @@
|
|
|
14
14
|
* `create` cannot invent a package body, so it says what a valid answer must
|
|
15
15
|
* contain and waits for one — that is a question, not a failure.
|
|
16
16
|
*/
|
|
17
|
+
import { join } from "node:path";
|
|
17
18
|
import { requireAdapter } from "../../domain/design/adapter.js";
|
|
18
19
|
import { DESIGN_DESCRIPTOR, DESIGN_OPERATIONS } from "../../domain/design/capability.js";
|
|
19
20
|
import { attainedMaturity, isIndexable, resolveOutputRoot, } from "../../domain/design/direct.js";
|
|
21
|
+
import { deriveStructuralSignals, judgeExpansion, } from "../../domain/design/expansion.js";
|
|
22
|
+
import { DESIGN_MANIFEST_FILE, DESIGN_MANIFEST_SCHEMA_ID, } from "../../domain/design/manifest.js";
|
|
20
23
|
import { DESIGN_ADAPTERS } from "../../domain/design/profiles.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
24
|
+
import { SIMPLE_CORE_SECTIONS, SIMPLE_DESIGN_FILE, SIMPLE_SECTIONS, designFolder, designSlug, nextPackageId, } from "../../domain/design/simple.js";
|
|
25
|
+
import { classifySource, reportSources, } from "../../domain/design/sources.js";
|
|
26
|
+
import { baseDigest } from "../../domain/proposal.js";
|
|
27
|
+
import { localDateIso } from "../dates.js";
|
|
28
|
+
import { readDesignIndex, resolveDesignPackage, } from "../design/design-index-service.js";
|
|
29
|
+
import { buildPackageCandidate, } from "../design/design-publish-service.js";
|
|
23
30
|
import { checkRecordPrecondition } from "../design/design-record-service.js";
|
|
31
|
+
import { buildSimpleProposal, resolveSimpleTarget, } from "../design/design-simple-service.js";
|
|
24
32
|
import { buildSemanticRequest, parseSemanticResponse } from "../semantic-operation/protocol.js";
|
|
25
33
|
import { registerCapability } from "./dispatcher.js";
|
|
26
34
|
/** Artefact ceilings for one authored revision. Generous, and still a ceiling. */
|
|
27
35
|
const LIMITS = { max_artifacts: 60, max_artifact_bytes: 256_000 };
|
|
36
|
+
/** The two operations that AUTHOR content, and therefore the only ones a route applies to. */
|
|
37
|
+
const AUTHORING_OPERATIONS = ["create", "update"];
|
|
28
38
|
export const designHandler = {
|
|
29
39
|
descriptor: DESIGN_DESCRIPTOR,
|
|
30
40
|
async run(ctx) {
|
|
@@ -76,6 +86,7 @@ async function validatePackage(ctx) {
|
|
|
76
86
|
},
|
|
77
87
|
];
|
|
78
88
|
const report = reportSources([], `${id}`);
|
|
89
|
+
const simple = found.mode === "simple";
|
|
79
90
|
const maturity = attainedMaturity(requestedMaturity(ctx), found.ok ? "handoff" : "outline", report);
|
|
80
91
|
const fields = {
|
|
81
92
|
package: found.id,
|
|
@@ -85,9 +96,13 @@ async function validatePackage(ctx) {
|
|
|
85
96
|
path: found.path,
|
|
86
97
|
root: "workspace",
|
|
87
98
|
indexable: true,
|
|
88
|
-
maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
|
|
99
|
+
maturity: { requested: requestedMaturity(ctx), attained: simple ? null : maturity.attained },
|
|
89
100
|
sources: [],
|
|
90
101
|
renditions: [],
|
|
102
|
+
// Judging an existing design reports the route it IS, not one this attempt
|
|
103
|
+
// chose: the signals that expanded it were recorded when it was published,
|
|
104
|
+
// and re-deriving them now from a different invocation would invent a cause.
|
|
105
|
+
route: { mode: simple ? "simple" : "package", signals: [], cause: null },
|
|
91
106
|
};
|
|
92
107
|
return {
|
|
93
108
|
kind: "completed",
|
|
@@ -147,12 +162,19 @@ async function authoring(ctx) {
|
|
|
147
162
|
},
|
|
148
163
|
};
|
|
149
164
|
}
|
|
165
|
+
// Simple by default, and the route is decided BEFORE the contract is published:
|
|
166
|
+
// what a valid answer looks like, and where it may land, are different on the
|
|
167
|
+
// two routes, so asking first and classifying afterwards would publish a
|
|
168
|
+
// contract for a route the run is not on.
|
|
169
|
+
const route = await decideRoute(ctx, sources, root.value);
|
|
170
|
+
if (!route.ok)
|
|
171
|
+
return { kind: "blocked", failure: route.failure };
|
|
150
172
|
const request = buildSemanticRequest({
|
|
151
173
|
operation: `${ctx.request.capability}.${ctx.request.operation}`,
|
|
152
174
|
inputs: ctx.request.inputs.map((i) => ({ name: i.name, value: i.value })),
|
|
153
|
-
contract:
|
|
154
|
-
inventory:
|
|
155
|
-
allowedDestinations:
|
|
175
|
+
contract: route.value.contract,
|
|
176
|
+
inventory: route.value.inventory,
|
|
177
|
+
allowedDestinations: route.value.destinations,
|
|
156
178
|
limits: LIMITS,
|
|
157
179
|
readSet: [],
|
|
158
180
|
readSetBytes: 0,
|
|
@@ -160,7 +182,13 @@ async function authoring(ctx) {
|
|
|
160
182
|
if (ctx.verb !== "validate") {
|
|
161
183
|
return {
|
|
162
184
|
kind: "needs_input",
|
|
163
|
-
gaps: [
|
|
185
|
+
gaps: [
|
|
186
|
+
request.contract,
|
|
187
|
+
`destinos permitidos: ${request.allowed_destinations.join(", ")}`,
|
|
188
|
+
// Answering means quoting this digest back verbatim; naming it here saves
|
|
189
|
+
// the caller from reimplementing `canonicalJson` to recompute it.
|
|
190
|
+
`input_digest: ${request.input_digest}`,
|
|
191
|
+
],
|
|
164
192
|
};
|
|
165
193
|
}
|
|
166
194
|
if (ctx.answer === null || ctx.answer.trim().length === 0) {
|
|
@@ -175,37 +203,463 @@ async function authoring(ctx) {
|
|
|
175
203
|
const parsed = parseSemanticResponse(ctx.answer, request);
|
|
176
204
|
if (!parsed.ok)
|
|
177
205
|
return { kind: "blocked", failure: parsed.failure };
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
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);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Which route this attempt runs on, and everything that follows from it.
|
|
213
|
+
*
|
|
214
|
+
* The structural half of the vocabulary is derived here from what the invocation
|
|
215
|
+
* really carries; the semantic half is whatever the caller declared, judged
|
|
216
|
+
* against the same closed list. Nothing else can widen the route: an unknown id
|
|
217
|
+
* or a structural one somebody typed comes back as a rejection in the verdict
|
|
218
|
+
* rather than as an expansion nobody can explain.
|
|
219
|
+
*/
|
|
220
|
+
async function decideRoute(ctx, sources, root) {
|
|
221
|
+
const index = ctx.workspace === null ? null : await readDesignIndex(ctx.fs, ctx.workspace);
|
|
222
|
+
// By IDENTITY and only when one was named. `find(p => p.id === null)` would
|
|
223
|
+
// match the first package whose manifest does not validate — an entry that has
|
|
224
|
+
// no identity is not the one this invocation continues.
|
|
225
|
+
const named = packageInput(ctx);
|
|
226
|
+
const targeted = index === null || named === null ? null : (index.packages.find((p) => p.id === named) ?? null);
|
|
227
|
+
const verdict = judgeExpansion(declaredExpansionSignals(ctx), deriveStructuralSignals({
|
|
228
|
+
sensitiveSources: ctx.request.policy.sensitive_sources === true,
|
|
229
|
+
externalTransmission: ctx.request.policy.external_transmission === true,
|
|
230
|
+
sources,
|
|
231
|
+
governanceRecords: (targeted?.manifest?.governance.reviews.length ?? 0) +
|
|
232
|
+
(targeted?.manifest?.governance.revocations.length ?? 0),
|
|
233
|
+
publishedRevisions: targeted?.manifest?.baselines.length ?? 0,
|
|
181
234
|
}));
|
|
182
|
-
// The
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
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.
|
|
238
|
+
//
|
|
239
|
+
// `render` and `record` are package operations whatever the signals say —
|
|
240
|
+
// projecting revisions and sealing governance decisions are things a catalog
|
|
241
|
+
// has and one document does not. Routing them by the vocabulary would ask a
|
|
242
|
+
// simple design for a maturity and a rendition it never had.
|
|
243
|
+
if (verdict.mode === "package" ||
|
|
244
|
+
!AUTHORING_OPERATIONS.includes(ctx.operation.name) ||
|
|
245
|
+
index === null ||
|
|
246
|
+
!isIndexable(root)) {
|
|
247
|
+
return packageRoute(ctx, verdict, index, root);
|
|
248
|
+
}
|
|
249
|
+
const resolved = resolveSimpleTarget(index, ctx.operation.name, {
|
|
250
|
+
title: textInput(ctx, "title"),
|
|
251
|
+
packageId: packageInput(ctx),
|
|
252
|
+
});
|
|
253
|
+
if (!resolved.ok) {
|
|
254
|
+
const { code, message, action } = resolved.failure;
|
|
255
|
+
return { ok: false, failure: { code, message, action } };
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
value: {
|
|
260
|
+
verdict,
|
|
261
|
+
contract: simpleContract(resolved.value),
|
|
262
|
+
inventory: {
|
|
263
|
+
root: root.root,
|
|
264
|
+
mode: "simple",
|
|
265
|
+
package: resolved.value.packageId,
|
|
266
|
+
revision: resolved.value.revision,
|
|
267
|
+
document: `${resolved.value.path}/${SIMPLE_DESIGN_FILE}`,
|
|
268
|
+
},
|
|
269
|
+
// The exact file, not its folder: on the simple route the CLI already knows
|
|
270
|
+
// the one destination, so anything else is not a design it can publish.
|
|
271
|
+
destinations: [`${resolved.value.path}/${SIMPLE_DESIGN_FILE}`],
|
|
272
|
+
target: resolved.value,
|
|
273
|
+
packageTarget: null,
|
|
274
|
+
root,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The package route: sealed when the target can be derived, verbatim when not.
|
|
280
|
+
*
|
|
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.
|
|
284
|
+
*/
|
|
285
|
+
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
|
+
};
|
|
295
|
+
}
|
|
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);
|
|
315
|
+
if (!resolved.ok)
|
|
316
|
+
return { ok: false, failure: resolved.failure };
|
|
317
|
+
const target = resolved.value;
|
|
318
|
+
return {
|
|
319
|
+
ok: true,
|
|
320
|
+
value: {
|
|
321
|
+
verdict,
|
|
322
|
+
contract: packageContract(ctx.operation.name, target),
|
|
323
|
+
inventory: {
|
|
324
|
+
root: root.root,
|
|
325
|
+
mode: "package",
|
|
326
|
+
package: target.packageId,
|
|
327
|
+
revision: target.revision,
|
|
328
|
+
path: target.path,
|
|
329
|
+
},
|
|
330
|
+
// The package folder, not the taxonomy root: the destination check is
|
|
331
|
+
// segment-based, so every artifact lands INSIDE this package or nowhere.
|
|
332
|
+
destinations: [target.path],
|
|
333
|
+
target: null,
|
|
334
|
+
packageTarget: target,
|
|
335
|
+
root,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The package route's target, derived BEFORE the contract is published.
|
|
341
|
+
*
|
|
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.
|
|
347
|
+
*/
|
|
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));
|
|
362
|
+
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())),
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const named = packageInput(ctx);
|
|
373
|
+
if (named === null) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
failure: {
|
|
377
|
+
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",
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const found = index.packages.find((p) => p.id === named) ?? null;
|
|
384
|
+
if (found === null || found.manifest === null) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
failure: {
|
|
388
|
+
code: "DESIGN_PACKAGE_NOT_FOUND",
|
|
389
|
+
message: `no hay ningún package ${named} legible bajo ${index.root}/`,
|
|
390
|
+
action: `revisá 'aw designs' para ver las identidades publicadas bajo ${index.root}/`,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const manifest = found.manifest;
|
|
395
|
+
const current = manifest.current_baseline;
|
|
396
|
+
const actual = current === null ? null : `${manifest.id}@r${current.revision}`;
|
|
397
|
+
// The declared base is text on the wire: "null" is how a caller states the
|
|
398
|
+
// package never published, which the failure below already advertised. The
|
|
399
|
+
// null case cannot happen — `decideRoute` demands the input first — and the
|
|
400
|
+
// guard keeps that a fact of this function, not of its caller.
|
|
401
|
+
const declaredRaw = textInput(ctx, "base");
|
|
402
|
+
if (declaredRaw === null) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
failure: {
|
|
406
|
+
code: "DESIGN_FIELD_INVALID",
|
|
407
|
+
message: "actualizar un package declara sobre qué revisión se preparó",
|
|
408
|
+
action: "pasá 'base' con la revisión vigente (por ejemplo DES-001@r3), o null si el package no publicó ninguna",
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const declared = declaredRaw === "null" ? null : declaredRaw;
|
|
413
|
+
if (declared !== actual) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
failure: {
|
|
417
|
+
code: "DESIGN_BASE_STALE",
|
|
418
|
+
message: `declaraste base ${declared ?? "ninguna revisión"} y la vigente es ${actual ?? "ninguna"}`,
|
|
419
|
+
action: "releé el package y rehacé la revisión sobre la base nueva: una publicada no se reescribe",
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
value: {
|
|
426
|
+
packageId: manifest.id,
|
|
427
|
+
path: found.path,
|
|
428
|
+
revision: (current?.revision ?? 0) + 1,
|
|
429
|
+
manifest,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The manifest a `create` starts from: the empty catalog every package begins
|
|
435
|
+
* with, so the candidate builder sees the same shape it sees on an update. It
|
|
436
|
+
* is never written as-is — the first publication writes the DERIVED one.
|
|
437
|
+
*/
|
|
438
|
+
function initialPackageManifest(packageId, title, created) {
|
|
439
|
+
return {
|
|
440
|
+
schema: DESIGN_MANIFEST_SCHEMA_ID,
|
|
441
|
+
id: packageId,
|
|
442
|
+
mode: "package",
|
|
443
|
+
title,
|
|
444
|
+
created,
|
|
445
|
+
derived_from: null,
|
|
446
|
+
current_baseline: null,
|
|
447
|
+
baselines: [],
|
|
448
|
+
catalog: { flows: [], screens: [], rules: [], tokens: [], renditions: [], assets: [] },
|
|
449
|
+
currentness: [],
|
|
450
|
+
governance: { reviews: [], revocations: [] },
|
|
451
|
+
relations: { specs: [], plans: [] },
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** 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;
|
|
457
|
+
const documentPath = `${target.path}/${SIMPLE_DESIGN_FILE}`;
|
|
458
|
+
const document = answered.find((a) => a.path === documentPath);
|
|
459
|
+
if (answered.length !== 1 || document === undefined) {
|
|
460
|
+
return {
|
|
461
|
+
kind: "blocked",
|
|
462
|
+
failure: {
|
|
463
|
+
code: "DESIGN_FIELD_INVALID",
|
|
464
|
+
message: `un diseño simple es un solo archivo y llegaron ${answered.length}`,
|
|
465
|
+
action: `respondé exactamente un artefacto en '${documentPath}': el manifest, el id, la revisión y el digest los deriva el CLI`,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
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, {
|
|
472
|
+
target,
|
|
473
|
+
document: document.content,
|
|
474
|
+
published: localDateIso(new Date()),
|
|
475
|
+
});
|
|
476
|
+
if (!built.ok) {
|
|
477
|
+
const first = built.failures[0];
|
|
478
|
+
return {
|
|
479
|
+
kind: "blocked",
|
|
480
|
+
failure: {
|
|
481
|
+
code: first?.code ?? "DESIGN_FIELD_INVALID",
|
|
482
|
+
message: first?.message ?? "el documento no cumple el contrato de un diseño simple",
|
|
483
|
+
action: first?.action ?? "corregí el documento y volvé a responder",
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
186
487
|
const fields = {
|
|
187
|
-
package:
|
|
188
|
-
baseline:
|
|
189
|
-
path:
|
|
190
|
-
root: root.
|
|
191
|
-
indexable:
|
|
488
|
+
package: built.value.packageId,
|
|
489
|
+
baseline: { revision: built.value.revision, digest: built.value.digest },
|
|
490
|
+
path: target.path,
|
|
491
|
+
root: route.root.kind,
|
|
492
|
+
indexable: true,
|
|
493
|
+
// No ladder to climb: see `DesignReceiptFields.maturity`.
|
|
494
|
+
maturity: { requested: requestedMaturity(ctx), attained: null },
|
|
495
|
+
sources: report.sources,
|
|
496
|
+
renditions: [],
|
|
497
|
+
route: routeOf(route.verdict),
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
kind: "durable",
|
|
501
|
+
artifacts: built.value.artifacts.map((a) => ({
|
|
502
|
+
path: a.path,
|
|
503
|
+
content: a.content,
|
|
504
|
+
overwrite: a.overwrite,
|
|
505
|
+
})),
|
|
506
|
+
output: {
|
|
507
|
+
value: {
|
|
508
|
+
design: fields,
|
|
509
|
+
artifacts: built.value.artifacts.map((a) => a.path),
|
|
510
|
+
gaps: [],
|
|
511
|
+
},
|
|
512
|
+
reference: null,
|
|
513
|
+
completeness: "partial",
|
|
514
|
+
},
|
|
515
|
+
base: built.value.base,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* The expanded route's durable step.
|
|
520
|
+
*
|
|
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.
|
|
530
|
+
*/
|
|
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
|
+
}
|
|
560
|
+
// From workspace-relative to package-relative, which is the vocabulary the
|
|
561
|
+
// candidate builder speaks. The destination check already confined every
|
|
562
|
+
// answer to the package folder, so the prefix always strips.
|
|
563
|
+
const prefix = `${target.path}/`;
|
|
564
|
+
const files = [];
|
|
565
|
+
for (const artifact of answered) {
|
|
566
|
+
const relative = artifact.path.slice(prefix.length);
|
|
567
|
+
if (CLI_DERIVED_FILES.includes(relative) || relative.startsWith("baselines/")) {
|
|
568
|
+
return {
|
|
569
|
+
kind: "blocked",
|
|
570
|
+
failure: {
|
|
571
|
+
code: "DESIGN_FIELD_INVALID",
|
|
572
|
+
message: `'${artifact.path}' no se autora: lo deriva y sella el CLI`,
|
|
573
|
+
action: "quitalo de 'artifacts': el CLI deriva y sella design-manifest.json, baselines/ y PACKAGE.md a partir de los artefactos normativos",
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
files.push({ path: relative, content: artifact.content });
|
|
578
|
+
}
|
|
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, {
|
|
582
|
+
manifest: target.manifest,
|
|
583
|
+
packagePath: target.path,
|
|
584
|
+
files,
|
|
585
|
+
published: localDateIso(new Date()),
|
|
586
|
+
});
|
|
587
|
+
if (!candidate.ok) {
|
|
588
|
+
// The gate's own verdict, with its real code and next action: this is where
|
|
589
|
+
// an invalid tree stops being publishable instead of being sealed verbatim.
|
|
590
|
+
const first = candidate.failures[0];
|
|
591
|
+
return {
|
|
592
|
+
kind: "blocked",
|
|
593
|
+
failure: {
|
|
594
|
+
code: first?.code ?? "DESIGN_FIELD_INVALID",
|
|
595
|
+
message: first?.message ?? "el package no cumple el contrato de publicación",
|
|
596
|
+
action: first?.action ?? "corregí los artefactos y volvé a responder",
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
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);
|
|
604
|
+
const fields = {
|
|
605
|
+
package: target.packageId,
|
|
606
|
+
baseline: { revision: candidate.value.revision, digest: candidate.value.baseline.digest },
|
|
607
|
+
path: target.path,
|
|
608
|
+
root: route.root.kind,
|
|
609
|
+
indexable: isIndexable(route.root),
|
|
192
610
|
maturity: { requested: requestedMaturity(ctx), attained: maturity.attained },
|
|
193
611
|
sources: report.sources,
|
|
194
612
|
renditions: [],
|
|
613
|
+
route: routeOf(route.verdict),
|
|
195
614
|
};
|
|
196
615
|
return {
|
|
197
616
|
kind: "durable",
|
|
198
|
-
artifacts,
|
|
617
|
+
artifacts: candidate.value.artifacts,
|
|
199
618
|
output: {
|
|
200
|
-
value: {
|
|
619
|
+
value: {
|
|
620
|
+
design: fields,
|
|
621
|
+
artifacts: candidate.value.artifacts.map((a) => a.path),
|
|
622
|
+
gaps: maturity.gaps,
|
|
623
|
+
},
|
|
201
624
|
reference: null,
|
|
202
625
|
// The durable step has not run yet, so nothing is published. Claiming
|
|
203
626
|
// `complete` here would let a gate accept a proposal as a package.
|
|
204
627
|
completeness: "partial",
|
|
205
628
|
},
|
|
206
|
-
base:
|
|
629
|
+
base: await packageManifestBase(ctx, target),
|
|
207
630
|
};
|
|
208
631
|
}
|
|
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
|
+
/**
|
|
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.
|
|
639
|
+
*/
|
|
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";
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
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.
|
|
651
|
+
*/
|
|
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)) };
|
|
658
|
+
}
|
|
659
|
+
/** The verdict as the receipt states it: mode, signals and the one-line cause. */
|
|
660
|
+
function routeOf(verdict) {
|
|
661
|
+
return { mode: verdict.mode, signals: verdict.fired, cause: verdict.cause };
|
|
662
|
+
}
|
|
209
663
|
/**
|
|
210
664
|
* What each operation has to be true BEFORE it can even ask for content.
|
|
211
665
|
*
|
|
@@ -279,6 +733,38 @@ function requestedMaturity(ctx) {
|
|
|
279
733
|
const value = inputValue(ctx, "maturity");
|
|
280
734
|
return value === "handoff" || value === "outline" ? value : null;
|
|
281
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* The expansion signals the caller put on the table.
|
|
738
|
+
*
|
|
739
|
+
* Accepted as a repeated input or as one comma-separated value, because a
|
|
740
|
+
* command line and a composing flow hand lists over differently — and neither
|
|
741
|
+
* spelling should be the one that silently drops a signal. Whether each id is
|
|
742
|
+
* admissible is `judgeExpansion`'s call, not this reader's.
|
|
743
|
+
*/
|
|
744
|
+
function declaredExpansionSignals(ctx) {
|
|
745
|
+
const raw = ctx.request.inputs.filter((i) => i.name === "expansion").map((i) => i.value);
|
|
746
|
+
return raw
|
|
747
|
+
.flatMap((value) => (Array.isArray(value) ? value.map(String) : String(value).split(",")))
|
|
748
|
+
.map((id) => id.trim())
|
|
749
|
+
.filter((id) => id.length > 0);
|
|
750
|
+
}
|
|
751
|
+
function textInput(ctx, name) {
|
|
752
|
+
const value = inputValue(ctx, name);
|
|
753
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
754
|
+
}
|
|
755
|
+
function packageInput(ctx) {
|
|
756
|
+
return textInput(ctx, "package");
|
|
757
|
+
}
|
|
758
|
+
/** What a valid answer is on the simple route: one document, three sections, no metadata. */
|
|
759
|
+
function simpleContract(target) {
|
|
760
|
+
return [
|
|
761
|
+
`Escribí el diseño como UN solo documento Markdown en '${target.path}/${SIMPLE_DESIGN_FILE}'.`,
|
|
762
|
+
`Abrilo con '# <título>' y usá solo estas secciones, en este orden: ${SIMPLE_SECTIONS.map((s) => `## ${s}`).join(", ")}.`,
|
|
763
|
+
`Siempre van ${SIMPLE_CORE_SECTIONS.map((s) => `'## ${s}'`).join(", ")}; las demás solo si dicen algo, y nunca vacías.`,
|
|
764
|
+
"No escribas manifest, id, revisión, digest, madurez ni referencias: todo eso lo deriva el CLI de este documento.",
|
|
765
|
+
"Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' y 'artifacts': [{path, content}] con ese único archivo.",
|
|
766
|
+
].join(" ");
|
|
767
|
+
}
|
|
282
768
|
function contractFor(operation) {
|
|
283
769
|
const shared = "Respondé un único objeto JSON con 'version', 'operation', 'input_digest', 'state': 'proposed' " +
|
|
284
770
|
"y 'artifacts': [{path, content}]. Cada 'path' es relativo al workspace y cae dentro de los " +
|
|
@@ -291,6 +777,30 @@ function contractFor(operation) {
|
|
|
291
777
|
};
|
|
292
778
|
return `${perOperation[operation] ?? ""} ${shared}`.trim();
|
|
293
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* What a valid answer is on the SEALED package route: the normative artifacts,
|
|
782
|
+
* and nothing the CLI derives.
|
|
783
|
+
*
|
|
784
|
+
* The split is the same one the simple route states: the agent authors content,
|
|
785
|
+
* the CLI owns identity, sealing and projections. Naming the derived files in
|
|
786
|
+
* the contract is what keeps a hand-authored manifest or baseline from coming
|
|
787
|
+
* back as an answer — those arrive as a rejection, not as a silent overwrite.
|
|
788
|
+
*/
|
|
789
|
+
function packageContract(operation, target) {
|
|
790
|
+
const perOperation = {
|
|
791
|
+
create: "Autorá la PRIMERA revisión del package a partir de las fuentes declaradas.",
|
|
792
|
+
update: "Autorá la revisión SIGUIENTE sobre la base declarada. No reescribas revisiones ya selladas.",
|
|
793
|
+
};
|
|
794
|
+
return [
|
|
795
|
+
perOperation[operation] ?? "",
|
|
796
|
+
`El id asignado es '${target.packageId}' y la carpeta '${target.path}': ambos van en el 'inventory' del request.`,
|
|
797
|
+
`El frontmatter de cada artefacto declara ese id de package (por ejemplo '${target.packageId}/FLW-001').`,
|
|
798
|
+
"NO autores 'design-manifest.json', nada bajo 'baselines/' ni 'PACKAGE.md': el CLI los deriva y sella a partir de tus artefactos, y rechaza la respuesta si los incluye.",
|
|
799
|
+
"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 los destinos permitidos. Ningún artefacto inventa un formato: los del UI Design Package v1 son los únicos aceptados.",
|
|
800
|
+
]
|
|
801
|
+
.join(" ")
|
|
802
|
+
.trim();
|
|
803
|
+
}
|
|
294
804
|
function inputValue(ctx, name) {
|
|
295
805
|
return ctx.request.inputs.find((i) => i.name === name)?.value;
|
|
296
806
|
}
|