@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.
- package/README.md +4 -16
- package/dist/adapters/git-cli.js +36 -0
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/application/capability/design-handler.js +472 -177
- package/dist/application/capability/design-handler.js.map +1 -1
- package/dist/application/capability/dispatcher.js +1 -1
- package/dist/application/capability/dispatcher.js.map +1 -1
- package/dist/application/capability/durable-effect.js +1 -1
- package/dist/application/capability/durable-effect.js.map +1 -1
- package/dist/application/check-branch-service.js +1 -0
- package/dist/application/check-branch-service.js.map +1 -1
- package/dist/application/checkpoint-service.js +4 -3
- package/dist/application/checkpoint-service.js.map +1 -1
- package/dist/application/design/consumer-document.js +179 -0
- package/dist/application/design/consumer-document.js.map +1 -0
- package/dist/application/design/design-content-gate-service.js +9 -1
- package/dist/application/design/design-content-gate-service.js.map +1 -1
- package/dist/application/design/design-gate-service.js +18 -1
- package/dist/application/design/design-gate-service.js.map +1 -1
- package/dist/application/design/design-index-service.js +22 -3
- package/dist/application/design/design-index-service.js.map +1 -1
- package/dist/application/design/design-publish-service.js +44 -136
- package/dist/application/design/design-publish-service.js.map +1 -1
- package/dist/application/design/design-simple-service.js +38 -24
- package/dist/application/design/design-simple-service.js.map +1 -1
- package/dist/application/dev-only-services.js +19 -12
- package/dist/application/dev-only-services.js.map +1 -1
- package/dist/application/docs-canon-service.js +79 -5
- package/dist/application/docs-canon-service.js.map +1 -1
- package/dist/application/dsn-reader-service.js +12 -47
- package/dist/application/dsn-reader-service.js.map +1 -1
- package/dist/application/export-service.js +20 -11
- package/dist/application/export-service.js.map +1 -1
- package/dist/application/flow/flow-service.js +25 -0
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +5 -0
- package/dist/application/flow/internal-actions.js.map +1 -1
- package/dist/application/flow/submit.js +152 -22
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/history-table.js +82 -44
- package/dist/application/history-table.js.map +1 -1
- package/dist/application/history-update-service.js +19 -9
- package/dist/application/history-update-service.js.map +1 -1
- package/dist/application/hook-git-commit-advisor.js +5 -4
- package/dist/application/hook-git-commit-advisor.js.map +1 -1
- package/dist/application/hook-sql-mutation-guard.js +1 -1
- package/dist/application/hook-sql-mutation-guard.js.map +1 -1
- package/dist/application/lifecycle-target.js +1 -0
- package/dist/application/lifecycle-target.js.map +1 -1
- package/dist/application/local-proposal.js +47 -23
- package/dist/application/local-proposal.js.map +1 -1
- package/dist/application/mcp-connections-service.js +37 -2
- package/dist/application/mcp-connections-service.js.map +1 -1
- package/dist/application/mcp-dbhub-launcher.js +22 -50
- package/dist/application/mcp-dbhub-launcher.js.map +1 -1
- package/dist/application/mcp-doctor-service.js +14 -23
- package/dist/application/mcp-doctor-service.js.map +1 -1
- package/dist/application/mcp-remove-service.js +3 -3
- package/dist/application/mcp-remove-service.js.map +1 -1
- package/dist/application/mcp-setup-service.js +4 -4
- package/dist/application/mcp-setup-service.js.map +1 -1
- package/dist/application/mcp-test-connection-service.js +4 -6
- package/dist/application/mcp-test-connection-service.js.map +1 -1
- package/dist/application/parsers/phases.js +17 -5
- package/dist/application/parsers/phases.js.map +1 -1
- package/dist/application/parsers/project-block.js +5 -11
- package/dist/application/parsers/project-block.js.map +1 -1
- package/dist/application/parsers/sessions-csv.js +16 -14
- package/dist/application/parsers/sessions-csv.js.map +1 -1
- package/dist/application/parsers/spec-relation.js +15 -11
- package/dist/application/parsers/spec-relation.js.map +1 -1
- package/dist/application/parsers/tasks.js +25 -1
- package/dist/application/parsers/tasks.js.map +1 -1
- package/dist/application/persist-service.js +67 -29
- package/dist/application/persist-service.js.map +1 -1
- package/dist/application/plugin-doctor/skills.js +1 -40
- package/dist/application/plugin-doctor/skills.js.map +1 -1
- package/dist/application/plugin-doctor-service.js +1 -3
- package/dist/application/plugin-doctor-service.js.map +1 -1
- package/dist/application/release-data/artifacts.js +1 -0
- package/dist/application/release-data/artifacts.js.map +1 -1
- package/dist/application/release-data/bundles.js +15 -6
- package/dist/application/release-data/bundles.js.map +1 -1
- package/dist/application/release-data/common.js +16 -7
- package/dist/application/release-data/common.js.map +1 -1
- package/dist/application/release-data/sessions.js +36 -8
- package/dist/application/release-data/sessions.js.map +1 -1
- package/dist/application/render/project-block.js +2 -2
- package/dist/application/render/project-block.js.map +1 -1
- package/dist/application/resume-service.js +9 -1
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/graph.js +14 -13
- package/dist/application/retirement/graph.js.map +1 -1
- package/dist/application/retirement/prepare.js +15 -2
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/self/clean-legacy.js +2 -4
- package/dist/application/self/clean-legacy.js.map +1 -1
- package/dist/application/self/mcp-config.js +8 -9
- package/dist/application/self/mcp-config.js.map +1 -1
- package/dist/application/session-close-service.js +8 -2
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-create-service.js +26 -14
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-resolver.js +80 -34
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/session-resume-service.js +4 -3
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/application/source-boundary-policy.js +483 -0
- package/dist/application/source-boundary-policy.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 +42 -20
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-migrate/apply.js +2 -4
- package/dist/application/workspace-migrate/apply.js.map +1 -1
- package/dist/application/workspace-migrate/plan.js +9 -6
- package/dist/application/workspace-migrate/plan.js.map +1 -1
- package/dist/application/workspace-migrate/preview.js +3 -3
- package/dist/application/workspace-migrate/preview.js.map +1 -1
- package/dist/application/worktree-service.js +3 -0
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/capability.js +124 -5
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/designs.js +2 -6
- package/dist/cli/commands/designs.js.map +1 -1
- package/dist/cli/commands/dev-only.js +1 -1
- package/dist/cli/commands/dev-only.js.map +1 -1
- package/dist/cli/commands/flow.js +3 -1
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/index.js +0 -2
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/mcp.js +53 -55
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/persist.js +27 -21
- package/dist/cli/commands/persist.js.map +1 -1
- package/dist/cli/commands/resume.js +2 -2
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/session-create.js +2 -1
- package/dist/cli/commands/session-create.js.map +1 -1
- package/dist/cli/help-groups.js +0 -1
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +0 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +1 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/tui/data/recommended-skills.js +2 -2
- package/dist/cli/tui/data/recommended-skills.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js +1 -1
- package/dist/cli/tui/tabs/mcp-tab.js +2 -2
- package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
- package/dist/domain/capability/protocol.js.map +1 -1
- package/dist/domain/correlative.js +121 -0
- package/dist/domain/correlative.js.map +1 -0
- package/dist/domain/design/capability.js +37 -3
- package/dist/domain/design/capability.js.map +1 -1
- package/dist/domain/design/direct.js.map +1 -1
- package/dist/domain/design/simple.js +33 -1
- package/dist/domain/design/simple.js.map +1 -1
- package/dist/domain/docs-canon.js +31 -0
- package/dist/domain/docs-canon.js.map +1 -0
- package/dist/domain/flow/answer.js +57 -1
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +28 -29
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/execution-result.js +13 -1
- package/dist/domain/flow/execution-result.js.map +1 -1
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/mcp-entry.js +7 -12
- package/dist/domain/mcp-entry.js.map +1 -1
- package/dist/domain/retirement/selector.js +16 -14
- package/dist/domain/retirement/selector.js.map +1 -1
- package/dist/domain/source-boundary.js +3 -0
- package/dist/domain/source-boundary.js.map +1 -0
- package/dist/domain/workline-node.js +12 -5
- package/dist/domain/workline-node.js.map +1 -1
- package/dist/runtime/namespace-resolver.js +0 -3
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/package.json +5 -5
- package/skills/w/SKILL.md +2 -2
- package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +14 -9
- package/skills/w/artifacts/artifacts-research/CONCLUSIONS.md +7 -0
- package/skills/w/commands/export-scripts.md +8 -2
- package/skills/w/commands/plan-new.md +1 -0
- package/skills/w/context/MANIFEST.json +2 -9
- package/skills/w/exports/export-scripts/EXPORT.md +13 -9
- package/skills/w/loops/CHASSIS.md +1 -1
- package/skills/w/loops/plan-exec-loop/LOOP.md +14 -10
- package/skills/w/loops/plan-new-loop/LOOP.md +16 -16
- package/skills/w/loops/plan-refine-loop/LOOP.md +6 -1
- package/skills/w/loops/quick-loop/LOOP.md +2 -2
- package/skills/w/loops/spec-refine-loop/LOOP.md +4 -0
- package/skills/w/modules/DB-RESEARCH-RULE.md +5 -5
- package/skills/w/modules/DB-SCRIPTS-ONLY.md +5 -6
- package/skills/w/modules/EXEC-DB-POLICY.md +2 -2
- package/skills/w/modules/PLAN-INPUT.md +11 -9
- package/skills/w/roles/sql/ROLE.md +9 -9
- package/dist/application/decisiones-service.js +0 -31
- package/dist/application/decisiones-service.js.map +0 -1
- package/dist/application/dev-bootstrap-dsn-service.js +0 -76
- package/dist/application/dev-bootstrap-dsn-service.js.map +0 -1
- package/dist/application/profile/profile-service.js +0 -199
- package/dist/application/profile/profile-service.js.map +0 -1
- package/dist/application/tasks-service.js +0 -45
- package/dist/application/tasks-service.js.map +0 -1
- package/dist/cli/commands/bootstrap-dsn.js +0 -16
- package/dist/cli/commands/bootstrap-dsn.js.map +0 -1
- package/dist/cli/tui/components/process-list.js +0 -43
- 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 {
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
236
|
-
//
|
|
237
|
-
//
|
|
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
|
|
361
|
+
* The package route. It seals, or it does not run.
|
|
280
362
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
410
|
+
* The route of an operation that publishes WITHOUT minting a revision.
|
|
341
411
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
373
|
-
if (
|
|
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: "
|
|
379
|
-
action: "pasá '
|
|
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 =
|
|
384
|
-
if (found ===
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
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
|
-
|
|
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
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
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
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
649
|
-
*
|
|
650
|
-
*
|
|
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
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return
|
|
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
|
|
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;
|