slidemind 0.3.20 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
package/src/cli/deck.mjs
CHANGED
|
@@ -1,17 +1,92 @@
|
|
|
1
|
+
import { draftLineTier, imageCountLines, lineShapeLines, listLines, missingAssetLines, repeatLines } from "../builders/draft-lines.mjs";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
3
5
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
6
|
+
import { requireAuthorizedEdits, seedRenderedSlides } from "./revision.mjs";
|
|
7
|
+
import { scopedSlideFiles } from "../lib/edit-transitions.mjs";
|
|
8
|
+
import { fail, loadBridgeModule, requirePlanApproved, requireReviewApproved, requireSlidesDraftChecked, requireSlidesPreserved, resolveDeckArg, runNode, splitDeckArg, renderProgressPrinter, dataUriToPng, freshenDeckBundle, withUnboundHint, SRC_DIR, BUILDERS_DIR } from "./common.mjs";
|
|
9
|
+
import { readDesignName } from "./designs.mjs";
|
|
10
|
+
import { deckSlug, previewPath, readPlanOwner, statePath } from "../lib/deck-paths.mjs";
|
|
11
|
+
import { currentSpec } from "../lib/concept-spec-binding.mjs";
|
|
5
12
|
|
|
6
13
|
export async function cmdPlan(rest) {
|
|
7
14
|
const { deck, passthrough } = splitDeckArg(rest, "plan");
|
|
15
|
+
const deckDir = resolveDeckArg(deck);
|
|
16
|
+
// A plan written one directory up (the deck folder instead of its
|
|
17
|
+
// workspace) left the gate approving an empty record and `concepts`
|
|
18
|
+
// generating nothing, four rounds in a row (x51 cloud). Say where it goes.
|
|
19
|
+
const record = path.join(deckDir, "plan.md");
|
|
20
|
+
const slides = (f) => { try { return /```storyboard-canvas/.test(fs.readFileSync(f, "utf8")); } catch { return false; } };
|
|
21
|
+
const stray = path.join(path.dirname(deckDir), "plan.md");
|
|
22
|
+
if (!slides(record) && stray !== record && slides(stray)) {
|
|
23
|
+
fail(`plan: the deck's plan record is ${record}, which has no storyboard-canvas slides; ${stray} has them. Move it (mv "${stray}" "${record}") and re-run \`slidemind plan ${deckSlug(deckDir)}\`.`);
|
|
24
|
+
}
|
|
25
|
+
// The agent writes the plan; the person reviews it. A record with no slides is
|
|
26
|
+
// never asked about: a gate that approves itself then made `concepts` generate
|
|
27
|
+
// nothing (7 of 9 x44–x52 batches), and with a person it handed them the
|
|
28
|
+
// scaffold's placeholder to review (a Codex agent chained `new && plan`,
|
|
29
|
+
// 2026-10-11). An extension-owned plan (a proposal pack) keeps its record
|
|
30
|
+
// elsewhere and its plan.md is deck metadata only: there are no storyboards.
|
|
31
|
+
const owner = readPlanOwner(deckDir)?.owner;
|
|
32
|
+
if (!slides(record) && (!owner || owner === "core")) {
|
|
33
|
+
fail(`plan: ${record} has no slides yet — write \`## Red thread\` and \`## Per-slide storyboards\` with one \`\`\`storyboard-canvas JSON block per slide (the design bundle's workflow/plan.md has the exact shape), then re-run \`slidemind plan ${deckSlug(deckDir)}\`.`);
|
|
34
|
+
}
|
|
35
|
+
if (!owner || owner === "core") {
|
|
36
|
+
if ((await ensurePlanDesign(deckDir)) === "blocked") return;
|
|
37
|
+
}
|
|
8
38
|
await runNode(path.join(BUILDERS_DIR, "plan.mjs"), [
|
|
9
39
|
"--deck",
|
|
10
|
-
|
|
40
|
+
deckDir,
|
|
11
41
|
...passthrough,
|
|
12
42
|
]);
|
|
13
43
|
}
|
|
14
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The plan page opens on the deck's design, so the deck must have one before it
|
|
47
|
+
* is asked about. A deck nobody bound (the agent wrote the plan without `new
|
|
48
|
+
* --brand`) is bound here the way `new` binds: the one design open in
|
|
49
|
+
* PowerPoint, else the one design cached on this machine. With several at hand
|
|
50
|
+
* the user named one — the agent binds it (`ready <deck> --brand`) and asks
|
|
51
|
+
* again: BLOCKED, exit 2. Nothing at hand (offline, nothing downloaded) leaves
|
|
52
|
+
* the choice to the page's own menu.
|
|
53
|
+
*/
|
|
54
|
+
async function ensurePlanDesign(deckDir) {
|
|
55
|
+
const bundle = await loadBridgeModule("bundle");
|
|
56
|
+
if (bundle.readDeckBundleRef(deckDir)?.presFP) return "bound";
|
|
57
|
+
const cfg = await loadBridgeModule("config");
|
|
58
|
+
if (cfg.readSessions().length === 0) return "none";
|
|
59
|
+
const cached = bundle.listCachedBundles();
|
|
60
|
+
let presFP = null;
|
|
61
|
+
try {
|
|
62
|
+
const client = await loadBridgeModule("client");
|
|
63
|
+
const { presentations } = await client.listOpenPresentationsDetailed();
|
|
64
|
+
const resolution = client.resolveDesignSource(presentations, {});
|
|
65
|
+
if (resolution.status === "resolved") presFP = resolution.entry.brandId;
|
|
66
|
+
} catch {
|
|
67
|
+
// no window to ask: the cache decides
|
|
68
|
+
}
|
|
69
|
+
if (!presFP && cached.length === 1) presFP = cached[0].presFP;
|
|
70
|
+
const meta = presFP ? bundle.readMaterializedBundleMeta(presFP) : null;
|
|
71
|
+
if (meta?.presFP) {
|
|
72
|
+
bundle.writeDeckBundleRef(deckDir, { presFP: meta.presFP, etag: meta.etag ?? null });
|
|
73
|
+
process.stderr.write(`slidemind: bound ${deckSlug(deckDir)} to design ${readDesignName(bundle.bundleDirForPresFP(meta.presFP)) ?? meta.presFP}\n`);
|
|
74
|
+
return "bound";
|
|
75
|
+
}
|
|
76
|
+
if (cached.length < 2) return "none";
|
|
77
|
+
const slug = deckSlug(deckDir);
|
|
78
|
+
const list = cached.map((b) => `${readDesignName(b.dir) ?? b.presFP} (${b.presFP})`).join(", ");
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
"BLOCKED: deck_unbound\n" +
|
|
81
|
+
`${slug} has no corporate design yet and several are at hand, so the plan page could not open on one. ` +
|
|
82
|
+
"Bind it to the design the user asked for, then run this again:\n" +
|
|
83
|
+
` slidemind ready ${slug} --brand <presFP>\n` +
|
|
84
|
+
`Designs on this machine: ${list}. The user named none? Ask which one — \`slidemind ls designs\` lists every design the backend serves.\n`,
|
|
85
|
+
);
|
|
86
|
+
process.exitCode = 2;
|
|
87
|
+
return "blocked";
|
|
88
|
+
}
|
|
89
|
+
|
|
15
90
|
export async function cmdReview(rest) {
|
|
16
91
|
const { deck, passthrough } = splitDeckArg(rest, "review");
|
|
17
92
|
const deckDir = resolveDeckArg(deck);
|
|
@@ -86,7 +161,7 @@ export async function cmdImagen(rest) {
|
|
|
86
161
|
const ref = bundleMod.readDeckBundleRef(deckDir);
|
|
87
162
|
if (!ref?.presFP) {
|
|
88
163
|
fail(
|
|
89
|
-
`deck ${
|
|
164
|
+
`deck ${deckSlug(deckDir)} is not bound to a design yet — run ` +
|
|
90
165
|
`'slidemind ready ${deck}' first (imagen routes by the deck's bound design).`,
|
|
91
166
|
);
|
|
92
167
|
}
|
|
@@ -231,11 +306,17 @@ export async function cmdBuildPptx(rest) {
|
|
|
231
306
|
let outPath = null;
|
|
232
307
|
let packDir = null;
|
|
233
308
|
let force = false;
|
|
309
|
+
let refreshDesign = false;
|
|
234
310
|
let asJson = false;
|
|
311
|
+
let publish = true;
|
|
235
312
|
for (let i = 0; i < passthrough.length; i++) {
|
|
236
313
|
const a = passthrough[i];
|
|
237
314
|
if (a === "--force") {
|
|
238
315
|
force = true;
|
|
316
|
+
} else if (a === "--no-publish") {
|
|
317
|
+
publish = false;
|
|
318
|
+
} else if (a === "--refresh-design") {
|
|
319
|
+
refreshDesign = true;
|
|
239
320
|
} else if (a === "--json") {
|
|
240
321
|
asJson = true;
|
|
241
322
|
} else if (a === "--to") {
|
|
@@ -272,9 +353,16 @@ export async function cmdBuildPptx(rest) {
|
|
|
272
353
|
fail(`explicit --pack ${path.resolve(packDir)} is mode ${deckMode.mode}, not fill-template`);
|
|
273
354
|
}
|
|
274
355
|
// Template packs have no HTML preview to review and carry their own plan
|
|
275
|
-
// sign-off; compose decks ship only
|
|
356
|
+
// sign-off; compose decks ship only once their preview has been approved
|
|
357
|
+
// (one-time per deck — a change after the approval is not re-reviewed).
|
|
276
358
|
if (deckMode.mode !== "fill-template") {
|
|
277
359
|
await requireReviewApproved(deckDir);
|
|
360
|
+
requireAuthorizedEdits(deckDir);
|
|
361
|
+
await requireSlidesPreserved(deckDir);
|
|
362
|
+
const { deliveredSlideFiles } = await import(
|
|
363
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
364
|
+
);
|
|
365
|
+
await requireSlidesDraftChecked(deckDir, { files: deliveredSlideFiles(deckDir, { refreshDesign }) });
|
|
278
366
|
await freshenDeckBundle(deckDir);
|
|
279
367
|
}
|
|
280
368
|
if (deckMode.mode === "fill-template") {
|
|
@@ -342,35 +430,78 @@ export async function cmdBuildPptx(rest) {
|
|
|
342
430
|
presentationId: to,
|
|
343
431
|
outPath,
|
|
344
432
|
force,
|
|
433
|
+
refreshDesign,
|
|
345
434
|
onProgress: renderProgressPrinter(),
|
|
346
435
|
})
|
|
347
436
|
.catch((err) => fail(withUnboundHint(deckDir, err)));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
)
|
|
352
|
-
|
|
437
|
+
// The package's measured lines, classed by the shared table as the add-in's
|
|
438
|
+
// build classes them.
|
|
439
|
+
const { splitPackageLines, packageDefectsText } = await import(
|
|
440
|
+
pathToFileURL(path.join(BUILDERS_DIR, "draft-check.mjs")).href
|
|
441
|
+
);
|
|
442
|
+
const { defects: packageDefects, notes: packageNotes } = splitPackageLines(result.warnings);
|
|
443
|
+
if (packageDefects.length > 0) process.exitCode = 1;
|
|
444
|
+
if (!asJson) {
|
|
353
445
|
process.stdout.write(
|
|
354
446
|
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
|
|
355
447
|
`${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
|
|
356
448
|
);
|
|
357
|
-
|
|
358
|
-
if (
|
|
449
|
+
const preview = result.packagePreview?.status === "rendered" ? result.packagePreview : result.preview;
|
|
450
|
+
if (preview?.dir) {
|
|
359
451
|
process.stdout.write(
|
|
360
|
-
` ${
|
|
452
|
+
` ${preview.files.length} ${preview.kind === "pptx" ? "exported PPTX" : "browser source"} slide png(s) in ${path.relative(process.cwd(), preview.dir)}/ — judge a <Chart> here (view-draft paints a stand-in); the concept comparison was the draft check's\n`,
|
|
361
453
|
);
|
|
362
|
-
} else if (result.preview?.note) {
|
|
363
|
-
process.stdout.write(` ${result.preview.note}\n`);
|
|
364
454
|
}
|
|
455
|
+
if (result.packagePreview?.note) process.stderr.write(` ${result.packagePreview.note}\n`);
|
|
456
|
+
if (packageDefects.length > 0) process.stdout.write(packageDefectsText(packageDefects, deckSlug(deckDir)));
|
|
365
457
|
}
|
|
366
|
-
for (const w of
|
|
458
|
+
for (const w of packageNotes) {
|
|
367
459
|
process.stderr.write(` warning: ${w}\n`);
|
|
368
460
|
}
|
|
369
461
|
for (const s of result.skippedSlides ?? []) {
|
|
370
462
|
process.stderr.write(` skipped ${s.stem}: ${s.reason}\n`);
|
|
371
463
|
}
|
|
464
|
+
// A deck connected to SharePoint — the file it was opened from, or a folder
|
|
465
|
+
// the user connected in the workspace — is delivered THERE, so the build
|
|
466
|
+
// publishes the candidate it just wrote. The connection is read from the
|
|
467
|
+
// deck's own state at every build: a folder connected mid-session is honoured
|
|
468
|
+
// by the next build without the agent having to know. `--no-publish` keeps a
|
|
469
|
+
// build local; `slidemind publish` retries one whose publish did not save.
|
|
470
|
+
// A build that ends on package defects is not ready for the user, so it does
|
|
471
|
+
// not go to SharePoint either: fixed and rebuilt, that build publishes.
|
|
472
|
+
const published = publish && result.candidate
|
|
473
|
+
? await (await import("./remote.mjs")).publishAfterBuild(deckDir, {
|
|
474
|
+
candidateId: result.candidate.candidateId,
|
|
475
|
+
withheld: packageDefects.length > 0
|
|
476
|
+
? { reason: "package_defects", message: `held back: ${packageDefects.length} package defect(s) — fix them and build again` }
|
|
477
|
+
: null,
|
|
478
|
+
})
|
|
479
|
+
: null;
|
|
480
|
+
if (published) process.stderr.write(published.text);
|
|
481
|
+
if (asJson) {
|
|
482
|
+
process.stdout.write(
|
|
483
|
+
`${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount ?? null, presId: result.presId ?? null, windowless: Boolean(result.windowless), packageDefects, warnings: result.warnings ?? [], skippedSlides: result.skippedSlides ?? [], targeted: result.targeted ?? null, preview: result.preview ?? null, packagePreview: result.packagePreview ?? null, publish: published?.json ?? null }, null, 2)}\n`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (published?.exitCode) process.exit(published.exitCode);
|
|
372
487
|
}
|
|
373
488
|
|
|
489
|
+
// `transcribe` names every asset it fetches `NN-<element id>` for the slide it
|
|
490
|
+
// fetched it FOR, and recolours it for THAT slide's background. So a slide that
|
|
491
|
+
// points at another slide's numbered asset is not reusing imagery, it is
|
|
492
|
+
// clearing an `[asset:error]` with the nearest file on disk — x44: nine such
|
|
493
|
+
// refs, four white glyphs landed on white cards and one blue glyph on a blue
|
|
494
|
+
// disc, all invisible, on the two worst-scoring slides of the deck. Existence
|
|
495
|
+
// is not evidence that the file is the right glyph, or legible where it now
|
|
496
|
+
// sits. Returns the owning slide number, or null when the reference is fine:
|
|
497
|
+
// an unnumbered asset (a deck image used on several slides) is never borrowed,
|
|
498
|
+
// and neither is a slide file that carries no number of its own.
|
|
499
|
+
export { borrowedAssetOwner } from "../builders/draft-lines.mjs";
|
|
500
|
+
|
|
501
|
+
// What a concept is to the draft check, in both outputs (text and --json).
|
|
502
|
+
// Same words as the add-in's see_draft caption (build-and-push.ts).
|
|
503
|
+
const CONCEPT_GUIDANCE = "the blueprint, not the target: every element it shows inside the content slot is on the render as that kind of thing (the same hero, grouping, order, hierarchy and focal element), every connector present with its line shape (straight, elbow, curve or arc) and one shape per cycle or fan. Build it cleaner than the painting: siblings the same size, even gaps, straight edges straight, round forms round, token colours; a cleaner, aligned build of the same elements needs no change. Delete what the painting drew by accident or in the template's chrome (a footer mark, a guide box, a stray stroke, a second wordmark) (workflow/create.md, Execute, don't trace).";
|
|
504
|
+
|
|
374
505
|
export async function cmdViewDraft(rest) {
|
|
375
506
|
const { deck, passthrough } = splitDeckArg(rest, "view-draft");
|
|
376
507
|
let to = null;
|
|
@@ -382,7 +513,11 @@ export async function cmdViewDraft(rest) {
|
|
|
382
513
|
// with a note when the backend is unreachable; --translate insists, and
|
|
383
514
|
// --no-translate keeps the check purely local.
|
|
384
515
|
let translate = "auto";
|
|
385
|
-
|
|
516
|
+
// Autofix is the default: the deterministic token conversions (raw px →
|
|
517
|
+
// --sp-*/--fs-*, em → px) were the largest lint class agents fought by hand
|
|
518
|
+
// (137 raw-spacing warnings on one 8-slide deck, x20 2026-09-18) and carry
|
|
519
|
+
// no design decision. --no-fix keeps a file byte-exact for inspection.
|
|
520
|
+
let fix = true;
|
|
386
521
|
let lintOnly = false;
|
|
387
522
|
let asJson = false;
|
|
388
523
|
// Converted slides only: promote the dialect lint's warnings (an empty
|
|
@@ -392,6 +527,8 @@ export async function cmdViewDraft(rest) {
|
|
|
392
527
|
const a = passthrough[i];
|
|
393
528
|
if (a === "--fix") {
|
|
394
529
|
fix = true;
|
|
530
|
+
} else if (a === "--no-fix") {
|
|
531
|
+
fix = false;
|
|
395
532
|
} else if (a === "--strict") {
|
|
396
533
|
strict = true;
|
|
397
534
|
} else if (a === "--lint-only") {
|
|
@@ -431,15 +568,22 @@ export async function cmdViewDraft(rest) {
|
|
|
431
568
|
only = onlySpec.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
|
|
432
569
|
if (files.includes(t)) return t;
|
|
433
570
|
if (/^\d{1,3}$/.test(t)) {
|
|
434
|
-
|
|
571
|
+
// Numeric prefix first (`03-loop.jsx`); then any file whose first
|
|
572
|
+
// number is that slide (`slide-03.jsx`, `s03-loop.jsx`) — three sim
|
|
573
|
+
// sessions named files that way and burned 6–13 failed `--only`
|
|
574
|
+
// calls each on "no slide file matches".
|
|
575
|
+
const hit =
|
|
576
|
+
files.find((f) => Number(/^(\d{1,3})[-_.]/.exec(f)?.[1]) === Number(t)) ??
|
|
577
|
+
files.find((f) => Number(/(\d{1,3})/.exec(f)?.[1]) === Number(t));
|
|
435
578
|
if (hit) return hit;
|
|
436
579
|
}
|
|
437
580
|
fail(`--only: no slide file matches '${t}' under ${path.join(deckDir, "slides")}`);
|
|
438
581
|
});
|
|
439
582
|
}
|
|
583
|
+
requireAuthorizedEdits(deckDir);
|
|
440
584
|
await requirePlanApproved(deckDir);
|
|
441
585
|
await freshenDeckBundle(deckDir);
|
|
442
|
-
// The HTML build runs FIRST — it
|
|
586
|
+
// The HTML build runs FIRST — it lints the selected slides against the
|
|
443
587
|
// bundle's capability table and copies the bundle's corporate-design.css +
|
|
444
588
|
// assets into the deck, which the type-ramp check below reads. (This is the
|
|
445
589
|
// work the retired `slidemind build` command used to do in a separate call;
|
|
@@ -447,22 +591,35 @@ export async function cmdViewDraft(rest) {
|
|
|
447
591
|
const { buildDeck } = await import(
|
|
448
592
|
pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
|
|
449
593
|
);
|
|
450
|
-
|
|
594
|
+
// An open edit scope bounds slidemind's own rewrites too: the autofix of a
|
|
595
|
+
// slide outside it would be the out-of-scope change the gate refuses.
|
|
596
|
+
const fixOnly = only ?? scopedSlideFiles(deckDir);
|
|
597
|
+
if (fix && fixOnly?.length !== 0) {
|
|
451
598
|
const { fixDeckSlides } = await import(
|
|
452
599
|
pathToFileURL(path.join(BUILDERS_DIR, "lint-fix.mjs")).href
|
|
453
600
|
);
|
|
454
|
-
const fixed = fixDeckSlides(deckDir);
|
|
601
|
+
const fixed = fixDeckSlides(deckDir, { only: fixOnly });
|
|
455
602
|
if (fixed.fixes > 0) {
|
|
456
|
-
|
|
603
|
+
// stderr, like the `[fix:withheld]` lines below it: `--json` must parse
|
|
604
|
+
// as ONE document, and this sentence used to sit in front of the payload.
|
|
605
|
+
process.stderr.write(
|
|
457
606
|
`Autofixed ${fixed.fixes} lint issue(s) in ${fixed.files} slide file(s) — re-read any file before patching it.\n`,
|
|
458
607
|
);
|
|
459
608
|
}
|
|
609
|
+
// Source someone else owns is reported, never rewritten — otherwise the
|
|
610
|
+
// next preview quietly undoes an edit. Stderr, so --json stays one payload.
|
|
611
|
+
for (const w of fixed.withheld ?? []) {
|
|
612
|
+
process.stderr.write(
|
|
613
|
+
`[fix:withheld] ${w.file} — autofix (${w.fixes.join(", ")}) NOT applied: ${w.reason}. ` +
|
|
614
|
+
`Its lint findings below still stand; apply them by hand if you want them.\n`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
460
617
|
}
|
|
461
618
|
// Advisory here: the converted rules print as warnings while slides are
|
|
462
619
|
// still being edited, and block at `review` / `build-pptx`. --strict gates now.
|
|
463
|
-
await buildDeck(deckDir, { strict, enforceConverted: strict });
|
|
620
|
+
await buildDeck(deckDir, { strict, enforceConverted: strict, only });
|
|
464
621
|
if (lintOnly) {
|
|
465
|
-
if (asJson) process.stdout.write(`${JSON.stringify({ deck:
|
|
622
|
+
if (asJson) process.stdout.write(`${JSON.stringify({ deck: deckSlug(deckDir), lint: "clean" })}\n`);
|
|
466
623
|
else process.stdout.write("Lint clean (no render — drop --lint-only for the draft previews).\n");
|
|
467
624
|
return;
|
|
468
625
|
}
|
|
@@ -471,6 +628,9 @@ export async function cmdViewDraft(rest) {
|
|
|
471
628
|
const mod = await import(
|
|
472
629
|
pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
|
|
473
630
|
);
|
|
631
|
+
// A concurrent edit during capture cannot inherit a passing preview hash.
|
|
632
|
+
const capturedSources = new Map((only ?? fs.readdirSync(path.join(deckDir, "slides")).filter(f => f.endsWith(".jsx")))
|
|
633
|
+
.map(file => [file, fs.readFileSync(path.join(deckDir, "slides", file), "utf8")]));
|
|
474
634
|
// The capture engine lints every slide again and prints the same lines the
|
|
475
635
|
// build just did; mute that second copy (its warnings still ride the result).
|
|
476
636
|
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
@@ -482,8 +642,8 @@ export async function cmdViewDraft(rest) {
|
|
|
482
642
|
presentationId: to,
|
|
483
643
|
onProgress: renderProgressPrinter(),
|
|
484
644
|
only,
|
|
485
|
-
// The server's export gate (slot-fit, diagram geometry,
|
|
486
|
-
//
|
|
645
|
+
// The server's export gate (slot-fit, diagram geometry, text escape,
|
|
646
|
+
// chrome) on the captured slides, verdict only, no .pptx, no review
|
|
487
647
|
// gate — so a slide is proven buildable before hand-over.
|
|
488
648
|
translate,
|
|
489
649
|
});
|
|
@@ -499,7 +659,7 @@ export async function cmdViewDraft(rest) {
|
|
|
499
659
|
|
|
500
660
|
const previewDir = outDir
|
|
501
661
|
? path.resolve(process.cwd(), outDir)
|
|
502
|
-
:
|
|
662
|
+
: previewPath(deckDir, "draft");
|
|
503
663
|
fs.mkdirSync(previewDir, { recursive: true });
|
|
504
664
|
|
|
505
665
|
// The measured half of the draft check — the same mechanics the add-in's
|
|
@@ -518,11 +678,6 @@ export async function cmdViewDraft(rest) {
|
|
|
518
678
|
const file = path.join(conceptsDir, `concept-slide-${String(Number(m[1])).padStart(2, "0")}.png`);
|
|
519
679
|
return fs.existsSync(file) ? file : null;
|
|
520
680
|
};
|
|
521
|
-
const inventory = check.inventoryWarnings(
|
|
522
|
-
deckDir,
|
|
523
|
-
(result.slides ?? []).map((s) => s.fileName).filter((f) => f.endsWith(".jsx")),
|
|
524
|
-
);
|
|
525
|
-
const deckLevel = inventory.filter((l) => l.includes("] assets/"));
|
|
526
681
|
// The measured rules come from the add-in's capture engine in the bundle —
|
|
527
682
|
// the same fill / type-scale / overlap checks see_draft runs in-app.
|
|
528
683
|
const { loadDraftChecks } = await import(
|
|
@@ -537,24 +692,168 @@ export async function cmdViewDraft(rest) {
|
|
|
537
692
|
"the design bundle's capture engine exports no draft checks — the connected add-in predates them. Update the add-in deployment, then run `slidemind download-bundle <deck>` and retry.",
|
|
538
693
|
);
|
|
539
694
|
}
|
|
540
|
-
// Type ramp from the
|
|
695
|
+
// Type ramp from the bundle's css — the deck carries no copy of the design.
|
|
541
696
|
let typeTokens = null;
|
|
542
697
|
try {
|
|
698
|
+
const { resolveSkillForDeck } = await import(
|
|
699
|
+
pathToFileURL(path.join(BUILDERS_DIR, "resolve-skill.mjs")).href
|
|
700
|
+
);
|
|
543
701
|
typeTokens = rules.typeScaleTokens(
|
|
544
|
-
fs.readFileSync(
|
|
702
|
+
fs.readFileSync(resolveSkillForDeck(deckDir).cssFile, "utf8"),
|
|
545
703
|
);
|
|
546
704
|
} catch {
|
|
547
705
|
typeTokens = null;
|
|
548
706
|
}
|
|
707
|
+
// The tiers are the shared table's (draft-lines.mjs → draftLineTier). The
|
|
708
|
+
// measured rules below run on authored slides only, as in the add-in: a
|
|
709
|
+
// converted slide keeps its source deck's geometry. Fill and type (and the
|
|
710
|
+
// split advice) print on the deck-wide pass only — a stretched region is
|
|
711
|
+
// information about density, not a defect; body sizes are the agent's call.
|
|
549
712
|
const measuredLines = (stem, render) =>
|
|
550
713
|
[
|
|
551
|
-
...rules.fill([{ stem, render }]),
|
|
552
|
-
...rules.typeScale([{ stem, render }], typeTokens),
|
|
553
714
|
...rules.overlap([{ stem, render }]),
|
|
554
715
|
// Text leaving its painted card — engines from 2026-09-06 on; older
|
|
555
716
|
// ones simply have no rule here.
|
|
556
717
|
...(typeof rules.escape === "function" ? rules.escape([{ stem, render }]) : []),
|
|
718
|
+
...(typeof rules.chrome === "function" ? rules.chrome([{ stem, render }]) : []),
|
|
719
|
+
// A word PowerPoint would break inside its frame — engines from
|
|
720
|
+
// 2026-09-27 on.
|
|
721
|
+
...(typeof rules.wordFit === "function" ? rules.wordFit([{ stem, render }]) : []),
|
|
722
|
+
// One text set as separate boxes (a declared block split, a sentence
|
|
723
|
+
// run on across two boxes) — engines from 2026-10-08 on.
|
|
724
|
+
...(typeof rules.splitText === "function" ? rules.splitText([{ stem, render }]) : []),
|
|
557
725
|
].map((l) => l.trim());
|
|
726
|
+
const advisoryLines = (stem, render) =>
|
|
727
|
+
[
|
|
728
|
+
...rules.fill([{ stem, render }]),
|
|
729
|
+
...rules.typeScale([{ stem, render }], typeTokens),
|
|
730
|
+
// Pieces set close on one row that only the agent can tell apart from
|
|
731
|
+
// two columns — engines from 2026-10-08 on (`splitText` carries the
|
|
732
|
+
// blocking ones: one declared block in several boxes, a sentence run on).
|
|
733
|
+
...(typeof rules.splitTextAdvice === "function" ? rules.splitTextAdvice([{ stem, render }]) : []),
|
|
734
|
+
].map((l) => l.trim());
|
|
735
|
+
|
|
736
|
+
// The image line — the slide's own count contract, written by the agent
|
|
737
|
+
// right after it looked at the concept: `// image: nodes=5 arrows=5 charts=0
|
|
738
|
+
// icons=5`. Arrows are compared against authored data-connector directives,
|
|
739
|
+
// charts against <Chart> elements, icons against <img> tags. FEWER in the
|
|
740
|
+
// JSX than the line says is a recorded defect; the omission this catches
|
|
741
|
+
// (sol@low shipped a loop with zero arrows and a scorecard with zero charts
|
|
742
|
+
// on x22, 2026-09-19) is invisible to every other check. No line, no check.
|
|
743
|
+
// The first count the agent wrote is the one that binds: x25 showed sol
|
|
744
|
+
// resolving `icons=8, JSX has 0` by editing the line down to icons=0. A
|
|
745
|
+
// lowered count is itself a defect; raising one (a miscount upward) is fine.
|
|
746
|
+
const imageLinesFile = statePath(deckDir, "image-lines.json");
|
|
747
|
+
let imageLines = {};
|
|
748
|
+
try {
|
|
749
|
+
imageLines = JSON.parse(fs.readFileSync(imageLinesFile, "utf8"));
|
|
750
|
+
} catch {}
|
|
751
|
+
// Counts come from the RENDER, not the source: a `.map()` over one <img>
|
|
752
|
+
// renders eleven icons but greps as one, and that false shortfall is what
|
|
753
|
+
// taught sol to lower its counts (x24). The capture boxes carry `imgSrc`,
|
|
754
|
+
// a non-native `connector` payload and a `chart` payload per element.
|
|
755
|
+
// When the add-in extracted a concept spec (concepts/concept-slide-NN.spec.json),
|
|
756
|
+
// ITS counts are the expectation and the agent's own line is ignored: the
|
|
757
|
+
// spec is machine-read from the mockup and cannot be edited down.
|
|
758
|
+
const specMod = await import(pathToFileURL(path.join(BUILDERS_DIR, "concept-spec.mjs")).href);
|
|
759
|
+
// What each slide still OWES, by element identity. This is what the old
|
|
760
|
+
// `ownership()` gate has been replaced by: the spec's counts used to switch
|
|
761
|
+
// themselves OFF as soon as a slide's bytes stopped matching
|
|
762
|
+
// state/transcribed.json, which is precisely when an edit is most likely to
|
|
763
|
+
// have dropped something (E09). A preservation record never lapses on a
|
|
764
|
+
// rewrite, so a hand-edited slide is now checked, not excused.
|
|
765
|
+
// Seed first, as the add-in's render does: a slide without a record gets one
|
|
766
|
+
// from its current evidence before its verdict is computed.
|
|
767
|
+
if (!outDir) seedRenderedSlides(deckDir, (result.slides ?? []).map((s) => s.fileName));
|
|
768
|
+
const preserve = await check.preservationDiagnostics(
|
|
769
|
+
deckDir,
|
|
770
|
+
(result.slides ?? []).map((s) => ({ fileName: s.fileName, render: s.render })),
|
|
771
|
+
{ persist: !outDir },
|
|
772
|
+
);
|
|
773
|
+
// Counting elements is the weaker check of the two: where a record stands,
|
|
774
|
+
// every element is checked by its own id instead, and an intentional removal
|
|
775
|
+
// has a legal exit (a scoped amendment) that a count never had.
|
|
776
|
+
// What the reader flagged on the concept, and the template's own marks the
|
|
777
|
+
// mapper left out: the agent judges the pair with these in hand (the add-in's
|
|
778
|
+
// see_draft prints the same `[concept:flaw]` lines).
|
|
779
|
+
const conceptNotesFor = (fileName) => {
|
|
780
|
+
const m = /^(\d{1,3})[-_.]/.exec(fileName);
|
|
781
|
+
const notes = m ? currentSpec(deckDir, Number(m[1]))?.notes ?? [] : [];
|
|
782
|
+
return notes
|
|
783
|
+
.filter((n) => /^concept flaw —|^left out as the template's own marks/.test(n))
|
|
784
|
+
.map((n) => `[concept:flaw] ${n}`);
|
|
785
|
+
};
|
|
786
|
+
const specCountsFor = (fileName) => {
|
|
787
|
+
const m = /^(\d{1,3})[-_.]/.exec(fileName);
|
|
788
|
+
if (!m || preserve.covered.has(fileName)) return null;
|
|
789
|
+
return specMod.expectedCountsFromSpec(currentSpec(deckDir, Number(m[1])));
|
|
790
|
+
};
|
|
791
|
+
const imageLineDefects = (fileName, render) => {
|
|
792
|
+
let src = "";
|
|
793
|
+
try {
|
|
794
|
+
src = fs.readFileSync(path.join(deckDir, "slides", fileName), "utf8");
|
|
795
|
+
} catch {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
const fromSpec = specCountsFor(fileName);
|
|
799
|
+
const m = /^\s*\/\/\s*image:\s*([^\n]*)$/m.exec(src);
|
|
800
|
+
if (!fromSpec && !m) return [];
|
|
801
|
+
const want = {};
|
|
802
|
+
const lowered = [];
|
|
803
|
+
if (fromSpec) {
|
|
804
|
+
Object.assign(want, fromSpec);
|
|
805
|
+
} else {
|
|
806
|
+
for (const [, k, v] of m[1].matchAll(/\b(arrows|charts|icons)\s*=\s*(\d+)/g)) want[k] = Number(v);
|
|
807
|
+
const first = imageLines[fileName] ?? {};
|
|
808
|
+
for (const k of ["arrows", "charts", "icons"]) {
|
|
809
|
+
if (first[k] != null && want[k] != null && want[k] < first[k]) {
|
|
810
|
+
lowered.push(`${k} ${first[k]}→${want[k]}`);
|
|
811
|
+
want[k] = first[k];
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (!imageLines[fileName]) {
|
|
815
|
+
imageLines[fileName] = { ...want };
|
|
816
|
+
fs.mkdirSync(path.dirname(imageLinesFile), { recursive: true });
|
|
817
|
+
fs.writeFileSync(imageLinesFile, JSON.stringify(imageLines, null, 2) + "\n");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return imageCountLines({ fileName, src, render, want, fromSpec, lowered });
|
|
821
|
+
};
|
|
822
|
+
// Every rendered line keeps the concept's shape, hand-edited slides included.
|
|
823
|
+
const lineShapeDefects = (fileName, render) => {
|
|
824
|
+
const m = /^(\d{1,3})[-_.]/.exec(fileName);
|
|
825
|
+
if (!m) return [];
|
|
826
|
+
let spec = null;
|
|
827
|
+
try { spec = currentSpec(deckDir, Number(m[1])); } catch { return []; }
|
|
828
|
+
// Declared repeated sets keep one size, one inner layout and their arrangement.
|
|
829
|
+
return [...lineShapeLines({ fileName, spec, src: capturedSources.get(fileName) ?? "", render }), ...repeatLines({ fileName, spec, render }), ...listLines({ fileName, spec, src: capturedSources.get(fileName) ?? "" })];
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// A slide that references an asset file that is not on disk renders a
|
|
833
|
+
// broken image here and a 422 at build-pptx (x28: one icon the fetcher never
|
|
834
|
+
// delivered failed the whole deck AFTER review). Deterministic, so it is a
|
|
835
|
+
// blocking defect the review gate holds on.
|
|
836
|
+
let bundleAssetDir;
|
|
837
|
+
const bundleHasAsset = (file) => {
|
|
838
|
+
if (bundleAssetDir === undefined) {
|
|
839
|
+
try {
|
|
840
|
+
bundleAssetDir = path.join(resolveBundleDirForDeck(deckDir), "assets");
|
|
841
|
+
} catch {
|
|
842
|
+
bundleAssetDir = null;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return bundleAssetDir ? fs.existsSync(path.join(bundleAssetDir, file)) : false;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const missingAssetDefects = (fileName) => {
|
|
849
|
+
let src = "";
|
|
850
|
+
try {
|
|
851
|
+
src = fs.readFileSync(path.join(deckDir, "slides", fileName), "utf8");
|
|
852
|
+
} catch {
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
return missingAssetLines({ fileName, src, hasAsset: (file) => fs.existsSync(path.join(deckDir, "assets", file)), hasBundleAsset: bundleHasAsset });
|
|
856
|
+
};
|
|
558
857
|
|
|
559
858
|
const written = [];
|
|
560
859
|
const record = [];
|
|
@@ -565,59 +864,94 @@ export async function cmdViewDraft(rest) {
|
|
|
565
864
|
const stem = slide.fileName.replace(/\.[^.]+$/, "");
|
|
566
865
|
const file = path.join(previewDir, `${stem}.png`);
|
|
567
866
|
fs.writeFileSync(file, png);
|
|
867
|
+
if (!outDir) {
|
|
868
|
+
const first = path.join(statePath(deckDir, "first-draft"), stem);
|
|
869
|
+
if (!fs.existsSync(path.join(first, "preview.png"))) {
|
|
870
|
+
fs.mkdirSync(first, { recursive: true });
|
|
871
|
+
fs.writeFileSync(path.join(first, "slide.jsx"), capturedSources.get(slide.fileName) ?? "");
|
|
872
|
+
fs.writeFileSync(path.join(first, "preview.png"), png);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
568
875
|
written.push(file);
|
|
569
876
|
const flag = slide.status === "degraded" ? " (DEGRADED)" : "";
|
|
570
877
|
out(` ${slide.fileName}${flag} → ${file}\n`);
|
|
571
878
|
if (slide.note) process.stderr.write(` note: ${slide.note}\n`);
|
|
572
879
|
const concept = conceptFor(slide.fileName);
|
|
573
|
-
|
|
880
|
+
// A Set: the chrome rule runs locally AND in the export gate over the
|
|
881
|
+
// same capture, so its verdict arrives twice — it is one defect.
|
|
882
|
+
const allLines =
|
|
574
883
|
slide.status === "ok"
|
|
575
|
-
? [
|
|
576
|
-
...measuredLines(stem, slide.render),
|
|
577
|
-
...
|
|
884
|
+
? [...new Set([
|
|
885
|
+
...(slide.converted ? [] : measuredLines(stem, slide.render)),
|
|
886
|
+
...imageLineDefects(slide.fileName, slide.render),
|
|
887
|
+
...lineShapeDefects(slide.fileName, slide.render),
|
|
888
|
+
...missingAssetDefects(slide.fileName),
|
|
578
889
|
...(slide.translateErrors ?? []),
|
|
579
|
-
|
|
890
|
+
// Approved image obligations: a source that is gone or changed,
|
|
891
|
+
// and an original the render places nowhere (or twice). Blocking
|
|
892
|
+
// — `build-pptx` refuses the same deck, so the draft is where it
|
|
893
|
+
// gets repaired, not after review.
|
|
894
|
+
...(slide.imageIntentIssues ?? []),
|
|
895
|
+
// Preservation: named element obligations, reported here and
|
|
896
|
+
// enforced at delivery (build-pptx / push).
|
|
897
|
+
...(preserve.byFile[slide.fileName] ?? []),
|
|
898
|
+
])]
|
|
580
899
|
: [];
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
900
|
+
// Everything the shared table does not class as advisory is recorded and
|
|
901
|
+
// holds review and delivery: its defects, every gate refusal, and a
|
|
902
|
+
// refusal's plain-text continuation lines.
|
|
903
|
+
const defects = allLines.filter((l) => draftLineTier(l) !== "advisory");
|
|
904
|
+
const capturedSource = capturedSources.get(slide.fileName);
|
|
905
|
+
const capturedHash = crypto.createHash("sha256").update(capturedSource ?? "").digest("hex");
|
|
906
|
+
const currentSourceFile = path.join(deckDir, "slides", slide.fileName);
|
|
907
|
+
if (!fs.existsSync(currentSourceFile) || check.hashFile(currentSourceFile) !== capturedHash)
|
|
908
|
+
defects.push(`[draft:stale] ${slide.fileName} changed during capture. Render and inspect it again.`);
|
|
909
|
+
const notes = allLines.filter((l) => draftLineTier(l) === "advisory");
|
|
910
|
+
const flaws = concept ? conceptNotesFor(slide.fileName) : [];
|
|
911
|
+
if (concept) out(` concept: ${concept} — ${CONCEPT_GUIDANCE}\n`);
|
|
912
|
+
for (const line of flaws) out(` ${line}\n`);
|
|
586
913
|
for (const line of defects) out(` ${line}\n`);
|
|
914
|
+
for (const line of notes) out(` ${line}\n`);
|
|
915
|
+
// Advisory lines only on the deck-wide pass: in a per-slide `--only`
|
|
916
|
+
// round they are noise next to the defects the agent is fixing (x23:
|
|
917
|
+
// sol spent rounds on 48 fill and 26 nested-paint lines that block
|
|
918
|
+
// nothing).
|
|
919
|
+
if (slide.status === "ok" && !slide.converted && !only) {
|
|
920
|
+
for (const line of advisoryLines(stem, slide.render)) {
|
|
921
|
+
out(` ${line} (advisory)\n`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
587
924
|
measured += defects.length;
|
|
588
|
-
|
|
925
|
+
// `notes` carries the advisory lines — without it an agent on --json
|
|
926
|
+
// never sees them at all.
|
|
927
|
+
slidesOut.push({ file: slide.fileName, status: slide.status, png: file, concept, flaws, defects, notes, note: slide.note ?? null });
|
|
589
928
|
const jsxFile = path.join(deckDir, "slides", slide.fileName);
|
|
590
929
|
if (slide.status === "ok" && fs.existsSync(jsxFile)) {
|
|
591
|
-
record.push({ fileName: slide.fileName, hash:
|
|
930
|
+
record.push({ fileName: slide.fileName, hash: capturedHash, defects });
|
|
592
931
|
}
|
|
593
932
|
} else {
|
|
594
933
|
out(` ${slide.fileName} — ${slide.note ?? slide.status}\n`);
|
|
934
|
+
// A purpose-built layout has no freeform region: checked, nothing to
|
|
935
|
+
// measure. Delivery asks every shipped slide for a current check.
|
|
936
|
+
const jsxFile = path.join(deckDir, "slides", slide.fileName);
|
|
937
|
+
if (slide.status === "no-preview" && fs.existsSync(jsxFile)) {
|
|
938
|
+
const capturedHash = crypto.createHash("sha256").update(capturedSources.get(slide.fileName) ?? "").digest("hex");
|
|
939
|
+
if (check.hashFile(jsxFile) === capturedHash) record.push({ fileName: slide.fileName, hash: capturedHash, defects: [] });
|
|
940
|
+
}
|
|
595
941
|
slidesOut.push({ file: slide.fileName, status: slide.status, png: null, concept: null, defects: [], note: slide.note ?? null });
|
|
596
942
|
}
|
|
597
943
|
}
|
|
598
|
-
for (const line of deckLevel) out(` ${line}\n`);
|
|
599
|
-
measured += deckLevel.length;
|
|
600
944
|
const translateRan = translate && result.translateRan !== false;
|
|
601
945
|
if (translateRan) {
|
|
602
946
|
const failed = (result.slides ?? []).filter((s) => s.translateErrors?.length).length;
|
|
603
947
|
out(
|
|
604
948
|
failed
|
|
605
949
|
? ` TRANSLATE GATE: ${failed} slide${failed === 1 ? "" : "s"} refused by the export gate (lines above) — these are the errors build-pptx would stop on.\n`
|
|
606
|
-
: " TRANSLATE GATE: every rendered slide passed the export gate (slot-fit, diagram geometry,
|
|
607
|
-
);
|
|
608
|
-
for (const w of result.translateWarnings ?? []) process.stderr.write(` note: ${w}\n`);
|
|
609
|
-
}
|
|
610
|
-
const verdict = check.verdictBlocker(deckDir);
|
|
611
|
-
if (verdict) {
|
|
612
|
-
out(
|
|
613
|
-
` self-check: ${verdict.replace(/\n\s+/g, "; ")}\n` +
|
|
614
|
-
" (under each slide in asset-plan.md: `draft-check:` then `- [x] <organism>` per organism found in the render, `- [ ] <organism> — <what differs>` otherwise; `review` refuses until every organism is ticked [x])\n",
|
|
950
|
+
: " TRANSLATE GATE: every rendered slide passed the export gate (slot-fit, diagram geometry, text escape, chrome).\n",
|
|
615
951
|
);
|
|
952
|
+
if (!only) for (const w of result.translateWarnings ?? []) process.stderr.write(` note: ${w}\n`);
|
|
616
953
|
}
|
|
617
954
|
if (record.length > 0 && !outDir) {
|
|
618
|
-
// Deck-level asset lines are recorded on the first slide so they stand
|
|
619
|
-
// until that slide (or the asset plan) changes.
|
|
620
|
-
if (deckLevel.length > 0) record[0].defects.push(...deckLevel);
|
|
621
955
|
// A partial run (--only) updates its slides and keeps the others' record.
|
|
622
956
|
const prior = only ? check.loadDraftCheck(deckDir)?.slides ?? {} : {};
|
|
623
957
|
const rendered = new Set(record.map((r) => r.fileName));
|
|
@@ -633,7 +967,7 @@ export async function cmdViewDraft(rest) {
|
|
|
633
967
|
} else if (measured > 0) {
|
|
634
968
|
out(
|
|
635
969
|
`DRAFT CHECK: ${measured} measured defect${measured === 1 ? "" : "s"} (above) — fix them in the JSX and re-run ` +
|
|
636
|
-
"`slidemind view-draft`; `
|
|
970
|
+
"`slidemind view-draft`; `review` and delivery refuse while they stand. " +
|
|
637
971
|
`Previews in ${previewDir} — NOT pushed.\n`,
|
|
638
972
|
);
|
|
639
973
|
} else {
|
|
@@ -649,13 +983,15 @@ export async function cmdViewDraft(rest) {
|
|
|
649
983
|
process.stdout.write(
|
|
650
984
|
`${JSON.stringify(
|
|
651
985
|
{
|
|
652
|
-
deck:
|
|
986
|
+
deck: deckSlug(deckDir),
|
|
653
987
|
previewDir,
|
|
988
|
+
// The pair rule the text output prints per slide: an agent on
|
|
989
|
+
// --json must read it too.
|
|
990
|
+
conceptGuidance: `Open each slide's concept and png together, in one look. The concept is ${CONCEPT_GUIDANCE}`,
|
|
654
991
|
slides: slidesOut,
|
|
655
992
|
measuredDefects: measured,
|
|
656
993
|
translateGate: translateRan ? (slidesOut.some((s) => s.defects.some((d) => /:error\]/.test(d))) ? "failed" : "passed") : "skipped",
|
|
657
|
-
|
|
658
|
-
reviewCanOpen: measured === 0 && !verdict,
|
|
994
|
+
reviewCanOpen: measured === 0,
|
|
659
995
|
warnings,
|
|
660
996
|
},
|
|
661
997
|
null,
|