slidemind 0.3.19 → 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/engine-slides.mjs +1 -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
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Transport-free draft diagnostics. Both clients use the same counts and asset rules.
|
|
2
|
+
import { contains, pathDistance, pointRef, sanitizeTrace } from "./line-fit.mjs";
|
|
3
|
+
import { fitNative, nodePreset, toleranceFor } from "./line-native.mjs";
|
|
4
|
+
import { readLines } from "./concept-lines.mjs";
|
|
5
|
+
import { connectorPaths, measuredFromRender } from "./settle.mjs";
|
|
6
|
+
import { arrange, correspond, linesWithin, repeatGroups } from "./repeat-groups.mjs";
|
|
7
|
+
import { fixedAspect } from "./concept-layout.mjs";
|
|
8
|
+
|
|
9
|
+
// What each draft line asks for — one table for both clients (the add-in's
|
|
10
|
+
// see_draft and build_and_push, the CLI's view-draft, review and delivery).
|
|
11
|
+
// A DEFECT is a measured fault in what would ship: text on text, a word too
|
|
12
|
+
// wide for its frame, one text split into boxes, a missing approved image or
|
|
13
|
+
// asset, an element a preserved slide lost; any other `[<rule>:error]` is a
|
|
14
|
+
// gate refusal. A defect holds the user's gate and delivery until a fresh
|
|
15
|
+
// draft shows it fixed. An ADVISORY names a difference to check against the
|
|
16
|
+
// concept and never holds anything (repairing every one was a second loop per
|
|
17
|
+
// build). Lines outside the table are notes.
|
|
18
|
+
export const DRAFT_DEFECT_TAGS = Object.freeze([
|
|
19
|
+
"overlap:warn",
|
|
20
|
+
"fit:warn",
|
|
21
|
+
"split:error",
|
|
22
|
+
"image:error",
|
|
23
|
+
"asset:error",
|
|
24
|
+
"preserve:warn",
|
|
25
|
+
"preserve:error",
|
|
26
|
+
]);
|
|
27
|
+
export const DRAFT_ADVISORY_TAGS = Object.freeze([
|
|
28
|
+
"fill:warn",
|
|
29
|
+
"type:warn",
|
|
30
|
+
"image:warn",
|
|
31
|
+
"image:note",
|
|
32
|
+
"repeat:warn",
|
|
33
|
+
"list:warn",
|
|
34
|
+
"split:warn",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** "defect", "advisory", or null for a line that is not a draft line. */
|
|
38
|
+
export function draftLineTier(line) {
|
|
39
|
+
const tag = /^\s*\[([a-z-]+:[a-z]+)\]/.exec(String(line ?? ""))?.[1];
|
|
40
|
+
if (!tag) return null;
|
|
41
|
+
if (DRAFT_ADVISORY_TAGS.includes(tag)) return "advisory";
|
|
42
|
+
if (DRAFT_DEFECT_TAGS.includes(tag) || tag.endsWith(":error")) return "defect";
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function borrowedAssetOwner(stem, file) {
|
|
47
|
+
const owner = /^(\d\d)[-_.]/.exec(file)?.[1];
|
|
48
|
+
if (!owner) return null;
|
|
49
|
+
const mine = /^(\d\d)[-_.]/.exec(stem)?.[1];
|
|
50
|
+
if (!mine || mine === owner) return null;
|
|
51
|
+
return owner;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// An arrow is a line with a head (concept-lines.mjs counts the concept's the
|
|
55
|
+
// same way). A render that carries no caps (the add-in's render evidence keeps
|
|
56
|
+
// only `{ native }`) counts every line, as before.
|
|
57
|
+
const HEAD = /^(?:triangle|stealth|arrow|diamond):/;
|
|
58
|
+
const headed = (c) => !("capStart" in c || "capEnd" in c) || HEAD.test(c.capStart ?? "") || HEAD.test(c.capEnd ?? "");
|
|
59
|
+
|
|
60
|
+
export const countRendered = (render) => {
|
|
61
|
+
const have = { arrows: 0, charts: 0, icons: 0 };
|
|
62
|
+
const walk = (box) => {
|
|
63
|
+
if (!box || typeof box !== "object") return;
|
|
64
|
+
if (box.connector && !box.connector.native && headed(box.connector)) have.arrows += 1;
|
|
65
|
+
if (box.chart) have.charts += 1;
|
|
66
|
+
if (box.imgSrc) have.icons += 1;
|
|
67
|
+
for (const c of box.children ?? []) walk(c);
|
|
68
|
+
};
|
|
69
|
+
for (const b of render?.boxes ?? []) walk(b);
|
|
70
|
+
return have;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// What each line tells the agent to DO, per client: the diagnosis is shared,
|
|
74
|
+
// the remedy names the tools that client's agent actually has. The defaults are
|
|
75
|
+
// the CLI's commands.
|
|
76
|
+
export const CLI_REMEDIES = {
|
|
77
|
+
sourceIcon: "search the image engine for the motif, then for what the element stands for (the step, label or claim it anchors), and attach the result that carries that meaning with `download-result --name <the referenced file>`",
|
|
78
|
+
specHint: "state/concept-specs/concept-slide-NN.json",
|
|
79
|
+
borrowed: (stem) => `download-result --name ${stem.slice(0, 2)}-<element id>.svg`,
|
|
80
|
+
missing: (file) => `Retry transcribe when the engine failed to answer; when it found no match, source another motif for what the element stands for (download-result --name ${file})`,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function imageCountLines({ fileName, src, render, want, fromSpec = true, lowered = [], remedies = CLI_REMEDIES }) {
|
|
84
|
+
const source = fromSpec ? "the concept spec lists" : "the image shows";
|
|
85
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
86
|
+
// Every line is a native line directive, glued or free; the render counts them.
|
|
87
|
+
const have = render?.boxes
|
|
88
|
+
? countRendered(render)
|
|
89
|
+
: {
|
|
90
|
+
// Each line tag, then its cap attributes, in whatever order they come.
|
|
91
|
+
arrows: (src.match(/<[^<>]*\bdata-(?:connector|sm-line)\b[^<>]*>/g) ?? [])
|
|
92
|
+
.filter((tag) => [...tag.matchAll(/\sdata-cap-(?:start|end)="([^"]*)"/g)].some((m) => HEAD.test(m[1]))).length,
|
|
93
|
+
charts: (src.match(/<Chart\b/g) ?? []).length,
|
|
94
|
+
icons: (src.match(/<img\b/g) ?? []).length,
|
|
95
|
+
};
|
|
96
|
+
const label = { arrows: "rendered connector", charts: "rendered chart", icons: "rendered icon" };
|
|
97
|
+
const lines = [];
|
|
98
|
+
if (lowered.length) {
|
|
99
|
+
lines.push(
|
|
100
|
+
`[image:warn] ${stem} — the image line was lowered (${lowered.join(", ")}). The count is the image's, not the JSX's: the first count stands, add the missing elements.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
for (const k of ["arrows", "charts", "icons"]) {
|
|
104
|
+
if (want[k] != null && have[k] < want[k]) {
|
|
105
|
+
// Icons come from an engine that may lack a motif (x29: two
|
|
106
|
+
// double-chevron glyphs no search returned), so an icon shortfall is
|
|
107
|
+
// a note; arrows and charts are drawn, so theirs is a warning
|
|
108
|
+
// (DRAFT_ADVISORY_TAGS: both are checked against the concept, neither
|
|
109
|
+
// holds a gate). A motif without a match is not a meaning without one:
|
|
110
|
+
// the remedy is another motif for the element's meaning, never a
|
|
111
|
+
// stand-in for the set (x114).
|
|
112
|
+
const advisory = k === "icons";
|
|
113
|
+
lines.push(
|
|
114
|
+
`[image:${advisory ? "note" : "warn"}] ${stem} — ${source} ${k}=${want[k]}, the render has ${have[k]} ${label[k]}${have[k] === 1 ? "" : "s"}. ${advisory ? `Each missing icon is a visible hole the reviewer will see: ${remedies.sourceIcon}. An engine that is unavailable or timed out has not answered — retry and keep the motif. Deleting the element, or setting a number, letter or plain shape in its place or across its set, does not fix it: every member keeps an asset in the set's one box, and only a brand with no image engine anchors a set typographically (composition.md → Icons and imagery).` : `Add the missing ${k}${fromSpec ? ` (${remedies.specHint} names each one)` : ""}.`}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function missingAssetLines({ fileName, src, hasAsset, hasBundleAsset = () => false, remedies = CLI_REMEDIES }) {
|
|
122
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
123
|
+
const lines = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const m of src.matchAll(/["'`]\.\/assets\/([^"'`{}\s]+)["'`]/g)) {
|
|
126
|
+
const file = m[1];
|
|
127
|
+
if (seen.has(file)) continue;
|
|
128
|
+
seen.add(file);
|
|
129
|
+
const owner = borrowedAssetOwner(stem, file);
|
|
130
|
+
if (owner) {
|
|
131
|
+
lines.push(
|
|
132
|
+
`[asset:error] ${stem} — ./assets/${file} was fetched for slide ${owner}, and its colour was chosen for that slide's background. Source the required motif with this slide's colour (${remedies.borrowed(stem)}). Do not substitute an unrelated same-slide icon or delete approved content to clear this diagnostic.`,
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (hasAsset(file)) continue;
|
|
137
|
+
// A brand chrome/master asset lives ONLY in the bundle cache — it is never
|
|
138
|
+
// copied into a deck. `rewriteBrandAssetRefs` (src/builders/build.mjs)
|
|
139
|
+
// redirects exactly these names to the brand URL when the deck does not
|
|
140
|
+
// own a file of that name, and the server resolves them from the design,
|
|
141
|
+
// so they are not missing. Mirror that rule rather than a bare prefix
|
|
142
|
+
// test: a deck that DOES own `assets/master-data.png` fell out above.
|
|
143
|
+
if (/^(?:chrome|master)-/.test(file) && hasBundleAsset(file)) continue;
|
|
144
|
+
lines.push(
|
|
145
|
+
`[asset:error] ${stem} — ./assets/${file} is referenced but not on disk. ${remedies.missing(file)}; do not delete approved content or substitute unrelated artwork.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ROUND = new Set(["circle", "ellipse", "ring"]);
|
|
152
|
+
const MAX_LINE_SHAPE_LINES = 6;
|
|
153
|
+
const SHAPE_WORD = { straight: "straight line", orthogonal: "elbow", curve: "curve", arc: "arc", polyline: "polyline" };
|
|
154
|
+
// A fitted connector's shape attributes as the slide writes them.
|
|
155
|
+
const shapeAttrs = (n) => [
|
|
156
|
+
...(n.from?.node ? [] : [`data-from="${pointRef(n.start)}"`]),
|
|
157
|
+
...(n.to?.node ? [] : [`data-to="${pointRef(n.end)}"`]),
|
|
158
|
+
`data-prst="${n.prst}"`,
|
|
159
|
+
...(Object.keys(n.av ?? {}).length ? [`data-adj="${Object.entries(n.av).map(([k, v]) => `${k}:${Math.round(v)}`).join(",")}"`] : []),
|
|
160
|
+
...(n.rot ? [`data-rot="${n.rot}"`] : []),
|
|
161
|
+
...(n.from?.node ? [`data-from-site="${n.from.site}"`] : []),
|
|
162
|
+
...(n.to?.node ? [`data-to-site="${n.to.site}"`] : []),
|
|
163
|
+
].join(" ");
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Every rendered connector measured against the line the concept draws, the
|
|
167
|
+
* way the fitter measures it: the directive's own preset, adjusts, turn and
|
|
168
|
+
* sites (the slide source `src`) placed over the nodes as the render measured
|
|
169
|
+
* them, evaluated by the shared preset evaluator, compared with the spec's
|
|
170
|
+
* trace. A line beyond tolerance is named with its distance — the shape the
|
|
171
|
+
* reviewer will see is wrong even though the count is right.
|
|
172
|
+
*/
|
|
173
|
+
export function lineShapeLines({ fileName, spec, src, render }) {
|
|
174
|
+
if (!render?.boxes?.length || !spec || !src) return [];
|
|
175
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
176
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
177
|
+
const round = (id) => { const e = byId.get(id); return e?.kind === "disc" || ROUND.has(e?.shape); };
|
|
178
|
+
// The concept was read onto a slot of the spec's size; a render on another
|
|
179
|
+
// (a layout canvas change) moves every line, which no shape knob undoes.
|
|
180
|
+
const slot = spec.slot;
|
|
181
|
+
const root = render.boxes[0]?.rect;
|
|
182
|
+
if (root && (Math.abs(root.w - slot.w) > 1 || Math.abs(root.h - slot.h) > 1)) return [];
|
|
183
|
+
const read = new Map(readLines(structuredClone(spec)).lines.map((l) => [l.id, l]));
|
|
184
|
+
const measured = measuredFromRender(render, spec.slot);
|
|
185
|
+
// A line drawn on a painted element (a mark on its disc) moves with it: its
|
|
186
|
+
// place is judged where that element renders, not where the sketch put it.
|
|
187
|
+
const carried = (line) => {
|
|
188
|
+
const el = byId.get(line?.source?.id);
|
|
189
|
+
const on = el?.parent ? byId.get(el.parent) : null;
|
|
190
|
+
const m = on?.fill && on.fill !== "none" && contains(on, el) ? measured.get(on.id) : null;
|
|
191
|
+
return m ? { x: m.x - on.x, y: m.y - on.y } : { x: 0, y: 0 };
|
|
192
|
+
};
|
|
193
|
+
// A free straight rule drawn inside a card (a divider under its title) is
|
|
194
|
+
// the card's layout, placed by the settle (text fit, one template per set),
|
|
195
|
+
// not a relationship the concept routes: its place is judged by the text
|
|
196
|
+
// and occlusion checks, never by the sketch's pixel position.
|
|
197
|
+
const rules = new Set((spec.elements ?? [])
|
|
198
|
+
.filter((e) => e.fill && e.fill !== "none" && !fixedAspect(e) && e.w > 0 && e.h > 0)
|
|
199
|
+
.flatMap((e) => linesWithin(spec, e))
|
|
200
|
+
.filter((c) => !c.from && !c.to && (c.route ?? "straight") === "straight")
|
|
201
|
+
.map((c) => c.id));
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const c of connectorPaths(src, measured, spec)) {
|
|
204
|
+
if (rules.has(c.id)) continue;
|
|
205
|
+
const line = read.get(c.id);
|
|
206
|
+
const shift = carried(line);
|
|
207
|
+
const raw = (line?.path?.points ?? []).filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y)).map((p) => ({ x: p.x + shift.x, y: p.y + shift.y }));
|
|
208
|
+
if (!(raw.length >= 2)) continue;
|
|
209
|
+
const trace = sanitizeTrace(raw, c.fromBox, c.toBox, { fromRound: round(c.fromEnd.node), toRound: round(c.toEnd.node) });
|
|
210
|
+
// The fitter's class: an end the concept glues keeps the glued tolerance
|
|
211
|
+
// after the fitter freed it onto the ink (no site near where it attaches).
|
|
212
|
+
const glued = Boolean(c.fromEnd.node || c.toEnd.node || line.ends?.some((e) => e.node));
|
|
213
|
+
const tolerance = toleranceFor(trace, glued);
|
|
214
|
+
const distance = pathDistance(c.points, trace);
|
|
215
|
+
if (!(Number.isFinite(distance) && distance > tolerance)) continue;
|
|
216
|
+
// The ONE directive the concept's shape names at these nodes: the fitter's
|
|
217
|
+
// own answer on the rendered geometry, never a search left to the agent.
|
|
218
|
+
const nodes = new Map(line.ends.filter((e) => e.node && measured.has(e.node)).map((e) => [e.node, nodePreset(byId.get(e.node), measured.get(e.node))]).filter(([, n]) => n));
|
|
219
|
+
const ends = line.ends.map((e) => ({ ...e, at: e.at && { x: e.at.x + shift.x, y: e.at.y + shift.y } }));
|
|
220
|
+
const fit = fitNative({ ...line, ends, path: { ...line.path, points: raw } }, nodes, null, { slot });
|
|
221
|
+
const shape = SHAPE_WORD[line.path?.kind] ?? "line";
|
|
222
|
+
const fix = fit?.object === "cxn" && fit.devPx <= tolerance && fit.devPx < distance - 1
|
|
223
|
+
? `Set ${shapeAttrs(fit)} — the one directive that draws the concept's ${shape} here — and keep it.`
|
|
224
|
+
: `No directive at these nodes draws the concept's ${shape} closer: keep this one and name the difference in your reply.`;
|
|
225
|
+
out.push(`[image:warn] ${stem} — the line ${c.id} (${c.from} → ${c.to}) runs ${Math.round(distance)}px from the concept's line (tolerance ${Math.round(tolerance)}px). Judge its shape (straight, elbow, curve or arc), not its pixels: where you aligned or evened the nodes on purpose, the line follows them. ${fix} Never try presets or sites one by one to quiet this.`);
|
|
226
|
+
}
|
|
227
|
+
if (out.length > MAX_LINE_SHAPE_LINES) {
|
|
228
|
+
const more = out.length - MAX_LINE_SHAPE_LINES;
|
|
229
|
+
return [...out.slice(0, MAX_LINE_SHAPE_LINES), `[image:warn] ${stem} — ${more} more line${more === 1 ? "" : "s"} off the concept's shape.`];
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const REPEAT_TOL = 3; // px: two members of one set may differ by rendering noise only (text tops round per line box)
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A set the reader declared (spec.repeats) as it renders now: one size, one
|
|
238
|
+
* inner layout, the arrangement's symmetry. The transcriber builds it that
|
|
239
|
+
* way; this names what an edit changed on one member and not the others. Text
|
|
240
|
+
* heights follow their copy and are not compared.
|
|
241
|
+
*/
|
|
242
|
+
export function repeatLines({ fileName, spec, render }) {
|
|
243
|
+
if (!render?.boxes?.length || !spec?.repeats?.length) return [];
|
|
244
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
245
|
+
const m = measuredFromRender(render, spec.slot);
|
|
246
|
+
const rendered = { ...spec, elements: (spec.elements ?? []).map((e) => (m.has(e.id) ? { ...e, ...pick(m.get(e.id)) } : e)) };
|
|
247
|
+
const out = [];
|
|
248
|
+
const r = (n) => Math.round(n);
|
|
249
|
+
for (const g of repeatGroups(rendered)) {
|
|
250
|
+
// A virtual member (a row of loose parts) is its parts' box: rendered when they all are.
|
|
251
|
+
const members = g.members.filter((e) => (e.ids ? e.ids.every((id) => m.has(id)) : m.has(e.id)));
|
|
252
|
+
if (members.length < 2) continue;
|
|
253
|
+
const name = `${g.id} (${members.map((e) => (e.ids ? `(${e.ids.join(" + ")})` : e.id)).join(", ")})`;
|
|
254
|
+
// Its box follows its words, so only its inner layout is compared.
|
|
255
|
+
const loose = members.some((e) => e.virtual);
|
|
256
|
+
const size = (e) => `${r(e.w)}×${r(e.h)}px`;
|
|
257
|
+
// An engine icon's own aspect is kept by the equal box's `fit="contain"`,
|
|
258
|
+
// never by sizing each member's box to its asset.
|
|
259
|
+
const aspect = (els) => (els.some((e) => m.get(e.id)?.content) ? ` A picture keeps its own aspect inside an equal box (fit="contain").` : "");
|
|
260
|
+
const off = loose ? [] : members.filter((e) => Math.abs(e.w - members[0].w) > REPEAT_TOL || Math.abs(e.h - members[0].h) > REPEAT_TOL);
|
|
261
|
+
if (off.length) {
|
|
262
|
+
out.push(`[repeat:warn] ${stem} — ${name} is one organism but renders in ${[...new Set(members.map(size))].join(" and ")}. Give every member the same size.${aspect(members)}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const kids = correspond(rendered.elements, members);
|
|
266
|
+
for (const [k, first] of (kids?.[0] ?? []).entries()) {
|
|
267
|
+
const rel = kids.map((l, i) => ({ e: l[k], x: l[k].x - members[i].x, y: l[k].y - members[i].y }));
|
|
268
|
+
const text = first.kind === "text";
|
|
269
|
+
const odd = rel.find((q) => Math.abs(q.x - rel[0].x) > REPEAT_TOL || Math.abs(q.y - rel[0].y) > REPEAT_TOL
|
|
270
|
+
|| Math.abs(q.e.w - rel[0].e.w) > REPEAT_TOL || (!text && Math.abs(q.e.h - rel[0].e.h) > REPEAT_TOL));
|
|
271
|
+
if (odd) out.push(`[repeat:warn] ${stem} — ${name}: #${odd.e.id} sits at ${r(odd.x)},${r(odd.y)} (${size(odd.e)}) in its card, #${rel[0].e.id} at ${r(rel[0].x)},${r(rel[0].y)} (${size(rel[0].e)}). One inner layout: change every member alike.${aspect([odd.e])}`);
|
|
272
|
+
}
|
|
273
|
+
const at = loose ? null : arrange({ ...g, members }, members[0].w, members[0].h);
|
|
274
|
+
const drift = at && members.map((e, i) => ({ e, d: Math.hypot(e.x + e.w / 2 - at[i].x, e.y + e.h / 2 - at[i].y) })).filter((q) => q.d > REPEAT_TOL);
|
|
275
|
+
if (drift?.length) out.push(`[repeat:warn] ${stem} — ${name} is a ${g.arrangement}${g.hub ? ` around #${g.hub.id}` : ""}, but ${drift.map((q) => `#${q.e.id} sits ${r(q.d)}px off it`).join(", ")}. Move the members together so the ${g.arrangement} stays even${g.arrangement === "ring" ? " and symmetric about its hub" : ""}.`);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pick = (v) => ({ x: v.x, y: v.y, w: v.w, h: v.h });
|
|
281
|
+
|
|
282
|
+
// The items the concept declares (concept-lists.mjs: `marker` on a text or
|
|
283
|
+
// on a block's paragraphs) against the `<li>` the slide writes. x101–x105: 22
|
|
284
|
+
// native lists left the first draft and came back as plain paragraphs or
|
|
285
|
+
// typed glyphs — a flattened list the glyph lint alone cannot name when the
|
|
286
|
+
// glyphs are gone too. A slide built from set components counts its template
|
|
287
|
+
// once, so it is not compared.
|
|
288
|
+
export function listLines({ fileName, spec, src }) {
|
|
289
|
+
const stem = fileName.replace(/\.[^.]+$/, "");
|
|
290
|
+
const source = String(src ?? "");
|
|
291
|
+
if (!source || /const Repeat\d+ = /.test(source)) return [];
|
|
292
|
+
const declared = (spec?.elements ?? []).reduce((n, e) => n + (e.paragraphs ? e.paragraphs.filter((p) => p.marker).length : e.marker ? 1 : 0), 0);
|
|
293
|
+
if (!declared) return [];
|
|
294
|
+
const written = (source.match(/<li[\s>]/g) ?? []).length;
|
|
295
|
+
if (written >= declared) return [];
|
|
296
|
+
return [`[list:warn] ${stem} — the concept declares ${declared} list item${declared === 1 ? "" : "s"}; the slide writes ${written} <li>. A flattened list: restore the <L> (bullet="✓" for ticks) with one <li> per item — a title that should not be bulleted becomes a <P>, never the whole list.`];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Preservation — what a preserved slide still owes, in one vocabulary and one
|
|
301
|
+
// wording on both clients (the add-in's preservation-gate.ts / render-batch.ts,
|
|
302
|
+
// the CLI's draft-check.mjs): `[preserve:warn]` an obligation this draft
|
|
303
|
+
// breaks (a defect), `[preserve:error]` a record nobody can read (delivery
|
|
304
|
+
// refuses until repaired). What no draft can answer is not a draft line: the
|
|
305
|
+
// agent cannot act on it, and delivery names it as a limitation.
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
// Failures that mean the element itself is gone.
|
|
309
|
+
const GONE = new Set(["removed_from_source", "absent_from_render", "absent_from_package"]);
|
|
310
|
+
|
|
311
|
+
// What a required element showed, so a line names content, not a number.
|
|
312
|
+
function shownAs(req) {
|
|
313
|
+
if (req?.excerpt) return ` "${req.excerpt}"`;
|
|
314
|
+
if (req?.mediaRef) return ` (picture ${req.mediaRef})`;
|
|
315
|
+
if (req?.kind === "native-object") return " (embedded object)";
|
|
316
|
+
if (req?.properties?.includes("chart")) return " (chart)";
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const stemOf = (fileName) => String(fileName).replace(/\.[^.]+$/, "");
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* One `[preserve:warn]` per element with a FAILED check, every failed check of
|
|
324
|
+
* it in one line. An element that is gone gets the one fix — its id back on the
|
|
325
|
+
* element that shows it (`data-sm-source-id={87}` for an imported shape); any
|
|
326
|
+
* other failure is restored in the slide's file. `supersede` is the client's
|
|
327
|
+
* way to declare an intended removal (the CLI's scoped revision), or null where
|
|
328
|
+
* the agent has none of its own.
|
|
329
|
+
*/
|
|
330
|
+
export function preservationFailureLines({ fileName, checks, active, supersede = null }) {
|
|
331
|
+
const byElement = new Map();
|
|
332
|
+
for (const c of checks ?? []) {
|
|
333
|
+
if (c?.status !== "failed") continue;
|
|
334
|
+
byElement.set(c.elementId ?? null, [...(byElement.get(c.elementId ?? null) ?? []), c]);
|
|
335
|
+
}
|
|
336
|
+
const stem = stemOf(fileName);
|
|
337
|
+
const or = supersede ? `, or ${supersede}` : "";
|
|
338
|
+
const refuses = "; a final replacement refuses while it stands.";
|
|
339
|
+
return [...byElement].map(([elementId, failed]) => {
|
|
340
|
+
const req = (active ?? []).find((r) => r?.elementId === elementId);
|
|
341
|
+
const where = elementId != null ? `#${elementId}${shownAs(req)}` : "the slide";
|
|
342
|
+
if (elementId != null && req?.kind !== "connection" && failed.every((c) => GONE.has(c.reason ?? ""))) {
|
|
343
|
+
const why = [...new Set(failed.map((c) => c.reason))].join(", ");
|
|
344
|
+
const idAttr = /^\d+$/.test(String(elementId)) ? `data-sm-source-id={${elementId}}` : `id="${elementId}"`;
|
|
345
|
+
return `[preserve:warn] ${stem} — ${where}: no longer on the slide (${why}) — it must stay: put ${idAttr} on the element that shows it${or}${refuses}`;
|
|
346
|
+
}
|
|
347
|
+
const what = failed
|
|
348
|
+
.map((c) => {
|
|
349
|
+
const detail = String(c.detail ?? "");
|
|
350
|
+
const said = elementId != null && detail.startsWith(`${elementId} `) ? detail.slice(String(elementId).length + 1) : detail;
|
|
351
|
+
return `${c.reason ?? "no reason given"}${c.scope ? ` (${c.scope})` : ""}${said ? ` — ${said}` : ""}`;
|
|
352
|
+
})
|
|
353
|
+
.join("; ");
|
|
354
|
+
return `[preserve:warn] ${stem} — ${where}: ${what} — restore it in slides/${fileName}${or}${refuses}`;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* `[preserve:error]` — the deck's preservation record cannot be read, so
|
|
360
|
+
* nothing about what its slides must keep can be verified and delivery
|
|
361
|
+
* refuses until it is repaired. The record is not the agent's to edit.
|
|
362
|
+
*/
|
|
363
|
+
export function preservationUnreadableLine({ fileName, detail = null }) {
|
|
364
|
+
return (
|
|
365
|
+
`[preserve:error] ${stemOf(fileName)} — this deck's preservation record could not be read` +
|
|
366
|
+
`${detail ? ` (${detail})` : ""}; nothing could be verified about what its slides must keep, and delivery ` +
|
|
367
|
+
`refuses until it is repaired. This is not a slide defect: never edit that file — name the problem in your reply.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Which route a deck is on, read from disk. Its own module because BOTH gates
|
|
2
|
+
// ask it — plan.mjs imports review.mjs, so the predicate cannot live in either
|
|
3
|
+
// without a cycle.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { splitPlanMarkdownIntoSegments } from "./storyboard-markdown.mjs";
|
|
8
|
+
import { readRevision } from "../lib/revision.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A deck that already EXISTS — adopted from someone's .pptx — with no storyboard
|
|
12
|
+
* in its plan.md is on the EDIT route, not the authoring route. Neither gate
|
|
13
|
+
* applies: there is no plan to sign off (the deck is the plan, and the
|
|
14
|
+
* storyboard gate exists to approve a story before it is built), and no preview
|
|
15
|
+
* to approve (the .pptx was the user's before any agent touched it, and changes
|
|
16
|
+
* go straight into it — the same rule as an authored deck after its one
|
|
17
|
+
* review). Manufacturing an approval for either would be a checkmark nobody gave.
|
|
18
|
+
*/
|
|
19
|
+
export function isEditRouteDeck(deckDir) {
|
|
20
|
+
const revision = readRevision(deckDir);
|
|
21
|
+
if (revision?.origin?.kind !== "adopted") return false;
|
|
22
|
+
let markdown = "";
|
|
23
|
+
try {
|
|
24
|
+
markdown = fs.readFileSync(path.join(deckDir, "plan.md"), "utf8");
|
|
25
|
+
} catch {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return splitPlanMarkdownIntoSegments(markdown).every((s) => s.type !== "storyboard");
|
|
29
|
+
}
|