slidemind 0.3.20 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- 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
|
@@ -4,285 +4,52 @@
|
|
|
4
4
|
// live in the add-in and reach the CLI as the capture engine's `draftChecks`
|
|
5
5
|
// export (`capture.mjs` → `loadDraftChecks`), so in-app see_draft and
|
|
6
6
|
// view-draft run one rule set with one wording. What stays CLI-side is what
|
|
7
|
-
// only exists in the CLI's world: the
|
|
8
|
-
//
|
|
7
|
+
// only exists in the CLI's world: the .draft-preview record and the gates it
|
|
8
|
+
// arms, and the 422 translate parser.
|
|
9
9
|
// Images are never overlaid: a magenta proportion grid on the concept and the
|
|
10
10
|
// render dominated both and hid whether the slide was built well.
|
|
11
11
|
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { deckPptxPath, deckSlug, statePath } from "../lib/deck-paths.mjs";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const SECTION_RE = /^(regions|organisms|connectors|icons):/;
|
|
26
|
-
const FETCHED_ASSET_RE = /^(engine|gen)-/;
|
|
27
|
-
// "none", "(none)", "n/a", "no connectors …", "none authorable …" — an
|
|
28
|
-
// explicit empty row, never a listed organism/connector.
|
|
29
|
-
const NONE_RE = /^\(?\s*(none|n\/a|no\b)/i;
|
|
30
|
-
|
|
31
|
-
const pad2 = (n) => String(n).padStart(2, "0");
|
|
32
|
-
|
|
33
|
-
// Route kind per connector row / directive — part of the mockup contract: a
|
|
34
|
-
// loop the concept draws as curves that lands as elbows is a different figure,
|
|
35
|
-
// and the count alone (5 = 5) never fires. Port of inventory.ts (gotcha 84).
|
|
36
|
-
const ROUTE_KINDS = ["straight", "elbow", "curved"];
|
|
37
|
-
const emptyRoutes = () => ({ straight: 0, elbow: 0, curved: 0 });
|
|
38
|
-
function routeKindOf(text) {
|
|
39
|
-
for (const token of text.split(",")) {
|
|
40
|
-
const t = token.trim().toLowerCase();
|
|
41
|
-
if (ROUTE_KINDS.includes(t)) return t;
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const describeRoutes = (r) => ROUTE_KINDS.map((k) => `${r[k]} ${k}`).join(", ");
|
|
46
|
-
|
|
47
|
-
/** Seed the inventory form once (never overwrites a filled one). `images`: [{slideNumber,label}]. */
|
|
48
|
-
export function seedAssetPlan(deckDir, images) {
|
|
49
|
-
const file = path.join(deckDir, ASSET_PLAN_FILE);
|
|
50
|
-
if (!images?.length || fs.existsSync(file)) return false;
|
|
51
|
-
const slides = [...images]
|
|
52
|
-
.sort((a, b) => a.slideNumber - b.slideNumber)
|
|
53
|
-
.map((img) =>
|
|
54
|
-
img.frame
|
|
55
|
-
? [
|
|
56
|
-
`## Slide ${pad2(img.slideNumber)} — ${img.label ?? `Slide ${img.slideNumber}`}`,
|
|
57
|
-
`jsx: slides/${pad2(img.slideNumber)}-<name>.jsx`,
|
|
58
|
-
`(frame slide — ${img.frame}: chrome slots only, authored from the plan as the layout's props; no inventory, no mockup, no verdict)`,
|
|
59
|
-
"",
|
|
60
|
-
].join("\n")
|
|
61
|
-
: [
|
|
62
|
-
`## Slide ${pad2(img.slideNumber)} — ${img.label ?? `Slide ${img.slideNumber}`}`,
|
|
63
|
-
`jsx: slides/${pad2(img.slideNumber)}-<name>.jsx`,
|
|
64
|
-
"regions:",
|
|
65
|
-
"- ",
|
|
66
|
-
"organisms:",
|
|
67
|
-
"- ",
|
|
68
|
-
"connectors:",
|
|
69
|
-
"- ",
|
|
70
|
-
"icons:",
|
|
71
|
-
"- ",
|
|
72
|
-
"",
|
|
73
|
-
].join("\n"),
|
|
74
|
-
);
|
|
75
|
-
fs.writeFileSync(
|
|
76
|
-
file,
|
|
77
|
-
[
|
|
78
|
-
"# Asset plan — concept inventory",
|
|
79
|
-
"",
|
|
80
|
-
"Fill this BEFORE writing any slide, from `concepts/concept-slide-NN.png` (the BODY region only —",
|
|
81
|
-
"chrome goes through layout props). One bullet per line; the",
|
|
82
|
-
"draft check counts `connectors:` against the JSX and every fetched asset against the slides.",
|
|
83
|
-
"",
|
|
84
|
-
"- regions: split of the content slot in percentages of its width/height, e.g. `hero left x0–60 y0–100`, `rail right x65–100`, `callout bottom y85–100`.",
|
|
85
|
-
"- organisms: every card, figure, badge, chip row, callout, loop, satellite — region, share, alignment.",
|
|
86
|
-
"- connectors: `from → to, straight|elbow|curved, arrow end|both|none, solid|dashed` — one per arrow/line in the mockup.",
|
|
87
|
-
"- icons: subject, prominence, engine query; append the fetched `assets/<file>` when sourced, or `(no result)`.",
|
|
88
|
-
"",
|
|
89
|
-
"After `slidemind view-draft`, under each slide add `draft-check:` followed by ONE tick line per",
|
|
90
|
-
"organism row: `- [x] <organism>` when it is in the render at the concept's position and share,",
|
|
91
|
-
"`- [ ] <organism> — <what differs>` when it is not. `review` refuses to open while a slide has",
|
|
92
|
-
"fewer ticks than organisms or any `[ ]` left — fix the miss in the JSX, re-render, re-tick.",
|
|
93
|
-
"",
|
|
94
|
-
...slides,
|
|
95
|
-
].join("\n"),
|
|
96
|
-
);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function parseAssetPlan(text) {
|
|
101
|
-
const out = new Map();
|
|
102
|
-
let current = null;
|
|
103
|
-
let section = "";
|
|
104
|
-
for (const raw of text.split("\n")) {
|
|
105
|
-
const line = raw.trim();
|
|
106
|
-
const heading = SLIDE_HEADING_RE.exec(line);
|
|
107
|
-
if (heading) {
|
|
108
|
-
current = { filled: false, connectors: 0, routes: emptyRoutes(), organisms: 0, icons: 0, iconsResolved: 0, verdict: null, ticked: 0, unticked: 0 };
|
|
109
|
-
out.set(Number(heading[1]), current);
|
|
110
|
-
section = "";
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (!current) continue;
|
|
114
|
-
if (/^draft-check:/i.test(line)) {
|
|
115
|
-
current.verdict = line.slice("draft-check:".length).trim();
|
|
116
|
-
section = "draft-check";
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (section === "draft-check") {
|
|
120
|
-
const tick = /^-\s*\[( |x|X)\]/.exec(line);
|
|
121
|
-
if (tick) {
|
|
122
|
-
if (tick[1] === " ") current.unticked += 1;
|
|
123
|
-
else current.ticked += 1;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const sec = SECTION_RE.exec(line);
|
|
128
|
-
if (sec) {
|
|
129
|
-
section = sec[1];
|
|
130
|
-
// Inline form (`connectors: a -> b, …; c -> d, …`) counts like bullets —
|
|
131
|
-
// agents write both shapes; only the seeded form uses bullets.
|
|
132
|
-
const inline = line.slice(sec[0].length).trim();
|
|
133
|
-
if (inline && !NONE_RE.test(inline)) {
|
|
134
|
-
if (section === "organisms" || section === "regions") current.filled = true;
|
|
135
|
-
if (section === "connectors") {
|
|
136
|
-
for (const row of inline.split(";")) {
|
|
137
|
-
if (!row.trim()) continue;
|
|
138
|
-
current.connectors += 1;
|
|
139
|
-
const kind = routeKindOf(row);
|
|
140
|
-
if (kind) current.routes[kind] += 1;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (!line.startsWith("-")) continue;
|
|
147
|
-
const body = line.slice(1).trim();
|
|
148
|
-
if (!body || NONE_RE.test(body)) continue;
|
|
149
|
-
if (section === "organisms" || section === "regions") current.filled = true;
|
|
150
|
-
if (section === "organisms") current.organisms += 1;
|
|
151
|
-
if (section === "connectors") {
|
|
152
|
-
current.connectors += 1;
|
|
153
|
-
const kind = routeKindOf(body);
|
|
154
|
-
if (kind) current.routes[kind] += 1;
|
|
155
|
-
}
|
|
156
|
-
if (section === "icons") {
|
|
157
|
-
current.icons += 1;
|
|
158
|
-
// Resolved = a fetched file appended, or an explicit "(no result)".
|
|
159
|
-
if (/assets\/|\(no result\)/i.test(body)) current.iconsResolved += 1;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return out;
|
|
163
|
-
}
|
|
17
|
+
// Which draft lines hold the gate is the shared table's (draft-lines.mjs,
|
|
18
|
+
// mirrored into the add-in): `view-draft` records every line it does not
|
|
19
|
+
// class as advisory, and that record is what `review` and delivery refuse on.
|
|
20
|
+
import {
|
|
21
|
+
draftLineTier,
|
|
22
|
+
preservationFailureLines,
|
|
23
|
+
preservationUnreadableLine,
|
|
24
|
+
} from "./draft-lines.mjs";
|
|
25
|
+
export { draftLineTier };
|
|
164
26
|
|
|
165
27
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
28
|
+
* A build's package lines (build-pptx, push), split by the same table: a
|
|
29
|
+
* defect means the file is not ready for the user; a line outside the table is
|
|
30
|
+
* a note; the advisories the draft check already printed are not repeated.
|
|
168
31
|
*/
|
|
169
|
-
export function
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const out = { slides: plan.size, unfilled: 0, icons: 0, iconsResolved: 0 };
|
|
179
|
-
for (const inv of plan.values()) {
|
|
180
|
-
if (!inv.filled) out.unfilled += 1;
|
|
181
|
-
out.icons += inv.icons;
|
|
182
|
-
out.iconsResolved += inv.iconsResolved;
|
|
183
|
-
}
|
|
184
|
-
return out;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function countConnectors(jsx) {
|
|
188
|
-
const attached = (jsx.match(/data-connector\b/g) ?? []).length;
|
|
189
|
-
const markers = (jsx.match(/marker-(?:end|start)\s*=/g) ?? []).length;
|
|
190
|
-
return attached + markers;
|
|
32
|
+
export function splitPackageLines(warnings) {
|
|
33
|
+
const lines = (warnings ?? []).map((w) => String(w).trim());
|
|
34
|
+
return {
|
|
35
|
+
defects: lines.filter((l) => draftLineTier(l) === "defect"),
|
|
36
|
+
notes: lines.filter((l) => draftLineTier(l) === null),
|
|
37
|
+
};
|
|
191
38
|
}
|
|
192
39
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const kind = /\bdata-route\s*=\s*["']([a-z]+)["']/.exec(tag)?.[1] ?? "straight";
|
|
201
|
-
if (ROUTE_KINDS.includes(kind)) routes[kind] += 1;
|
|
202
|
-
}
|
|
203
|
-
return routes;
|
|
40
|
+
/** The `PACKAGE DEFECTS:` block both delivering commands print. */
|
|
41
|
+
export function packageDefectsText(defects, deckName) {
|
|
42
|
+
return (
|
|
43
|
+
`PACKAGE DEFECTS: ${defects.length} measured defect${defects.length === 1 ? "" : "s"} in the built file — not ready for the user. ` +
|
|
44
|
+
`Fix each in the JSX, run \`slidemind view-draft ${deckName} --only <slide>\`, then deliver again:\n` +
|
|
45
|
+
defects.map((l) => ` ${l}\n`).join("")
|
|
46
|
+
);
|
|
204
47
|
}
|
|
205
48
|
|
|
206
|
-
|
|
207
|
-
* `[inventory:warn]` lines for the deck: per authored slide (inventory empty,
|
|
208
|
-
* connectors listed > drawn) plus fetched assets no slide references. Empty
|
|
209
|
-
* when the deck has no asset-plan.md.
|
|
210
|
-
*/
|
|
211
|
-
export function inventoryWarnings(deckDir, jsxFiles = null) {
|
|
212
|
-
const planFile = path.join(deckDir, ASSET_PLAN_FILE);
|
|
213
|
-
if (!fs.existsSync(planFile)) return [];
|
|
214
|
-
let plan;
|
|
215
|
-
try {
|
|
216
|
-
plan = parseAssetPlan(fs.readFileSync(planFile, "utf8"));
|
|
217
|
-
} catch {
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
if (plan.size === 0) return [];
|
|
221
|
-
const slidesDir = path.join(deckDir, "slides");
|
|
222
|
-
const allJsx = fs.existsSync(slidesDir)
|
|
223
|
-
? fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx")).sort()
|
|
224
|
-
: [];
|
|
225
|
-
const files = jsxFiles ?? allJsx;
|
|
226
|
-
const lines = [];
|
|
227
|
-
for (const file of files) {
|
|
228
|
-
const num = SLIDE_FILE_NUMBER_RE.exec(file);
|
|
229
|
-
if (!num) continue;
|
|
230
|
-
const inv = plan.get(Number(num[1]));
|
|
231
|
-
if (!inv) continue;
|
|
232
|
-
const jsxPath = `slides/${file}`;
|
|
233
|
-
if (!inv.filled) {
|
|
234
|
-
lines.push(
|
|
235
|
-
`[inventory:warn] ${jsxPath} — asset-plan.md has no regions/organisms rows for this slide. Inventory its concept mockup (every organism, its region and share) and rebuild from that list, not from memory.`,
|
|
236
|
-
);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
let jsx = "";
|
|
240
|
-
try {
|
|
241
|
-
jsx = fs.readFileSync(path.join(slidesDir, file), "utf8");
|
|
242
|
-
} catch {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const drawn = countConnectors(jsx);
|
|
246
|
-
if (inv.connectors > drawn) {
|
|
247
|
-
lines.push(
|
|
248
|
-
`[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.connectors} connector${inv.connectors === 1 ? "" : "s"} for this slide, the JSX draws ${drawn} (data-connector / marker-end). Draw the missing ones per workflow/authoring.md → Arrow contract; a mockup connector is never dropped.`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
const drawnRoutes = countRoutes(jsx);
|
|
252
|
-
for (const kind of ROUTE_KINDS) {
|
|
253
|
-
if (inv.routes[kind] > drawnRoutes[kind]) {
|
|
254
|
-
lines.push(
|
|
255
|
-
`[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.routes[kind]} ${kind} connector${inv.routes[kind] === 1 ? "" : "s"} for this slide, the JSX draws ${drawnRoutes[kind]} (${describeRoutes(drawnRoutes)}). The mockup's route shape is the contract: keep data-route="${kind}" and fix the endpoint sides or node positions instead of changing the route kind.`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Fetched assets nobody references: the mockup staged them, the deck lost them.
|
|
261
|
-
const referenced = allJsx
|
|
262
|
-
.map((f) => {
|
|
263
|
-
try {
|
|
264
|
-
return fs.readFileSync(path.join(slidesDir, f), "utf8");
|
|
265
|
-
} catch {
|
|
266
|
-
return "";
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
.join("\n");
|
|
270
|
-
const assetsDir = path.join(deckDir, "assets");
|
|
271
|
-
const assets = fs.existsSync(assetsDir) ? fs.readdirSync(assetsDir).sort() : [];
|
|
272
|
-
const planText = fs.readFileSync(planFile, "utf8");
|
|
273
|
-
for (const file of assets) {
|
|
274
|
-
if (!FETCHED_ASSET_RE.test(file)) continue;
|
|
275
|
-
if (referenced.includes(file)) continue;
|
|
276
|
-
if (new RegExp(`${file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]*\\(unused\\)`).test(planText)) continue;
|
|
277
|
-
lines.push(
|
|
278
|
-
`[inventory:warn] assets/${file} was fetched for this deck but no slide references it — place it where the mockup stages that icon (\`<img src="./assets/${file}">\`), or mark it \`(unused)\` in asset-plan.md.`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
return lines;
|
|
282
|
-
}
|
|
49
|
+
export const DRAFT_CHECK_FILE = "draft-check.json";
|
|
283
50
|
|
|
284
51
|
// ---------------------------------------------------------------------------
|
|
285
|
-
// The draft-check record — what `review`
|
|
52
|
+
// The draft-check record — what `review`, `build-pptx` and `push` refuse on.
|
|
286
53
|
// ---------------------------------------------------------------------------
|
|
287
54
|
|
|
288
55
|
export function hashFile(file) {
|
|
@@ -291,7 +58,7 @@ export function hashFile(file) {
|
|
|
291
58
|
|
|
292
59
|
/** Persist the measured defects per slide, keyed by the jsx content hash. */
|
|
293
60
|
export function saveDraftCheck(deckDir, slides) {
|
|
294
|
-
const file =
|
|
61
|
+
const file = statePath(deckDir, DRAFT_CHECK_FILE);
|
|
295
62
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
296
63
|
const record = { version: 1, at: new Date().toISOString(), slides: {} };
|
|
297
64
|
for (const s of slides) record.slides[s.fileName] = { hash: s.hash, defects: s.defects };
|
|
@@ -301,117 +68,97 @@ export function saveDraftCheck(deckDir, slides) {
|
|
|
301
68
|
|
|
302
69
|
export function loadDraftCheck(deckDir) {
|
|
303
70
|
try {
|
|
304
|
-
return JSON.parse(fs.readFileSync(
|
|
71
|
+
return JSON.parse(fs.readFileSync(statePath(deckDir, DRAFT_CHECK_FILE), "utf8"));
|
|
305
72
|
} catch {
|
|
306
73
|
return null;
|
|
307
74
|
}
|
|
308
75
|
}
|
|
309
76
|
|
|
310
77
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
78
|
+
* Why these slides may not reach the user yet, or null. Every slide file
|
|
79
|
+
* delivered (default: every authored `.jsx` under slides/) needs a draft check
|
|
80
|
+
* of its current bytes that left no defect standing. A slide never checked, or
|
|
81
|
+
* edited after its check, holds like a defect: only a new view-draft clears a
|
|
82
|
+
* check, and editing is not evidence that a defect was fixed.
|
|
314
83
|
*/
|
|
315
|
-
export function draftCheckBlocker(deckDir) {
|
|
316
|
-
const
|
|
317
|
-
|
|
84
|
+
export function draftCheckBlocker(deckDir, { files = null } = {}) {
|
|
85
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
86
|
+
let shipped = files;
|
|
87
|
+
if (!shipped) {
|
|
88
|
+
try {
|
|
89
|
+
shipped = fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx"));
|
|
90
|
+
} catch {
|
|
91
|
+
shipped = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const slides = loadDraftCheck(deckDir)?.slides ?? {};
|
|
318
95
|
const standing = [];
|
|
319
|
-
for (const
|
|
320
|
-
|
|
321
|
-
const file = path.join(deckDir, "slides", fileName);
|
|
96
|
+
for (const fileName of [...new Set(shipped)].filter((f) => f.endsWith(".jsx")).sort((a, b) => a.localeCompare(b, "en", { numeric: true }))) {
|
|
97
|
+
const file = path.join(slidesDir, fileName);
|
|
322
98
|
if (!fs.existsSync(file)) continue;
|
|
323
|
-
|
|
324
|
-
|
|
99
|
+
const entry = slides[fileName];
|
|
100
|
+
if (!entry) {
|
|
101
|
+
standing.push(`[draft:unchecked] ${fileName} has no draft check. Run view-draft --only ${fileName} and inspect the new image.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (hashFile(file) !== entry.hash) {
|
|
105
|
+
standing.push(`[draft:stale] ${fileName} changed after its preview. Re-run view-draft --only ${fileName} and inspect the new image.`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
standing.push(...(entry.defects ?? []));
|
|
325
109
|
}
|
|
326
110
|
if (standing.length === 0) return null;
|
|
327
111
|
return (
|
|
328
|
-
`the
|
|
329
|
-
`
|
|
112
|
+
`the draft check has ${standing.length} unresolved defect${standing.length === 1 ? "" : "s"} or unchecked slide${standing.length === 1 ? "" : "s"}. ` +
|
|
113
|
+
`Fix the defects, re-run \`slidemind view-draft\`, then retry:\n` +
|
|
330
114
|
standing.map((l) => ` ${l}`).join("\n")
|
|
331
115
|
);
|
|
332
116
|
}
|
|
333
117
|
|
|
334
|
-
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
// The self-check verdict — an inventory, not an opinion.
|
|
337
|
-
//
|
|
338
|
-
// Every `draft-check:` verdict on disk across 181 judged sessions reads
|
|
339
|
-
// "matches" (15 of 15), including slides the judge scored 0.71 with eight
|
|
340
|
-
// organisms missing. A verdict is free; a tick per organism is not — the
|
|
341
|
-
// agent has to find each one in the render or say what differs.
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Concept-seeded slides whose asset-plan.md section lists organisms but whose
|
|
346
|
-
* `draft-check:` is missing, ticks fewer organisms than listed, or leaves a
|
|
347
|
-
* `[ ]` standing. null when every such slide is fully ticked.
|
|
348
|
-
*/
|
|
349
|
-
export function verdictBlocker(deckDir) {
|
|
350
|
-
const planFile = path.join(deckDir, ASSET_PLAN_FILE);
|
|
351
|
-
if (!fs.existsSync(planFile)) return null;
|
|
352
|
-
let plan;
|
|
353
|
-
try {
|
|
354
|
-
plan = parseAssetPlan(fs.readFileSync(planFile, "utf8"));
|
|
355
|
-
} catch {
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
const slidesDir = path.join(deckDir, "slides");
|
|
359
|
-
const conceptsDir = path.join(deckDir, "concepts");
|
|
360
|
-
const files = fs.existsSync(slidesDir)
|
|
361
|
-
? fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx")).sort()
|
|
362
|
-
: [];
|
|
363
|
-
const problems = [];
|
|
364
|
-
for (const file of files) {
|
|
365
|
-
const num = SLIDE_FILE_NUMBER_RE.exec(file);
|
|
366
|
-
if (!num) continue;
|
|
367
|
-
const n = Number(num[1]);
|
|
368
|
-
const inv = plan.get(n);
|
|
369
|
-
if (!inv || inv.organisms === 0) continue;
|
|
370
|
-
if (!fs.existsSync(path.join(conceptsDir, `concept-slide-${pad2(n)}.png`))) continue;
|
|
371
|
-
const ticks = inv.ticked + inv.unticked;
|
|
372
|
-
if (inv.verdict === null) {
|
|
373
|
-
problems.push(`slides/${file}: no draft-check verdict — asset-plan.md lists ${inv.organisms} organisms; open the concept and the render together and tick each one`);
|
|
374
|
-
} else if (inv.unticked > 0) {
|
|
375
|
-
problems.push(`slides/${file}: ${inv.unticked} organism${inv.unticked === 1 ? "" : "s"} ticked [ ] (differs) — fix in the JSX, re-run view-draft, re-tick`);
|
|
376
|
-
} else if (ticks < inv.organisms) {
|
|
377
|
-
problems.push(`slides/${file}: asset-plan.md lists ${inv.organisms} organisms, the verdict ticks ${ticks} — one \`- [x]\`/\`- [ ]\` line per organism`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (problems.length === 0) return null;
|
|
381
|
-
return (
|
|
382
|
-
"the self-check is incomplete — a verdict is one tick per organism, not one word:\n" +
|
|
383
|
-
problems.map((l) => ` ${l}`).join("\n")
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
118
|
// ---------------------------------------------------------------------------
|
|
388
|
-
// Translate-gate errors — the server's slot-fit / diagram-geometry /
|
|
389
|
-
// connector-endpoints
|
|
119
|
+
// Translate-gate errors — the server's slot-fit / diagram-geometry / escape
|
|
120
|
+
// verdicts (older add-ins also sent connector-endpoints), folded into the draft check so a builder
|
|
390
121
|
// learns them on ITS slide before the deck-wide export, not five build-pptx
|
|
391
122
|
// rounds later (x18: 17 of 39 minutes were that loop).
|
|
392
123
|
// ---------------------------------------------------------------------------
|
|
393
124
|
|
|
394
125
|
/**
|
|
395
|
-
* Split a translate 422 message into per-slide lines. The server
|
|
396
|
-
* `<file>.jsx:
|
|
397
|
-
*
|
|
398
|
-
*
|
|
126
|
+
* Split a translate 422 message into per-slide lines. The server prefixes each
|
|
127
|
+
* failing slide's error with `<file>.jsx: `, and that prefix makes it the
|
|
128
|
+
* current slide whatever follows: a `<rule>: <summary>` gate header (kept as
|
|
129
|
+
* `[<rule>:error] <file> — <summary>`), a tagged `[<tag>:error] <stem> — …`
|
|
130
|
+
* line (chrome), or plain text (a degraded render). Gate headers are followed
|
|
131
|
+
* by ` [<rule>:error] <stem> — …` lines; a slide that fails several measured
|
|
132
|
+
* gates reports each further gate as a bare `<rule>: <summary>` header under
|
|
133
|
+
* the same file. A chrome verdict quotes the slide's text, and a line break
|
|
134
|
+
* in that text splits it: the next unmatched line completes the chrome line
|
|
135
|
+
* just added, wherever it was filed. Unattributed lines land under "" so
|
|
136
|
+
* nothing is lost.
|
|
399
137
|
*/
|
|
400
138
|
export function parseTranslateErrors(message) {
|
|
401
139
|
const out = new Map();
|
|
140
|
+
let open = null; // the file whose last line is a chrome verdict cut inside its quote
|
|
402
141
|
const add = (file, line) => {
|
|
403
142
|
if (!out.has(file)) out.set(file, []);
|
|
404
143
|
out.get(file).push(line);
|
|
144
|
+
open = /^\[chrome:error\]/.test(line) && (line.match(/"/g) ?? []).length % 2 ? file : null;
|
|
405
145
|
};
|
|
406
146
|
const text = String(message ?? "").replace(/^[^\n]*\/translate:\s*/, "");
|
|
407
147
|
let current = "";
|
|
408
148
|
for (const raw of text.split("\n")) {
|
|
409
149
|
const line = raw.trimEnd();
|
|
410
150
|
if (!line.trim()) continue;
|
|
411
|
-
const head = /^\s*(\S+\.jsx):\s+(
|
|
151
|
+
const head = /^\s*(\S+\.jsx):\s+(.*)$/.exec(line);
|
|
412
152
|
if (head) {
|
|
413
153
|
current = head[1];
|
|
414
|
-
|
|
154
|
+
const rule = /^([a-z-]+):\s+(.*)$/.exec(head[2]);
|
|
155
|
+
if (rule) add(current, `[${rule[1]}:error] ${current} — ${rule[2]}`);
|
|
156
|
+
else add(current, head[2].startsWith("[") ? head[2] : line.trim());
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const bare = current ? /^(slot-fit|svg-overlay|connector-endpoints|diagram-geometry|escape):\s+(.*)$/.exec(line) : null;
|
|
160
|
+
if (bare) {
|
|
161
|
+
add(current, `[${bare[1]}:error] ${current} — ${bare[2]}`);
|
|
415
162
|
continue;
|
|
416
163
|
}
|
|
417
164
|
const tagged = /^\s*\[([a-z-]+):error\]\s+(\S+)\s+—/.exec(line);
|
|
@@ -421,7 +168,472 @@ export function parseTranslateErrors(message) {
|
|
|
421
168
|
add(file, line.trim());
|
|
422
169
|
continue;
|
|
423
170
|
}
|
|
424
|
-
|
|
171
|
+
if (open) {
|
|
172
|
+
const file = open;
|
|
173
|
+
add(file, `${out.get(file).pop()} ${line.trim()}`);
|
|
174
|
+
} else add(current, line.trim());
|
|
425
175
|
}
|
|
426
176
|
return out;
|
|
427
177
|
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Preservation — what a slide still OWES, checked by element identity.
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
//
|
|
183
|
+
// This replaces the old `ownership()` gate, which switched the concept-spec
|
|
184
|
+
// count check OFF the moment a slide's bytes stopped matching
|
|
185
|
+
// `state/transcribed.json`. That is exactly backwards: a hand edit is when a
|
|
186
|
+
// slide is most likely to lose something, and "the hash changed" was read as
|
|
187
|
+
// "nothing is owed any more" (E09 — a title edit that drops an arrow).
|
|
188
|
+
//
|
|
189
|
+
// What is owed now lives in `state/slide-preservation.json` (src/lib/edit-state.mjs),
|
|
190
|
+
// one requirement per element identity, and it survives any rewrite. Two
|
|
191
|
+
// evidence channels answer it, because neither can answer alone:
|
|
192
|
+
//
|
|
193
|
+
// source — the only place a connector can be checked. Its id is DERIVED from
|
|
194
|
+
// the pair it joins (`conn-a-b`) and never reaches the DOM, so the
|
|
195
|
+
// render literally cannot see one.
|
|
196
|
+
// render — the only place "present in the JSX but paints nothing" shows up.
|
|
197
|
+
//
|
|
198
|
+
// The modules are imported lazily: `edit-state` pulls in revision + bundle
|
|
199
|
+
// resolution, and `draft-check` is loaded eagerly by `status` and `concepts`.
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The two packages a slide's obligations can be proved against: the import it
|
|
203
|
+
* came FROM, and the deck as last DELIVERED. Both or nothing — comparing a
|
|
204
|
+
* package with itself proves only that it exists, which is the "manufacture a
|
|
205
|
+
* package to satisfy the gate" move. A slide with no import baseline (anything
|
|
206
|
+
* authored rather than converted) therefore has no package evidence and stays
|
|
207
|
+
* `unknown`; see `packageEvidenceReachable`.
|
|
208
|
+
*/
|
|
209
|
+
function packagePair(deckDir, fileName) {
|
|
210
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
211
|
+
const base = path.join(deckDir, "conversions", `${stem}.slide.pptx`);
|
|
212
|
+
const delivered = deckPptxPath(deckDir);
|
|
213
|
+
if (!fs.existsSync(base) || !fs.existsSync(delivered)) return null;
|
|
214
|
+
return { base, delivered };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Can this slide's obligations be proved from packages at all? */
|
|
218
|
+
export function packageEvidenceReachable(deckDir, fileName) {
|
|
219
|
+
return packagePair(deckDir, fileName) !== null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* One slide's package evidence: its import compared against the bytes the deck
|
|
224
|
+
* last delivered, keyed by the element identity both packages carry.
|
|
225
|
+
*
|
|
226
|
+
* The delivered slide is found by its GUID in the baked source tags, never by
|
|
227
|
+
* position — a native slide spliced in ahead of it would shift every index.
|
|
228
|
+
* Null whenever anything cannot be established; nothing here guesses.
|
|
229
|
+
*/
|
|
230
|
+
function workspaceResolvedTargets(deckDir, fileName, entry) {
|
|
231
|
+
// A requirement's relationship targets are minted in THREE namespaces and
|
|
232
|
+
// only one of them is an OOXML part path:
|
|
233
|
+
//
|
|
234
|
+
// start / end an element id on the same slide → the package resolves it
|
|
235
|
+
// package-part a WORKSPACE path → the filesystem does
|
|
236
|
+
// native-slot an index into the conversion → that record does
|
|
237
|
+
// record's `preserved[]`
|
|
238
|
+
//
|
|
239
|
+
// `composeSlideEvidence().resolvedTargets` only speaks the first. Passing it
|
|
240
|
+
// alone makes every native-object requirement fail `relationship_broken`
|
|
241
|
+
// against a file that is sitting right there — a FALSE DELETION, which is
|
|
242
|
+
// worse than the `unknown` it replaces. So each target is resolved in the
|
|
243
|
+
// namespace it was minted in, and anything that does not resolve stays
|
|
244
|
+
// unresolved and still fails.
|
|
245
|
+
const resolved = [];
|
|
246
|
+
let preserved = 0;
|
|
247
|
+
try {
|
|
248
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
249
|
+
const record = JSON.parse(fs.readFileSync(path.join(deckDir, "conversions", `${stem}.json`), "utf8"));
|
|
250
|
+
preserved = Array.isArray(record?.preserved) ? record.preserved.length : 0;
|
|
251
|
+
} catch {}
|
|
252
|
+
for (const req of entry?.active ?? []) {
|
|
253
|
+
for (const rel of req.relationships ?? []) {
|
|
254
|
+
if (rel.role === "package-part") {
|
|
255
|
+
if (fs.existsSync(path.join(deckDir, String(rel.target)))) resolved.push(rel.target);
|
|
256
|
+
} else if (rel.role === "native-slot") {
|
|
257
|
+
const slot = Number(rel.target);
|
|
258
|
+
if (Number.isInteger(slot) && slot >= 0 && slot < preserved) resolved.push(rel.target);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return resolved;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function packageEvidenceFor(deckDir, fileName, slideKey, entry) {
|
|
266
|
+
const pair = packagePair(deckDir, fileName);
|
|
267
|
+
if (!pair) return null;
|
|
268
|
+
try {
|
|
269
|
+
const [{ unzip }, { slideOrderPaths }, tags, { composeSlideEvidence }, verdictMod] = await Promise.all([
|
|
270
|
+
import("../pptx/zip.mjs"),
|
|
271
|
+
import("../pptx/ooxml.mjs"),
|
|
272
|
+
import("../pptx/tags.mjs"),
|
|
273
|
+
import("../pptx/package-evidence.mjs"),
|
|
274
|
+
import("../lib/preservation-verdict.mjs"),
|
|
275
|
+
]);
|
|
276
|
+
const beforeFiles = unzip(fs.readFileSync(pair.base));
|
|
277
|
+
const afterFiles = unzip(fs.readFileSync(pair.delivered));
|
|
278
|
+
const part = slideOrderPaths(afterFiles).find((candidate) => {
|
|
279
|
+
try {
|
|
280
|
+
return tags.decodeSourceTags(tags.readSlideTags(afterFiles, candidate))?.guid === slideKey;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
if (!part) return null; // the delivered deck does not carry this slide
|
|
286
|
+
const before = composeSlideEvidence(beforeFiles);
|
|
287
|
+
const after = composeSlideEvidence(afterFiles, { slidePart: part });
|
|
288
|
+
if (!before.slidePart || !after.slidePart) return null;
|
|
289
|
+
return verdictMod.comparePackageEvidence(before.shapes, after.shapes, {
|
|
290
|
+
resolvedTargets: [
|
|
291
|
+
...after.resolvedTargets,
|
|
292
|
+
...workspaceResolvedTargets(deckDir, fileName, entry),
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
// Evidence that cannot be composed is absent evidence, never a pass.
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function preservationModules() {
|
|
302
|
+
const [state, verdict, guids] = await Promise.all([
|
|
303
|
+
import("../lib/edit-state.mjs"),
|
|
304
|
+
import("../lib/preservation-verdict.mjs"),
|
|
305
|
+
import("../lib/slide-guids.mjs"),
|
|
306
|
+
]);
|
|
307
|
+
return { state, verdict, guids };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// failed > unsupported > unknown > passed. `unsupported` rolls up as `unknown`
|
|
311
|
+
// downstream — the verdict module already does that for its own status, and
|
|
312
|
+
// missing evidence is never a pass.
|
|
313
|
+
const EVIDENCE_ORDER = ["failed", "unsupported", "unknown", "passed"];
|
|
314
|
+
const worstOf = (statuses) => EVIDENCE_ORDER.find((s) => statuses.includes(s)) ?? "passed";
|
|
315
|
+
|
|
316
|
+
// The CLI's way to declare an intended removal, named in every [preserve:warn].
|
|
317
|
+
const supersedeHint = (deckDir) =>
|
|
318
|
+
`supersede the obligation explicitly (\`slidemind revision begin ${deckSlug(deckDir)} --operation targeted-edit\`)`;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The preservation diagnostics for one captured deck, per slide file.
|
|
322
|
+
*
|
|
323
|
+
* `slides` is `[{ fileName, render }]` straight off the capture. Returns
|
|
324
|
+
* `{ byFile, covered }` — `covered` names every slide that HAS a record,
|
|
325
|
+
* which is what stands the spec-count check down (each of its elements is
|
|
326
|
+
* checked by id instead, which is strictly stronger than counting them).
|
|
327
|
+
*
|
|
328
|
+
* Draft diagnostics never block: they name the element and the repair. What
|
|
329
|
+
* blocks is {@link preservationBlocker} at delivery.
|
|
330
|
+
*/
|
|
331
|
+
export async function preservationDiagnostics(deckDir, slides, { persist = true } = {}) {
|
|
332
|
+
const empty = { byFile: {}, covered: new Set() };
|
|
333
|
+
let mods;
|
|
334
|
+
try {
|
|
335
|
+
mods = await preservationModules();
|
|
336
|
+
} catch {
|
|
337
|
+
return empty;
|
|
338
|
+
}
|
|
339
|
+
const { readSlidePreservation, writeSlidePreservation, openOperations, sourceHash } = mods.state;
|
|
340
|
+
const { requirementEvidenceFromSource, checkRequirements, evidenceStatus } = mods.state;
|
|
341
|
+
const { compareRenderEvidence, preservationVerdict, readSlideElements } = mods.verdict;
|
|
342
|
+
|
|
343
|
+
let doc;
|
|
344
|
+
try {
|
|
345
|
+
doc = readSlidePreservation(deckDir);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
// Delivery refuses an unreadable record (preservationBlocker), so the draft
|
|
348
|
+
// names it as a defect, on the first slide, as the add-in's render does.
|
|
349
|
+
const first = (slides ?? []).find((s) => /\.jsx$/i.test(s.fileName ?? ""));
|
|
350
|
+
if (!first) return empty;
|
|
351
|
+
return {
|
|
352
|
+
...empty,
|
|
353
|
+
byFile: { [first.fileName]: [preservationUnreadableLine({ fileName: first.fileName, detail: err.message })] },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (Object.keys(doc.slides ?? {}).length === 0) return empty;
|
|
357
|
+
|
|
358
|
+
const guids = mods.guids.loadSlideGuids(deckDir);
|
|
359
|
+
let operations = [];
|
|
360
|
+
try {
|
|
361
|
+
operations = openOperations(deckDir);
|
|
362
|
+
} catch {}
|
|
363
|
+
|
|
364
|
+
const byFile = {};
|
|
365
|
+
const covered = new Set();
|
|
366
|
+
const next = { ...doc, slides: { ...doc.slides } };
|
|
367
|
+
let changed = false;
|
|
368
|
+
|
|
369
|
+
for (const { fileName, render } of slides ?? []) {
|
|
370
|
+
if (!/\.jsx$/i.test(fileName)) continue;
|
|
371
|
+
const jsxPath = `slides/${fileName}`;
|
|
372
|
+
const slideKey = guids[jsxPath];
|
|
373
|
+
const entry = slideKey ? doc.slides[slideKey] : null;
|
|
374
|
+
if (!entry) continue;
|
|
375
|
+
let jsx;
|
|
376
|
+
try {
|
|
377
|
+
jsx = fs.readFileSync(path.join(deckDir, jsxPath), "utf8");
|
|
378
|
+
} catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
covered.add(fileName);
|
|
382
|
+
const operation =
|
|
383
|
+
operations.find((o) => (o.targets ?? []).some((t) => t?.slideKey === slideKey)) ?? null;
|
|
384
|
+
|
|
385
|
+
// Ask the render only about ids the source actually declares as a DOM id.
|
|
386
|
+
// Everything else (connectors above all) is the source check's business —
|
|
387
|
+
// asking the render would read "absent" for every arrow ever drawn.
|
|
388
|
+
const declared = readSlideElements(jsx).byId;
|
|
389
|
+
const ids = (entry.active ?? []).map((r) => r.elementId).filter((id) => declared.has(id));
|
|
390
|
+
let renderEvidence = compareRenderEvidence(render ?? null, {
|
|
391
|
+
canvas: render?.canvas ?? null,
|
|
392
|
+
elementIds: ids,
|
|
393
|
+
});
|
|
394
|
+
if (ids.length > 0 && renderEvidence.indexed.length === 0) {
|
|
395
|
+
// Boxes, but not one carries an identity: this capture proved nothing
|
|
396
|
+
// about any element. Silence is `unknown`, never absence.
|
|
397
|
+
renderEvidence = {
|
|
398
|
+
...renderEvidence,
|
|
399
|
+
evidence: "absent",
|
|
400
|
+
byId: Object.fromEntries(
|
|
401
|
+
ids.map((id) => [id, { status: "unknown", reason: "no_identified_boxes" }]),
|
|
402
|
+
),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// The third channel. Present only for a slide whose import AND delivered
|
|
406
|
+
// bytes are both on disk; a draft has neither, and gets none.
|
|
407
|
+
const packageEvidence = await packageEvidenceFor(deckDir, fileName, slideKey, entry);
|
|
408
|
+
const verdict = preservationVerdict({
|
|
409
|
+
entry,
|
|
410
|
+
operation,
|
|
411
|
+
slideKey,
|
|
412
|
+
render: renderEvidence,
|
|
413
|
+
packageEvidence,
|
|
414
|
+
binding: { sourceHash: sourceHash(deckDir, jsxPath) },
|
|
415
|
+
});
|
|
416
|
+
const source = checkRequirements(
|
|
417
|
+
entry,
|
|
418
|
+
requirementEvidenceFromSource(jsx, { origin: entry.origin }),
|
|
419
|
+
{ operation },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// One verdict, both channels. The source violations become checks of the
|
|
423
|
+
// verdict's own shape so that everything downstream — the lines here, the
|
|
424
|
+
// refusal at delivery — reads ONE list and names the same elements.
|
|
425
|
+
const checks = [
|
|
426
|
+
...verdict.checks,
|
|
427
|
+
...source.violations.map((v) => ({
|
|
428
|
+
requirementId: v.requirementId ?? null,
|
|
429
|
+
elementId: v.elementId ?? null,
|
|
430
|
+
kind: null,
|
|
431
|
+
scope: null,
|
|
432
|
+
status: "failed",
|
|
433
|
+
reason: v.code,
|
|
434
|
+
detail: v.detail,
|
|
435
|
+
})),
|
|
436
|
+
];
|
|
437
|
+
// `checkRequirements` reports every non-`media` scope UNVERIFIED, because
|
|
438
|
+
// source bytes cannot prove a text, fill or geometry is unchanged. That is
|
|
439
|
+
// "source cannot say", not "nobody can" — so once the package HAS spoken,
|
|
440
|
+
// its silence must not veto the answer. Its VIOLATIONS always count: a
|
|
441
|
+
// missing element, a repointed connector or a swapped asset is a fact about
|
|
442
|
+
// the source, which no package evidence makes untrue.
|
|
443
|
+
const sourceStatus = packageEvidence
|
|
444
|
+
? source.violations.length > 0
|
|
445
|
+
? "failed"
|
|
446
|
+
: "passed"
|
|
447
|
+
: evidenceStatus(source);
|
|
448
|
+
const status = worstOf([verdict.status, sourceStatus]);
|
|
449
|
+
const counts = Object.fromEntries(EVIDENCE_ORDER.map((s) => [s, 0]));
|
|
450
|
+
for (const c of checks) counts[c.status] = (counts[c.status] ?? 0) + 1;
|
|
451
|
+
const merged = { ...verdict, status, checks, counts };
|
|
452
|
+
|
|
453
|
+
// One wording on both clients (draft-lines.mjs).
|
|
454
|
+
const lines = preservationFailureLines({
|
|
455
|
+
fileName,
|
|
456
|
+
checks,
|
|
457
|
+
active: entry.active,
|
|
458
|
+
supersede: supersedeHint(deckDir),
|
|
459
|
+
});
|
|
460
|
+
if (lines.length > 0) byFile[fileName] = lines;
|
|
461
|
+
|
|
462
|
+
if (entry.evidence !== status || entry.verdict?.binding?.sourceHash !== merged.binding.sourceHash) {
|
|
463
|
+
next.slides[slideKey] = { ...entry, evidence: status, verdict: merged };
|
|
464
|
+
changed = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (persist && changed) {
|
|
469
|
+
// Obligations are never dropped here — only the verdict and the evidence
|
|
470
|
+
// status move — so the transition rule has nothing to refuse. A refusal is
|
|
471
|
+
// still swallowed: a draft preview must not die over its own bookkeeping.
|
|
472
|
+
try {
|
|
473
|
+
writeSlidePreservation(deckDir, next);
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
return { byFile, covered };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Why a FINAL replacement cannot go ahead, or null.
|
|
481
|
+
*
|
|
482
|
+
* Scope is REPLACEMENT, and a preservation record is what marks it — not
|
|
483
|
+
* session bookkeeping. Three things seed one, and all three mean "this slide
|
|
484
|
+
* existed before this build": `revision begin` (migrated from current
|
|
485
|
+
* evidence), `adopt` (restored from the package's embedded tags,
|
|
486
|
+
* src/lib/preservation-tags.mjs), and `transcribe --repair`. A deck authored
|
|
487
|
+
* from scratch acquires none of them, so ordinary authoring is never gated —
|
|
488
|
+
* this change safeguards replacement, it does not add a precondition to
|
|
489
|
+
* creating slides.
|
|
490
|
+
*
|
|
491
|
+
* Deliberately NOT keyed on an open operation. An operation is how a user
|
|
492
|
+
* DECLARES an edit and what permits a removal (`permittedChanges`, honoured in
|
|
493
|
+
* the draft verdict); it is not how a replacement is detected. Keying on it
|
|
494
|
+
* would have left the commonest replacement there is — `build-pptx` on an
|
|
495
|
+
* adopted deck, whose records `adopt` restored — entirely ungated, so E09's
|
|
496
|
+
* protection would have existed only for users who happened to open a scope by
|
|
497
|
+
* hand. Build-time inference (`inferBuildOperations`) would answer the same
|
|
498
|
+
* question one indirection later and has no production caller today.
|
|
499
|
+
*
|
|
500
|
+
* ponytail: draft evidence alone rarely reaches `passed` (a connector's
|
|
501
|
+
* endpoints and a native's package closure are only resolvable against the
|
|
502
|
+
* final package, which is task 5.5/5.6's channel). Until that lands, the way
|
|
503
|
+
* past it is to fix what the draft names, not to remove the record.
|
|
504
|
+
*/
|
|
505
|
+
export async function preservationBlocker(deckDir) {
|
|
506
|
+
const clear = { blocker: null, note: null };
|
|
507
|
+
let mods;
|
|
508
|
+
try {
|
|
509
|
+
mods = await preservationModules();
|
|
510
|
+
} catch {
|
|
511
|
+
return clear;
|
|
512
|
+
}
|
|
513
|
+
const { readSlidePreservation, finalEvidenceBlockers, sourceHash } = mods.state;
|
|
514
|
+
const { verdictIsStale, requirementSetHash } = mods.verdict;
|
|
515
|
+
|
|
516
|
+
let doc;
|
|
517
|
+
try {
|
|
518
|
+
doc = readSlidePreservation(deckDir);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
return { blocker: err.message, note: null };
|
|
521
|
+
}
|
|
522
|
+
// The sidecar remembers a slide's identity after the file is gone; only what
|
|
523
|
+
// is still IN the deck can be replaced by this build, so only that is gated.
|
|
524
|
+
const fileOf = new Map(
|
|
525
|
+
Object.entries(mods.guids.loadSlideGuids(deckDir))
|
|
526
|
+
.filter(([jsxPath]) => fs.existsSync(path.join(deckDir, jsxPath)))
|
|
527
|
+
.map(([jsxPath, key]) => [key, jsxPath]),
|
|
528
|
+
);
|
|
529
|
+
const keys = Object.keys(doc.slides ?? {}).filter((slideKey) => fileOf.has(slideKey));
|
|
530
|
+
if (keys.length === 0) return { blocker: null, note: null };
|
|
531
|
+
|
|
532
|
+
// Is the recorded verdict about the bytes we are shipping?
|
|
533
|
+
const current = (slideKey) =>
|
|
534
|
+
!verdictIsStale(doc.slides[slideKey]?.verdict, {
|
|
535
|
+
sourceHash: sourceHash(deckDir, fileOf.get(slideKey)),
|
|
536
|
+
requirementSetHash: requirementSetHash(doc.slides[slideKey]),
|
|
537
|
+
});
|
|
538
|
+
const failedOf = (slideKey) =>
|
|
539
|
+
(doc.slides[slideKey]?.verdict?.checks ?? []).filter((c) => c.status === "failed" && c.elementId != null);
|
|
540
|
+
|
|
541
|
+
// `finalEvidenceBlockers` is the authority on EVIDENCE state, and it yields
|
|
542
|
+
// nothing for a `passed` entry. Freshness is a dimension it does not model,
|
|
543
|
+
// so every slide is walked — otherwise a verdict that passed once would keep
|
|
544
|
+
// certifying bytes it never saw.
|
|
545
|
+
const byKey = new Map(finalEvidenceBlockers(deckDir, keys).map((b) => [b.slideKey, b]));
|
|
546
|
+
const unproven = [];
|
|
547
|
+
const problems = [];
|
|
548
|
+
for (const slideKey of keys) {
|
|
549
|
+
const blocker = byKey.get(slideKey) ?? {
|
|
550
|
+
slideKey,
|
|
551
|
+
code: "evidence_passed",
|
|
552
|
+
reason: "preservation evidence is 'passed'",
|
|
553
|
+
};
|
|
554
|
+
// A record with a VIOLATED obligation refuses, always — that is E09.
|
|
555
|
+
if (blocker.code === "evidence_failed" || failedOf(blocker.slideKey).length > 0) {
|
|
556
|
+
problems.push(blocker);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
// Anything else is "not proven", and there are two very different reasons.
|
|
560
|
+
// Nobody has checked THESE bytes: one command fixes that, so refuse and
|
|
561
|
+
// name it. Checking is what catches an edit made after the last draft.
|
|
562
|
+
if (!current(slideKey)) {
|
|
563
|
+
problems.push({
|
|
564
|
+
...blocker,
|
|
565
|
+
code: "evidence_stale",
|
|
566
|
+
reason: "these exact bytes have not been checked (no verdict, or one bound to earlier source)",
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (!byKey.has(slideKey)) continue; // checked, current, and evidence passed
|
|
571
|
+
// Checked, nothing violated, and something still unproven. Which of two
|
|
572
|
+
// things that is decides whether it refuses.
|
|
573
|
+
//
|
|
574
|
+
// The package channel now exists (`src/pptx/package-evidence.mjs`), so a
|
|
575
|
+
// slide whose import and delivered bytes are both on disk CAN reach
|
|
576
|
+
// `passed` — and one that did not is a genuine failure to prove, which
|
|
577
|
+
// refuses. That is the guide's rung, restored.
|
|
578
|
+
//
|
|
579
|
+
// A slide with no import baseline has no package to be proved against at
|
|
580
|
+
// all; refusing it would block authored decks for evidence that cannot
|
|
581
|
+
// exist, which is the outage this compromise was opened to avoid. It ships
|
|
582
|
+
// with the limitation named, and is still never recorded as a pass.
|
|
583
|
+
if (packageEvidenceReachable(deckDir, path.basename(fileOf.get(slideKey)))) {
|
|
584
|
+
problems.push({
|
|
585
|
+
...blocker,
|
|
586
|
+
code: "evidence_unproven",
|
|
587
|
+
reason:
|
|
588
|
+
"this slide can be proved against its imported and delivered packages, and this render did not prove it",
|
|
589
|
+
});
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
unproven.push(blocker);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// The note NAMES what went unproven, per slide and per element. A generic
|
|
596
|
+
// caveat would let a later reader mistake a shipped-with-note slide for a
|
|
597
|
+
// verified one, which is the only thing that would make this compromise
|
|
598
|
+
// unacceptable. The record still says `unknown`; nothing here writes a pass.
|
|
599
|
+
const WHY = {
|
|
600
|
+
relationships_not_resolved: "what it is bound to could not be resolved",
|
|
601
|
+
not_checked_in_render: "absent from this render's evidence",
|
|
602
|
+
not_checked_in_package: "no final-package evidence",
|
|
603
|
+
no_evidence_supplied: "no evidence at all",
|
|
604
|
+
scope_not_compared: "its protected properties were not compared",
|
|
605
|
+
no_identified_boxes: "the capture carried no element identities",
|
|
606
|
+
};
|
|
607
|
+
const note = unproven.length
|
|
608
|
+
? `preservation: ${unproven.length} slide${unproven.length === 1 ? "" : "s"} shipped with obligations this build ` +
|
|
609
|
+
`could not prove — nothing was violated, nothing was certified either:\n` +
|
|
610
|
+
unproven
|
|
611
|
+
.map((b) => {
|
|
612
|
+
const owed = new Map();
|
|
613
|
+
for (const c of doc.slides[b.slideKey]?.verdict?.checks ?? []) {
|
|
614
|
+
if (c.status !== "unknown" && c.status !== "unsupported") continue;
|
|
615
|
+
const id = c.elementId ?? "(slide)";
|
|
616
|
+
if (!owed.has(id)) owed.set(id, new Set());
|
|
617
|
+
owed.get(id).add(WHY[c.reason] ?? c.reason ?? "unproven");
|
|
618
|
+
}
|
|
619
|
+
const items = [...owed].map(([id, why]) => `${id} (${[...why].join("; ")})`);
|
|
620
|
+
return ` ${fileOf.get(b.slideKey) ?? b.slideKey} — ${items.join(", ") || b.reason}`;
|
|
621
|
+
})
|
|
622
|
+
.join("\n")
|
|
623
|
+
: null;
|
|
624
|
+
if (problems.length === 0) return { blocker: null, note };
|
|
625
|
+
|
|
626
|
+
const slug = deckSlug(deckDir);
|
|
627
|
+
const detail = problems.map((p) => {
|
|
628
|
+
const file = fileOf.get(p.slideKey) ?? p.slideKey;
|
|
629
|
+
const failed = failedOf(p.slideKey).map((c) => c.elementId);
|
|
630
|
+
const owed = failed.length > 0 ? ` — unmet: ${[...new Set(failed)].join(", ")}` : "";
|
|
631
|
+
return ` ${file} — ${p.reason}${owed}`;
|
|
632
|
+
});
|
|
633
|
+
return { note, blocker: (
|
|
634
|
+
`${problems.length} slide${problems.length === 1 ? "" : "s"} cannot be replaced: ` +
|
|
635
|
+
`${problems.length === 1 ? "its" : "their"} preservation obligations are not established.\n${detail.join("\n")}\n` +
|
|
636
|
+
`Fix the [preserve:warn] lines and re-run \`slidemind view-draft ${slug}\` to re-establish the verdict. ` +
|
|
637
|
+
`An intentional removal is superseded explicitly: \`slidemind revision begin ${slug} --operation targeted-edit --slides <sel>\`.`
|
|
638
|
+
) };
|
|
639
|
+
}
|