@tacuchi/agent-workflow-cli 25.4.0 → 25.6.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 +2 -0
- package/dist/application/cut-intent-ledger.js +164 -0
- package/dist/application/cut-intent-ledger.js.map +1 -0
- package/dist/application/export-service.js +253 -37
- package/dist/application/export-service.js.map +1 -1
- package/dist/application/history-publications.js +76 -0
- package/dist/application/history-publications.js.map +1 -0
- package/dist/application/lineage-service.js +119 -0
- package/dist/application/lineage-service.js.map +1 -0
- package/dist/application/paths-service.js +8 -0
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/persist-service.js +9 -3
- package/dist/application/persist-service.js.map +1 -1
- package/dist/application/release-pass-ledger.js +327 -0
- package/dist/application/release-pass-ledger.js.map +1 -0
- package/dist/application/resume-service.js +9 -5
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/prepare.js +2 -2
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/session-resolver.js +8 -2
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/{retirement/graph.js → workline-graph.js} +18 -12
- package/dist/application/workline-graph.js.map +1 -0
- package/dist/application/workline-index-service.js +200 -16
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-materialization-service.js +40 -4
- package/dist/application/workspace-materialization-service.js.map +1 -1
- package/dist/application/worktree-service.js +276 -6
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/cut-intent.js +176 -0
- package/dist/cli/commands/cut-intent.js.map +1 -0
- package/dist/cli/commands/export.js +64 -15
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/release-pass.js +228 -0
- package/dist/cli/commands/release-pass.js.map +1 -0
- package/dist/cli/commands/resume.js +3 -0
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/status.js +7 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/worktree.js +19 -5
- package/dist/cli/commands/worktree.js.map +1 -1
- package/dist/cli/help-groups.js +11 -0
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +27 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +4 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/tui/data/recommended-skills.js +39 -10
- package/dist/cli/tui/data/recommended-skills.js.map +1 -1
- package/dist/domain/cut-intent.js +79 -0
- package/dist/domain/cut-intent.js.map +1 -0
- package/dist/domain/flow/authority.js +8 -0
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/release-pass.js +104 -0
- package/dist/domain/release-pass.js.map +1 -0
- package/dist/runtime/namespace-resolver.js +3 -10
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/dist/runtime/workline-marker.js +70 -0
- package/dist/runtime/workline-marker.js.map +1 -0
- package/package.json +1 -1
- package/skills/w/commands/export-scripts.md +13 -12
- package/skills/w/exports/README.md +4 -2
- package/skills/w/exports/export-scripts/EXPORT.md +39 -45
- package/dist/application/retirement/graph.js.map +0 -1
package/README.md
CHANGED
|
@@ -187,6 +187,8 @@ Workspace artifacts live under `.<namespace>/`. Resolution order (first match wi
|
|
|
187
187
|
- `self install-skill` / `self doctor` / `self update` / `mcp` — CLI maintenance.
|
|
188
188
|
- `amend <apply|revert|list>` — correct the WORDING of an already closed spec or plan in one act, under the workspace lock and with the document's own digest as the compare-and-swap base; it demands an explicit declaration that no scope, criteria or rules move, records the exact pre-image in an append-only ledger, and refuses structurally whatever touches the contract (naming the refinement instead). CLI-only: there is no `/w:amend`.
|
|
189
189
|
- `settle <list|prepare|apply>` — settle or acknowledge the live obligations a decision note left on a plan whose execution run is already closed. `list` shows each one with its note, position, class, whether that class was declared and the plan's CURRENT resume point; `prepare` derives the same settlement note the closure derives, writes nothing and returns the digest that authorizes it; `apply` re-derives from the live workspace, demands that digest and publishes under the lock. It refuses while an execution run holds the plan, naming that run — its closure settles its own obligations. CLI-only: there is no `/w:settle`.
|
|
190
|
+
- `cut-intent <declare|show>` — the intention with which a spec cut into several plans was meant to be executed: which plans travel together in one pass, in what order, and which are deferred with their cause. Correcting means declaring again — the previous record stays legible underneath, so a reordering is something a person can review instead of simply inherit. It restricts nothing: executing out of the declared order stays valid and is only warned. CLI-only: there is no `/w:cut-intent`.
|
|
191
|
+
- `release-pass <list|declare|arrived|applied|revert|link>` — the pass to production as its own object, because closed is not released. `declare` opens one over the sources it covers; `arrived` registers one source's arrival, and while another is still missing the pass reads partially released, naming both; `applied` registers that the SQL the pass carries RAN against a named environment — its own axis, never a fourth arrival kind, so a pass with no such record reads NO RECORD rather than nothing-applied; `revert` adds a reversion that never erases the arrivals it follows; `link` attaches a document by workspace-relative path, checking only that it exists. Nothing here checks the world: registering is DECLARING a fact somebody already knows. It feeds the `production` axis of `status`/`resume` and the `--environment` filter of `export-scripts`. CLI-only: there is no `/w:release-pass`.
|
|
190
192
|
|
|
191
193
|
Run `agent-workflow --help` (or `aw --help`) for the full list, or `agent-workflow <command> --help` for per-command flags.
|
|
192
194
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable record of how somebody meant to execute a cut — and the only place
|
|
3
|
+
* it lives.
|
|
4
|
+
*
|
|
5
|
+
* Same shape and same reasons as the claims ledger next to it: append-only, one
|
|
6
|
+
* JSON record per line, under `.workflow/` and deliberately OUTSIDE `docs/`. The
|
|
7
|
+
* corpus is for documents somebody published; an intent is workspace state, not a
|
|
8
|
+
* document, and putting it in `docs/` would make the record itself look like a
|
|
9
|
+
* spec or a plan.
|
|
10
|
+
*
|
|
11
|
+
* Append-only is load-bearing for a different reason here than there. A
|
|
12
|
+
* revocation has to be irrevocable; an intent, by contrast, is *meant* to be
|
|
13
|
+
* corrected — plans get reordered, a group gets split, something moves to the
|
|
14
|
+
* next pass. So a correction is a NEW record that supersedes the previous one
|
|
15
|
+
* for reading, and the previous one stays exactly where it was. Rewriting it
|
|
16
|
+
* would destroy the one thing that makes a reorder reviewable: what the order
|
|
17
|
+
* used to be, and therefore that somebody changed it.
|
|
18
|
+
*/
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { assertDeclarable, positionOf, } from "../domain/cut-intent.js";
|
|
21
|
+
import { formatNodeId, isWorklineKind } from "../domain/workline-node.js";
|
|
22
|
+
/** Lives next to HISTORY.md and claims.jsonl: workspace state, never corpus. */
|
|
23
|
+
const LEDGER_FILE = "cut-intents.jsonl";
|
|
24
|
+
const LEDGER_VERSION = 1;
|
|
25
|
+
export function cutIntentLedgerPath(paths) {
|
|
26
|
+
return join(paths.cwdRoot(), LEDGER_FILE);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Add one record. Append-only by construction: there is no update and no delete.
|
|
30
|
+
*
|
|
31
|
+
* Validated before the write for the reason the whole file exists: a malformed
|
|
32
|
+
* record cannot be corrected in place, only buried, so the cheap refusal is here.
|
|
33
|
+
* One record is one short line, so `O_APPEND` keeps concurrent writers from
|
|
34
|
+
* interleaving halves of it — the same bound the claims ledger relies on, and the
|
|
35
|
+
* same reason a record is not pretty-printed.
|
|
36
|
+
*/
|
|
37
|
+
export async function appendCutIntent(fs, paths, event) {
|
|
38
|
+
assertDeclarable(event.intent);
|
|
39
|
+
const record = { version: LEDGER_VERSION, event: "declared", ...event };
|
|
40
|
+
await fs.appendText(cutIntentLedgerPath(paths), `${JSON.stringify(record)}\n`);
|
|
41
|
+
}
|
|
42
|
+
/** Every record, oldest first. A missing ledger reads as empty, never as an error. */
|
|
43
|
+
export async function readCutIntents(fs, paths) {
|
|
44
|
+
const path = cutIntentLedgerPath(paths);
|
|
45
|
+
if (!(await fs.exists(path)))
|
|
46
|
+
return { events: [], unreadable: 0 };
|
|
47
|
+
const raw = await fs.readText(path);
|
|
48
|
+
const events = [];
|
|
49
|
+
let unreadable = 0;
|
|
50
|
+
for (const line of raw.split("\n")) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (trimmed.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const parsed = parseEvent(trimmed);
|
|
55
|
+
if (parsed === null)
|
|
56
|
+
unreadable += 1;
|
|
57
|
+
else
|
|
58
|
+
events.push(parsed);
|
|
59
|
+
}
|
|
60
|
+
return { events, unreadable };
|
|
61
|
+
}
|
|
62
|
+
function parseEvent(line) {
|
|
63
|
+
let value;
|
|
64
|
+
try {
|
|
65
|
+
value = JSON.parse(line);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== "object" || value === null)
|
|
71
|
+
return null;
|
|
72
|
+
const candidate = value;
|
|
73
|
+
if (typeof candidate.at !== "string" || candidate.event !== "declared")
|
|
74
|
+
return null;
|
|
75
|
+
const intent = candidate.intent;
|
|
76
|
+
if (typeof intent !== "object" || intent === null)
|
|
77
|
+
return null;
|
|
78
|
+
if (!isNode(intent.spec) || !isNodeList(intent.order) || !isNodeList(intent.deferred)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function isNode(value) {
|
|
84
|
+
if (typeof value !== "object" || value === null)
|
|
85
|
+
return false;
|
|
86
|
+
const node = value;
|
|
87
|
+
return isWorklineKind(node.kind) && typeof node.key === "string" && node.key.length > 0;
|
|
88
|
+
}
|
|
89
|
+
function isNodeList(value) {
|
|
90
|
+
return Array.isArray(value) && value.every(isNode);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The intent in force for this spec: the LAST record about it, or `null`.
|
|
94
|
+
*
|
|
95
|
+
* Derived rather than stored, because the ledger is append-only and a "current"
|
|
96
|
+
* flag would be exactly the mutable state that makes an append-only log
|
|
97
|
+
* pointless. Last-wins is the whole correction mechanism: declaring again is how
|
|
98
|
+
* you fix an order, and the superseded record stays readable above it.
|
|
99
|
+
*/
|
|
100
|
+
export function currentIntentOf(events, spec) {
|
|
101
|
+
const key = formatNodeId(spec);
|
|
102
|
+
let current = null;
|
|
103
|
+
for (const event of events) {
|
|
104
|
+
if (formatNodeId(event.intent.spec) === key)
|
|
105
|
+
current = event;
|
|
106
|
+
}
|
|
107
|
+
return current;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The intent in force that mentions this plan, or `null`.
|
|
111
|
+
*
|
|
112
|
+
* Asked by plan rather than by spec because that is the question the board has:
|
|
113
|
+
* it is holding a plan and wants to know where the person put it. A plan is
|
|
114
|
+
* mentioned by at most one cut in force — its spec's — so the first match is the
|
|
115
|
+
* answer, and a plan named by a superseded record only counts if the record that
|
|
116
|
+
* superseded it still names it.
|
|
117
|
+
*/
|
|
118
|
+
export function currentIntentForPlan(events, plan) {
|
|
119
|
+
const specs = new Set(events.map((event) => formatNodeId(event.intent.spec)));
|
|
120
|
+
const key = formatNodeId(plan);
|
|
121
|
+
for (const spec of specs) {
|
|
122
|
+
const [kind, ...rest] = spec.split(":");
|
|
123
|
+
if (!isWorklineKind(kind))
|
|
124
|
+
continue;
|
|
125
|
+
const current = currentIntentOf(events, { kind, key: rest.join(":") });
|
|
126
|
+
if (current === null)
|
|
127
|
+
continue;
|
|
128
|
+
const mentioned = [...current.intent.order, ...current.intent.deferred].some((node) => formatNodeId(node) === key);
|
|
129
|
+
if (mentioned)
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* What the workspace answers about one plan — the explicit reading, never a list.
|
|
136
|
+
*
|
|
137
|
+
* The `declared: false` branch carries its reason because the three ways a plan
|
|
138
|
+
* can have no declared place are not the same thing to the person reading it:
|
|
139
|
+
* nobody ever declared a cut for its spec, the ledger could not be fully read, or
|
|
140
|
+
* a cut exists and simply does not mention this plan. Collapsing them into an
|
|
141
|
+
* empty list is what would let a caller fill the silence with the correlative.
|
|
142
|
+
*/
|
|
143
|
+
export function readingForPlan(read, plan) {
|
|
144
|
+
const current = currentIntentForPlan(read.events, plan);
|
|
145
|
+
if (current === null) {
|
|
146
|
+
if (read.unreadable > 0) {
|
|
147
|
+
return {
|
|
148
|
+
declared: false,
|
|
149
|
+
reason: `no hay intención declarada legible para '${formatNodeId(plan)}': el libro tiene ${read.unreadable} línea(s) que no se pueden leer, así que su ausencia no está probada`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
declared: false,
|
|
154
|
+
reason: `nadie declaró una intención de corte que nombre a '${formatNodeId(plan)}'`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
declared: true,
|
|
159
|
+
intent: current.intent,
|
|
160
|
+
at: current.at,
|
|
161
|
+
position: positionOf(current.intent, plan),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=cut-intent-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cut-intent-ledger.js","sourceRoot":"","sources":["../../src/application/cut-intent-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { basename, relative, sep } from "node:path";
|
|
1
2
|
import { CORRELATIVE_SOURCE, isCorrelative } from "../domain/correlative.js";
|
|
2
3
|
import { localDateIso } from "./dates.js";
|
|
3
4
|
import { runNextNumber } from "./dev-only-services.js";
|
|
4
5
|
import { resolveDocsCanon } from "./docs-canon-service.js";
|
|
6
|
+
import { appendPublications, publicationRows } from "./history-publications.js";
|
|
5
7
|
import { withCwdLock } from "./lock-service.js";
|
|
6
8
|
import { runReleaseData } from "./release-data-service.js";
|
|
9
|
+
import { derivePasses, readReleasePasses } from "./release-pass-ledger.js";
|
|
7
10
|
import { approvalDigest, buildSemanticRequest, parseSemanticResponse, readEnvelopeScope, } from "./semantic-operation/protocol.js";
|
|
8
11
|
import { publishArtifacts } from "./semantic-operation/publish.js";
|
|
9
12
|
export const EXPORT_CATEGORIES = [
|
|
@@ -17,11 +20,12 @@ export const EXPORT_CATEGORIES = [
|
|
|
17
20
|
* command guide. Keep its semantic anchors exported so a small parity guard
|
|
18
21
|
* catches doctrine that drifts away from what the CLI actually sends.
|
|
19
22
|
*/
|
|
20
|
-
export const SCRIPTS_FINAL_STATE_CONTRACT = "Un dossier con 00-ROLLBACK.sql y README.md obligatorios, más los forwards NN-<nombre>.sql numerados de forma continua desde 01. El CLI NUNCA ejecuta SQL. El bundle publica el ESTADO FINAL NETO de la secuencia, no una réplica por sesión: lo que nace y muere dentro de la secuencia se omite; lo migrado va directo a su forma final; lo que el contexto declara retirado se omite aunque ningún script lo elimine. 00-ROLLBACK.sql invierte ese ESTADO FINAL en orden seguro para las dependencias, no el reverso literal de los forwards. Reconciliá contra el código además de las sesiones y la base. Excluí identidades concretas y semillas de prueba; conservá sólo objetos compartidos y necesarios para el estado final.";
|
|
23
|
+
export const SCRIPTS_FINAL_STATE_CONTRACT = "Un dossier con 00-ROLLBACK.sql y README.md obligatorios, más los forwards NN-<nombre>.sql numerados de forma continua desde 01. El CLI NUNCA ejecuta SQL. El bundle publica el ESTADO FINAL NETO de la secuencia, no una réplica por sesión: lo que nace y muere dentro de la secuencia se omite; lo migrado va directo a su forma final; lo que el contexto declara retirado se omite aunque ningún script lo elimine. 00-ROLLBACK.sql invierte ese ESTADO FINAL en orden seguro para las dependencias, no el reverso literal de los forwards. Reconciliá contra el código además de las sesiones y la base. Excluí identidades concretas y semillas de prueba; conservá sólo objetos compartidos y necesarios para el estado final. Un bundle previo que entra al origen es MATERIAL A RECONCILIAR, no historia intocable: dos bundles que se contradicen publican el estado final neto resultante, nunca su suma cronológica.";
|
|
21
24
|
export const SCRIPTS_FINAL_STATE_CONTRACT_ANCHORS = [
|
|
22
25
|
"ESTADO FINAL NETO",
|
|
23
26
|
"orden seguro para las dependencias",
|
|
24
27
|
"objetos compartidos y necesarios para el estado final",
|
|
28
|
+
"MATERIAL A RECONCILIAR",
|
|
25
29
|
];
|
|
26
30
|
const POLICIES = {
|
|
27
31
|
diagrams: {
|
|
@@ -57,6 +61,7 @@ const POLICIES = {
|
|
|
57
61
|
};
|
|
58
62
|
const LIMITS = { max_artifacts: 64, max_artifact_bytes: 512 * 1024 };
|
|
59
63
|
const FORWARD_RE = /^(\d{2})-[^/]+\.sql$/;
|
|
64
|
+
const EXPORT_BASES = ["sessions", "bundles", "workspace"];
|
|
60
65
|
// ── prepare ──────────────────────────────────────────────────────────────────
|
|
61
66
|
export async function prepareExport(fs, env, paths, category, selection = {},
|
|
62
67
|
// Injected so the midnight boundary is testable: a preparation that outlives
|
|
@@ -74,27 +79,10 @@ now = () => new Date()) {
|
|
|
74
79
|
};
|
|
75
80
|
}
|
|
76
81
|
const policy = resolvePolicy(category, canon.canon[category]);
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
failure: {
|
|
82
|
-
code: "EXPORT_CORPUS_UNAVAILABLE",
|
|
83
|
-
message: corpus.error,
|
|
84
|
-
action: "revisá el workspace y los filtros --sessions/--since/--source",
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (corpus.sessions.length === 0) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
failure: {
|
|
92
|
-
code: "EXPORT_CORPUS_EMPTY",
|
|
93
|
-
message: "ninguna sesión coincide con los filtros",
|
|
94
|
-
action: "ampliá --since, quitá --sessions, o revisá que existan sesiones cerradas",
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
82
|
+
const resolved = await resolveMaterial(fs, env, paths, category, selection);
|
|
83
|
+
if (!resolved.ok)
|
|
84
|
+
return resolved;
|
|
85
|
+
const material = resolved.value;
|
|
98
86
|
// Pinned when the answer echoed them, derived only on a first preparation:
|
|
99
87
|
// re-deriving either at `validate` renames the very unit the answer wrote to,
|
|
100
88
|
// and neither is workspace state that a stale check should be defending.
|
|
@@ -120,6 +108,9 @@ now = () => new Date()) {
|
|
|
120
108
|
...(selection.sessions !== undefined ? { sessions: selection.sessions } : {}),
|
|
121
109
|
...(selection.since !== undefined ? { since: selection.since } : {}),
|
|
122
110
|
...(selection.source !== undefined ? { source: selection.source } : {}),
|
|
111
|
+
...(selection.from !== undefined ? { from: selection.from } : {}),
|
|
112
|
+
...(selection.exclude !== undefined ? { exclude: selection.exclude } : {}),
|
|
113
|
+
...(selection.environment !== undefined ? { environment: selection.environment } : {}),
|
|
123
114
|
date,
|
|
124
115
|
next,
|
|
125
116
|
};
|
|
@@ -130,18 +121,34 @@ now = () => new Date()) {
|
|
|
130
121
|
required: policy.required,
|
|
131
122
|
extensions: policy.extensions,
|
|
132
123
|
overwritable: policy.overwritable,
|
|
133
|
-
|
|
124
|
+
// What the material was composed from, and what stayed in and out of it —
|
|
125
|
+
// declared BEFORE anything is composed, which is the only moment at which
|
|
126
|
+
// the person can still disagree with the origin.
|
|
127
|
+
origins: material.origins,
|
|
128
|
+
sessions: material.sessions,
|
|
129
|
+
bundles: material.bundles,
|
|
130
|
+
standalone_sql: material.standalone,
|
|
131
|
+
excluded: material.excluded,
|
|
132
|
+
exclude_unmatched: material.unmatched,
|
|
133
|
+
environment: material.environment,
|
|
134
134
|
date,
|
|
135
135
|
};
|
|
136
|
-
const readSet =
|
|
136
|
+
const readSet = materialPaths(material);
|
|
137
137
|
const request = buildSemanticRequest({
|
|
138
138
|
operation: `export-${category}`,
|
|
139
|
-
// What the seal defends is workspace state: the
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
// What the seal defends is workspace state: the MATERIAL the scope covers —
|
|
140
|
+
// sessions, loose SQL and previously published bundles alike, since any of
|
|
141
|
+
// them appearing or changing changes what the dossier should have contained
|
|
142
|
+
// — and the folder this workspace publishes to. The scope rides along so an
|
|
143
|
+
// altered echo cannot pass as the original one.
|
|
144
|
+
inputs: {
|
|
145
|
+
corpus: material.sessions,
|
|
146
|
+
bundles: material.bundles,
|
|
147
|
+
standalone: material.standalone,
|
|
148
|
+
dir: policy.dir,
|
|
149
|
+
scope,
|
|
150
|
+
},
|
|
151
|
+
sealed: "el material del alcance o el destino declarado de la categoría",
|
|
145
152
|
scope,
|
|
146
153
|
contract: `${policy.contract} Respondé artifacts con paths dentro de ${unit}${policy.overwritable === null ? "" : ` (o exactamente ${policy.overwritable})`}. El NNN es consultivo: el CLI reasigna el número dentro del lock. Copiá 'scope' TAL CUAL en tu respuesta: validate y apply lo leen en vez de re-derivarlo.`,
|
|
147
154
|
inventory,
|
|
@@ -162,12 +169,104 @@ function resolvePolicy(category, dir) {
|
|
|
162
169
|
overwritable: overwritable === undefined ? null : `${resolved}/${overwritable}`,
|
|
163
170
|
};
|
|
164
171
|
}
|
|
165
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The composable origin belongs to the SQL bundle and to nothing else.
|
|
174
|
+
*
|
|
175
|
+
* The other three categories publish documents an author writes; there is no
|
|
176
|
+
* previous manual to re-consolidate and no environment a diagram ran against.
|
|
177
|
+
* Accepting the flags there would answer a question those categories never ask.
|
|
178
|
+
*/
|
|
179
|
+
function checkComposableSelection(category, selection) {
|
|
180
|
+
// Belonging comes FIRST: telling a category that does not compose its origin
|
|
181
|
+
// which bases exist would send it to fix a value that was never going to be
|
|
182
|
+
// read, and it would be rejected again on the next invocation.
|
|
183
|
+
const named = ["from", "exclude", "environment"].filter((key) => selection[key] !== undefined);
|
|
184
|
+
if (category !== "scripts" && named.length > 0) {
|
|
185
|
+
return {
|
|
186
|
+
code: "EXPORT_SCOPE_INVALID",
|
|
187
|
+
message: `${named.map((k) => `--${k}`).join(", ")} es del bundle de SQL: export-${category} parte siempre del corpus de sesiones`,
|
|
188
|
+
action: "quitá esos flags, o usá aw export-scripts si lo que querés componer es el bundle",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Rejected HERE and not when the base is read, for the same reason a malformed
|
|
192
|
+
// `--date` is: the invocation that supplied it is the one that can fix it, and
|
|
193
|
+
// an unknown base silently read as one of the three would compose a different
|
|
194
|
+
// origin than the one that was asked for.
|
|
195
|
+
if (selection.from !== undefined && !EXPORT_BASES.includes(selection.from)) {
|
|
196
|
+
return {
|
|
197
|
+
code: "EXPORT_SCOPE_INVALID",
|
|
198
|
+
message: `--from '${selection.from}' no es una base: ${EXPORT_BASES.join(", ")}`,
|
|
199
|
+
action: "repetí la invocación con una de las tres bases, o sin --from para partir de las sesiones",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The material this preparation covers, or the reason there is none.
|
|
206
|
+
*
|
|
207
|
+
* The three ways it can fail — a base that is not one, an origin this category
|
|
208
|
+
* does not compose, and an origin that came back empty — answer the same
|
|
209
|
+
* question and travel together, so `prepare` reads as the sequence it is.
|
|
210
|
+
*/
|
|
211
|
+
async function resolveMaterial(fs, env, paths, category, selection) {
|
|
212
|
+
const invalid = checkComposableSelection(category, selection);
|
|
213
|
+
if (invalid !== null)
|
|
214
|
+
return { ok: false, failure: invalid };
|
|
215
|
+
const material = await composeMaterial(fs, env, paths, selection);
|
|
216
|
+
if ("error" in material) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
failure: {
|
|
220
|
+
code: "EXPORT_CORPUS_UNAVAILABLE",
|
|
221
|
+
message: material.error,
|
|
222
|
+
action: "revisá el workspace y los filtros --sessions/--since/--source",
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (materialCount(material) === 0)
|
|
227
|
+
return { ok: false, failure: emptyOrigin(material) };
|
|
228
|
+
return { ok: true, value: material };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Why the origin came back empty — and "everything already ran" is its own answer.
|
|
232
|
+
*
|
|
233
|
+
* Proposing a bundle with nothing in it would be the wrong outcome twice over:
|
|
234
|
+
* there is nothing to deliver, and the reason there is nothing is good news the
|
|
235
|
+
* person asked for. Folding it into the generic empty corpus would send them
|
|
236
|
+
* looking for a filter to widen.
|
|
237
|
+
*/
|
|
238
|
+
function emptyOrigin(material) {
|
|
239
|
+
const applied = material.excluded.filter((item) => item.reason === "applied");
|
|
240
|
+
if (applied.length > 0 && material.environment !== null) {
|
|
241
|
+
return {
|
|
242
|
+
code: "EXPORT_ORIGIN_ALREADY_APPLIED",
|
|
243
|
+
message: `todo el material que quedaba en el origen ya consta aplicado en '${material.environment.name}': no hay nada que consolidar`,
|
|
244
|
+
action: `nada que hacer; si igual querés reconsolidarlo, repetí la invocación sin --environment ${material.environment.name}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
code: "EXPORT_CORPUS_EMPTY",
|
|
249
|
+
message: `ningún material del origen (${material.origins.join(", ")}) coincide con los filtros`,
|
|
250
|
+
action: "ampliá --since, quitá --sessions o --exclude, probá otro --from, o revisá que existan sesiones cerradas",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The material of this preparation: a base brings, the exclusions subtract.
|
|
255
|
+
*
|
|
256
|
+
* Both halves in one place, because "where did this come from" and "why is this
|
|
257
|
+
* not here" are the two questions `prepare` has to answer together — and the
|
|
258
|
+
* listings the bases read are the ones `release-data` already produces, walked
|
|
259
|
+
* once by it rather than twice by this.
|
|
260
|
+
*/
|
|
261
|
+
async function composeMaterial(fs, env, paths, selection) {
|
|
262
|
+
const base = selection.from ?? "sessions";
|
|
166
263
|
const input = {
|
|
167
264
|
includeClosed: true,
|
|
168
|
-
//
|
|
169
|
-
// what already lives
|
|
170
|
-
|
|
265
|
+
// A base of `sessions` is the behavior that always was: graduated bundles are
|
|
266
|
+
// previous exports and re-exporting them would duplicate what already lives
|
|
267
|
+
// in docs/. The other bases are asking for exactly that material.
|
|
268
|
+
includeGraduated: base !== "sessions",
|
|
269
|
+
includeStandaloneSql: base === "workspace",
|
|
171
270
|
...(selection.sessions !== undefined ? { sessions: selection.sessions } : {}),
|
|
172
271
|
...(selection.since !== undefined ? { since: selection.since } : {}),
|
|
173
272
|
...(selection.source !== undefined ? { sourceAlias: selection.source } : {}),
|
|
@@ -175,7 +274,104 @@ async function readCorpus(fs, env, paths, selection) {
|
|
|
175
274
|
const data = await runReleaseData(fs, env, paths, input);
|
|
176
275
|
if ("error" in data)
|
|
177
276
|
return { error: data.error };
|
|
178
|
-
|
|
277
|
+
const sessions = base === "bundles" ? [] : data.sessions;
|
|
278
|
+
const standalone = base === "workspace" ? (data.standalone_sql ?? []) : [];
|
|
279
|
+
const bundles = base === "sessions" ? [] : (data.graduated_bundles ?? []);
|
|
280
|
+
const origins = [
|
|
281
|
+
...(base === "bundles" ? [] : ["sessions"]),
|
|
282
|
+
...(base === "workspace" ? ["standalone-sql"] : []),
|
|
283
|
+
...(base === "sessions" ? [] : ["bundles"]),
|
|
284
|
+
];
|
|
285
|
+
const named = new Set(selection.exclude ?? []);
|
|
286
|
+
const manual = [
|
|
287
|
+
...sessions.map((s) => piece("sessions", s.folder, s.path ?? s.folder)),
|
|
288
|
+
...standalone.map((f) => piece("standalone-sql", f.name, f.path)),
|
|
289
|
+
...bundles.map((b) => piece("bundles", bundleName(b), b.path)),
|
|
290
|
+
]
|
|
291
|
+
.filter((item) => named.has(item.name))
|
|
292
|
+
.map((item) => ({ ...item, reason: "manual" }));
|
|
293
|
+
// A name that subtracted nothing is DECLARED, not rejected: the base or the
|
|
294
|
+
// filters may have left that piece out already, and refusing an invocation
|
|
295
|
+
// whose intent is served would be hostile. But a typo looks identical from
|
|
296
|
+
// here, and staying silent would ship the very material the person believed
|
|
297
|
+
// they had taken out — which is the failure this whole command exists against.
|
|
298
|
+
const matched = new Set(manual.map((item) => item.name));
|
|
299
|
+
const unmatched = [...named].filter((name) => !matched.has(name));
|
|
300
|
+
// The environment subtracts from what the manual exclusions already left, and
|
|
301
|
+
// by the SAME road: both are exclusions of pieces and differ only in the
|
|
302
|
+
// reason the inventory declares, which is the whole of D-05.
|
|
303
|
+
const kept = bundles.filter((b) => !named.has(bundleName(b)));
|
|
304
|
+
const environment = await filterByEnvironment(fs, paths, selection.environment, kept);
|
|
305
|
+
const excluded = [...manual, ...environment.excluded];
|
|
306
|
+
const out = new Set(excluded.map((item) => item.name));
|
|
307
|
+
return {
|
|
308
|
+
origins,
|
|
309
|
+
sessions: sessions.filter((s) => !out.has(s.folder)),
|
|
310
|
+
bundles: bundles.filter((b) => !out.has(bundleName(b))),
|
|
311
|
+
standalone: standalone.filter((f) => !out.has(f.name)),
|
|
312
|
+
excluded,
|
|
313
|
+
unmatched,
|
|
314
|
+
environment: environment.filter,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Which of these bundles the book says already ran against this environment.
|
|
319
|
+
*
|
|
320
|
+
* The chain adds no new piece: a pass LINKS the bundle by workspace-relative
|
|
321
|
+
* path and that same pass has an application for the environment. One record is
|
|
322
|
+
* enough — a bundle linked to two passes where only one ran there did run, and
|
|
323
|
+
* demanding unanimity would re-deliver SQL that is already in place, which is
|
|
324
|
+
* the error this filter exists to prevent.
|
|
325
|
+
*/
|
|
326
|
+
async function filterByEnvironment(fs, paths, environment, bundles) {
|
|
327
|
+
if (environment === undefined)
|
|
328
|
+
return { excluded: [], filter: null };
|
|
329
|
+
const passes = derivePasses((await readReleasePasses(fs, paths)).events);
|
|
330
|
+
const there = passes.filter((derived) => derived.application.axis === "applied" &&
|
|
331
|
+
derived.application.environments.includes(environment));
|
|
332
|
+
// Both sides normalized to `/`: the book stores the path a person typed and
|
|
333
|
+
// this one comes from the filesystem, so on Windows the same bundle would be
|
|
334
|
+
// `docs/scripts/…` in one and `docs\\scripts\\…` in the other and nothing would
|
|
335
|
+
// ever match — every bundle would read as pending and be delivered twice.
|
|
336
|
+
const linked = new Set(there.flatMap((derived) => derived.artifacts).map(slashed));
|
|
337
|
+
const excluded = bundles
|
|
338
|
+
.filter((bundle) => linked.has(slashed(relative(paths.workspaceDir(), bundle.path))))
|
|
339
|
+
.map((bundle) => ({
|
|
340
|
+
...piece("bundles", bundleName(bundle), bundle.path),
|
|
341
|
+
reason: "applied",
|
|
342
|
+
}));
|
|
343
|
+
return {
|
|
344
|
+
excluded,
|
|
345
|
+
filter: {
|
|
346
|
+
name: environment,
|
|
347
|
+
axis: there.length === 0 ? "no-record" : "applied",
|
|
348
|
+
scanned: bundles.length,
|
|
349
|
+
excluded: excluded.length,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** One spelling for a path that two different producers wrote. */
|
|
354
|
+
function slashed(path) {
|
|
355
|
+
return path.split(sep).join("/");
|
|
356
|
+
}
|
|
357
|
+
/** What names a bundle in `--exclude` and in the inventory: its directory. */
|
|
358
|
+
function bundleName(bundle) {
|
|
359
|
+
return basename(bundle.path);
|
|
360
|
+
}
|
|
361
|
+
function piece(origin, name, path) {
|
|
362
|
+
return { origin, name, path };
|
|
363
|
+
}
|
|
364
|
+
/** How many pieces stayed in. Zero is an empty origin, whatever the base was. */
|
|
365
|
+
function materialCount(material) {
|
|
366
|
+
return material.sessions.length + material.bundles.length + material.standalone.length;
|
|
367
|
+
}
|
|
368
|
+
/** Everything the composer has to read, across the three origins. */
|
|
369
|
+
function materialPaths(material) {
|
|
370
|
+
return [
|
|
371
|
+
...material.sessions.map((s) => s.path ?? s.folder),
|
|
372
|
+
...material.standalone.map((f) => f.path),
|
|
373
|
+
...material.bundles.map((b) => b.path),
|
|
374
|
+
];
|
|
179
375
|
}
|
|
180
376
|
// ── the scope, travelling between stages ─────────────────────────────────────
|
|
181
377
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -205,6 +401,9 @@ export function readExportScope(raw) {
|
|
|
205
401
|
...(scope.sessions !== undefined ? { sessions: scope.sessions } : {}),
|
|
206
402
|
...(scope.since !== undefined ? { since: scope.since } : {}),
|
|
207
403
|
...(scope.source !== undefined ? { source: scope.source } : {}),
|
|
404
|
+
...(scope.from !== undefined ? { from: scope.from } : {}),
|
|
405
|
+
...(scope.exclude !== undefined ? { exclude: scope.exclude } : {}),
|
|
406
|
+
...(scope.environment !== undefined ? { environment: scope.environment } : {}),
|
|
208
407
|
date: scope.date,
|
|
209
408
|
next: scope.next,
|
|
210
409
|
},
|
|
@@ -221,7 +420,13 @@ function scopeShapeError(scope) {
|
|
|
221
420
|
if (scope.sessions !== undefined && !isStringArray(scope.sessions)) {
|
|
222
421
|
return "'sessions' tiene que ser una lista de códigos de texto";
|
|
223
422
|
}
|
|
224
|
-
|
|
423
|
+
if (scope.exclude !== undefined && !isStringArray(scope.exclude)) {
|
|
424
|
+
return "'exclude' tiene que ser una lista de nombres de texto";
|
|
425
|
+
}
|
|
426
|
+
if (scope.from !== undefined && !EXPORT_BASES.includes(scope.from)) {
|
|
427
|
+
return `'from' tiene que ser ${EXPORT_BASES.join(", ")}`;
|
|
428
|
+
}
|
|
429
|
+
for (const key of ["since", "source", "environment"]) {
|
|
225
430
|
if (scope[key] !== undefined && typeof scope[key] !== "string") {
|
|
226
431
|
return `'${key}' tiene que ser texto`;
|
|
227
432
|
}
|
|
@@ -255,10 +460,15 @@ export function conflictingScopeFlags(echoed, flags) {
|
|
|
255
460
|
if (flags.sessions !== undefined && !same(flags.sessions, echoed.sessions)) {
|
|
256
461
|
conflicts.push("--sessions");
|
|
257
462
|
}
|
|
463
|
+
if (flags.exclude !== undefined && !same(flags.exclude, echoed.exclude)) {
|
|
464
|
+
conflicts.push("--exclude");
|
|
465
|
+
}
|
|
258
466
|
for (const [flag, key] of [
|
|
259
467
|
["--since", "since"],
|
|
260
468
|
["--source", "source"],
|
|
261
469
|
["--date", "date"],
|
|
470
|
+
["--from", "from"],
|
|
471
|
+
["--environment", "environment"],
|
|
262
472
|
]) {
|
|
263
473
|
if (flags[key] !== undefined && flags[key] !== echoed[key])
|
|
264
474
|
conflicts.push(flag);
|
|
@@ -367,9 +577,15 @@ export async function applyExport(fs, env, paths, input) {
|
|
|
367
577
|
const artifacts = (parsed.value.artifacts ?? []).map((artifact) => renumber(artifact, input.prepared, minted, policy));
|
|
368
578
|
// Whole dossier or nothing: `publishArtifacts` restores every previous
|
|
369
579
|
// state on the first failure.
|
|
370
|
-
|
|
580
|
+
const published = await publishArtifacts(fs, paths.workspaceDir(), artifacts, {
|
|
371
581
|
overwrite: input.allowOverwrite === true,
|
|
372
582
|
});
|
|
583
|
+
// Under the SAME lock as the write, for the same reason as in `persist`: two
|
|
584
|
+
// concurrent publications outside it would lose one of the two rows.
|
|
585
|
+
if (published.ok) {
|
|
586
|
+
await appendPublications(fs, paths.cwdHistoryFile(), publicationRows(published.value.written, `export-${input.prepared.category}`));
|
|
587
|
+
}
|
|
588
|
+
return published;
|
|
373
589
|
});
|
|
374
590
|
if ("error" in result) {
|
|
375
591
|
return {
|