@ijfw/memory-server 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// uispec-drift.js -- v1.5.0 audit-MED-design-#8 + #10.
|
|
2
|
+
//
|
|
3
|
+
// Two design-contract enforcement helpers, sharing one file because they
|
|
4
|
+
// read the same UI-SPEC.md.
|
|
5
|
+
//
|
|
6
|
+
// #8 Bundle-size budget: parse `bundle_kb_budget: <N>` from UI-SPEC.md,
|
|
7
|
+
// compare to a measured KB total from the shipped build output.
|
|
8
|
+
//
|
|
9
|
+
// #10 Palette / Tailwind drift detector: parse `## 3. Color & Contrast`
|
|
10
|
+
// tokens from UI-SPEC.md, scan shipped code for `class="..."` Tailwind
|
|
11
|
+
// color classes, flag any color tokens NOT declared in the spec.
|
|
12
|
+
//
|
|
13
|
+
// Pure-stdlib. Graceful-degrade on every error path. No external network.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, extname } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// UI-SPEC parser
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a UI-SPEC.md text body into a structured contract.
|
|
24
|
+
*
|
|
25
|
+
* Looks for:
|
|
26
|
+
* - `bundle_kb_budget: <N>` -- inline YAML-ish field, any line.
|
|
27
|
+
* - `a11y_target: <ID>` -- e.g. `WCAG-2.2-AA`
|
|
28
|
+
* - `max_violations: <N>` -- a11y violation budget
|
|
29
|
+
* - Color tokens of form `#rrggbb` or `rgb()` under the "Color & Contrast"
|
|
30
|
+
* section (between `## 3.` and the next `## ` header).
|
|
31
|
+
*
|
|
32
|
+
* @param {string} text
|
|
33
|
+
* @returns {{bundleKbBudget: number|null, a11yTarget: string|null, maxViolations: number|null, paletteHex: string[], paletteTokens: string[]}}
|
|
34
|
+
*/
|
|
35
|
+
export function parseUISpec(text) {
|
|
36
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
37
|
+
return { bundleKbBudget: null, a11yTarget: null, maxViolations: null, paletteHex: [], paletteTokens: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fieldMatch = (pat) => {
|
|
41
|
+
const m = text.match(pat);
|
|
42
|
+
return m ? m[1].trim() : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const bundleStr = fieldMatch(/^[*\s>-]*bundle_kb_budget\s*:\s*([0-9]+)\s*$/im);
|
|
46
|
+
const a11yTarget = fieldMatch(/^[*\s>-]*a11y_target\s*:\s*([A-Za-z0-9.-]+)\s*$/im);
|
|
47
|
+
const violationsStr = fieldMatch(/^[*\s>-]*max_violations\s*:\s*([0-9]+)\s*$/im);
|
|
48
|
+
|
|
49
|
+
// Color section: scan from "## 3" through next "## " or EOF.
|
|
50
|
+
const colorSection = (() => {
|
|
51
|
+
const i = text.search(/^##\s*3\b/m);
|
|
52
|
+
if (i < 0) return '';
|
|
53
|
+
const rest = text.slice(i + 1);
|
|
54
|
+
const j = rest.search(/^##\s+/m);
|
|
55
|
+
return j < 0 ? rest : rest.slice(0, j);
|
|
56
|
+
})();
|
|
57
|
+
|
|
58
|
+
const hex = Array.from(colorSection.matchAll(/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g))
|
|
59
|
+
.map((m) => `#${m[1].toLowerCase()}`)
|
|
60
|
+
// Normalise 3-digit -> 6-digit so comparison is stable.
|
|
61
|
+
.map((c) => (c.length === 4 ? `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}` : c));
|
|
62
|
+
|
|
63
|
+
// Tailwind-style token names declared in the section (e.g. `bg-slate-900`, `text-emerald-500`).
|
|
64
|
+
const tokens = Array.from(
|
|
65
|
+
colorSection.matchAll(/\b((?:bg|text|border|ring|from|to|via|fill|stroke)-[a-z]+-\d{2,3})\b/g),
|
|
66
|
+
).map((m) => m[1]);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
bundleKbBudget: bundleStr ? Number(bundleStr) : null,
|
|
70
|
+
a11yTarget,
|
|
71
|
+
maxViolations: violationsStr ? Number(violationsStr) : null,
|
|
72
|
+
paletteHex: Array.from(new Set(hex)),
|
|
73
|
+
paletteTokens: Array.from(new Set(tokens)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// #8 Bundle-size budget check
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const DEFAULT_BUNDLE_EXTS = new Set(['.js', '.mjs', '.cjs', '.css']);
|
|
82
|
+
const DEFAULT_BUILD_DIRS = ['.next', 'dist', 'build', 'out', 'public/build'];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk a build directory and sum sizes of JS/CSS assets.
|
|
86
|
+
* Graceful: missing dir -> {totalKb: null, files: [], dir: null}.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {string} [opts.dir] Specific build dir; auto-detect when absent.
|
|
90
|
+
* @param {string} [opts.projectRoot] Defaults to cwd.
|
|
91
|
+
* @param {Set<string>} [opts.exts] File extensions to include.
|
|
92
|
+
*/
|
|
93
|
+
export function measureBundleSize(opts = {}) {
|
|
94
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
95
|
+
const exts = opts.exts || DEFAULT_BUNDLE_EXTS;
|
|
96
|
+
|
|
97
|
+
let dir = opts.dir;
|
|
98
|
+
if (!dir) {
|
|
99
|
+
for (const candidate of DEFAULT_BUILD_DIRS) {
|
|
100
|
+
const abs = join(projectRoot, candidate);
|
|
101
|
+
if (existsSync(abs)) {
|
|
102
|
+
dir = abs;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (!opts.dir.startsWith('/')) {
|
|
107
|
+
dir = join(projectRoot, opts.dir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!dir || !existsSync(dir)) {
|
|
111
|
+
return { totalKb: null, files: [], dir: dir || null, reason: 'build-dir-missing' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const files = [];
|
|
115
|
+
let totalBytes = 0;
|
|
116
|
+
const stack = [dir];
|
|
117
|
+
while (stack.length > 0) {
|
|
118
|
+
const cur = stack.pop();
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const ent of entries) {
|
|
126
|
+
if (ent.name.startsWith('.')) continue;
|
|
127
|
+
const abs = join(cur, ent.name);
|
|
128
|
+
if (ent.isDirectory()) {
|
|
129
|
+
stack.push(abs);
|
|
130
|
+
} else if (ent.isFile() && exts.has(extname(ent.name))) {
|
|
131
|
+
try {
|
|
132
|
+
const sz = statSync(abs).size;
|
|
133
|
+
totalBytes += sz;
|
|
134
|
+
files.push({ path: abs, bytes: sz });
|
|
135
|
+
} catch {
|
|
136
|
+
/* unreadable -- skip */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
totalKb: Math.round((totalBytes / 1024) * 10) / 10,
|
|
144
|
+
files,
|
|
145
|
+
dir,
|
|
146
|
+
reason: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compose a budget verdict from a parsed spec + a bundle measurement.
|
|
152
|
+
*
|
|
153
|
+
* @param {{bundleKbBudget:number|null}} spec
|
|
154
|
+
* @param {{totalKb:number|null, dir:string|null}} measurement
|
|
155
|
+
* @returns {{pass: boolean|null, actualKb: number|null, budgetKb: number|null, reason: string}}
|
|
156
|
+
*/
|
|
157
|
+
export function evaluateBundleBudget(spec, measurement) {
|
|
158
|
+
if (!spec || spec.bundleKbBudget == null) {
|
|
159
|
+
return { pass: null, actualKb: measurement?.totalKb ?? null, budgetKb: null, reason: 'no-budget-declared' };
|
|
160
|
+
}
|
|
161
|
+
if (!measurement || measurement.totalKb == null) {
|
|
162
|
+
return { pass: null, actualKb: null, budgetKb: spec.bundleKbBudget, reason: measurement?.reason || 'no-measurement' };
|
|
163
|
+
}
|
|
164
|
+
const pass = measurement.totalKb <= spec.bundleKbBudget;
|
|
165
|
+
return {
|
|
166
|
+
pass,
|
|
167
|
+
actualKb: measurement.totalKb,
|
|
168
|
+
budgetKb: spec.bundleKbBudget,
|
|
169
|
+
reason: pass
|
|
170
|
+
? 'within-budget'
|
|
171
|
+
: `bundle ${measurement.totalKb} KB > budget ${spec.bundleKbBudget} KB`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// #10 Palette drift detector
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.html', '.vue', '.svelte', '.css', '.scss', '.mdx']);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Walk a source scope and return Tailwind color tokens + raw hex values
|
|
183
|
+
* found in `class="..."` / `className="..."` / inline `style="color:..."`.
|
|
184
|
+
*
|
|
185
|
+
* @param {string|string[]} scope Single dir or list of dirs (absolute or relative to projectRoot).
|
|
186
|
+
* @param {object} [opts]
|
|
187
|
+
* @param {string} [opts.projectRoot]
|
|
188
|
+
* @returns {{tokens: string[], hex: string[], files: number}}
|
|
189
|
+
*/
|
|
190
|
+
export function scanCodeForTailwind(scope, opts = {}) {
|
|
191
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
192
|
+
const dirs = Array.isArray(scope) ? scope : [scope];
|
|
193
|
+
const tokens = new Set();
|
|
194
|
+
const hex = new Set();
|
|
195
|
+
let files = 0;
|
|
196
|
+
|
|
197
|
+
for (const d of dirs) {
|
|
198
|
+
const abs = d.startsWith('/') ? d : join(projectRoot, d);
|
|
199
|
+
if (!existsSync(abs)) continue;
|
|
200
|
+
|
|
201
|
+
const stack = [abs];
|
|
202
|
+
while (stack.length > 0) {
|
|
203
|
+
const cur = stack.pop();
|
|
204
|
+
let entries;
|
|
205
|
+
try {
|
|
206
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const ent of entries) {
|
|
211
|
+
if (ent.name.startsWith('.') || ent.name === 'node_modules') continue;
|
|
212
|
+
const nxt = join(cur, ent.name);
|
|
213
|
+
if (ent.isDirectory()) {
|
|
214
|
+
stack.push(nxt);
|
|
215
|
+
} else if (ent.isFile() && CODE_EXTS.has(extname(ent.name))) {
|
|
216
|
+
let body;
|
|
217
|
+
try {
|
|
218
|
+
body = readFileSync(nxt, 'utf8');
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
files += 1;
|
|
223
|
+
// Tailwind color tokens, e.g. bg-slate-900, text-rose-500/50.
|
|
224
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- scans developer-authored Tailwind class strings in local source files; bounded {2,3} digit count
|
|
225
|
+
for (const m of body.matchAll(/\b((?:bg|text|border|ring|from|to|via|fill|stroke)-[a-z]+-\d{2,3})(?:\/\d+)?\b/g)) {
|
|
226
|
+
tokens.add(m[1]);
|
|
227
|
+
}
|
|
228
|
+
// Raw hex.
|
|
229
|
+
for (const m of body.matchAll(/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g)) {
|
|
230
|
+
const c = m[1].length === 3
|
|
231
|
+
? `#${m[1][0]}${m[1][0]}${m[1][1]}${m[1][1]}${m[1][2]}${m[1][2]}`
|
|
232
|
+
: `#${m[1].toLowerCase()}`;
|
|
233
|
+
hex.add(c);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
tokens: Array.from(tokens).sort(),
|
|
242
|
+
hex: Array.from(hex).sort(),
|
|
243
|
+
files,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Compute drift = tokens/hex used in code but NOT declared in UI-SPEC.
|
|
249
|
+
*
|
|
250
|
+
* @param {ReturnType<typeof parseUISpec>} spec
|
|
251
|
+
* @param {ReturnType<typeof scanCodeForTailwind>} scan
|
|
252
|
+
* @returns {Array<{type:'token'|'hex',value:string,severity:'flag'|'block',declared:string[]}>}
|
|
253
|
+
*/
|
|
254
|
+
export function diffPaletteDrift(spec, scan) {
|
|
255
|
+
const declaredTokens = new Set(spec.paletteTokens || []);
|
|
256
|
+
const declaredHex = new Set(spec.paletteHex || []);
|
|
257
|
+
|
|
258
|
+
const findings = [];
|
|
259
|
+
|
|
260
|
+
// Color-bearing utilities are the ones we lock to the palette.
|
|
261
|
+
// Spacing/layout utilities (bg-* with color names like 'transparent'/'current'/
|
|
262
|
+
// 'inherit') do not appear with -\d+ suffix so are not in scope.
|
|
263
|
+
for (const tok of scan.tokens) {
|
|
264
|
+
if (!declaredTokens.has(tok)) {
|
|
265
|
+
findings.push({
|
|
266
|
+
type: 'token',
|
|
267
|
+
value: tok,
|
|
268
|
+
severity: declaredTokens.size === 0 ? 'flag' : 'block',
|
|
269
|
+
declared: Array.from(declaredTokens),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const c of scan.hex) {
|
|
274
|
+
if (!declaredHex.has(c)) {
|
|
275
|
+
findings.push({
|
|
276
|
+
type: 'hex',
|
|
277
|
+
value: c,
|
|
278
|
+
severity: declaredHex.size === 0 ? 'flag' : 'block',
|
|
279
|
+
declared: Array.from(declaredHex),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return findings;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Read a UI-SPEC.md file from disk. Returns {ok, spec, error}.
|
|
289
|
+
*/
|
|
290
|
+
export function loadUISpec(uiSpecPath) {
|
|
291
|
+
if (!uiSpecPath || !existsSync(uiSpecPath)) {
|
|
292
|
+
return { ok: false, spec: null, error: 'ui-spec-missing' };
|
|
293
|
+
}
|
|
294
|
+
let body;
|
|
295
|
+
try {
|
|
296
|
+
body = readFileSync(uiSpecPath, 'utf8');
|
|
297
|
+
} catch (e) {
|
|
298
|
+
return { ok: false, spec: null, error: `read-failed: ${e.code || e.message}` };
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, spec: parseUISpec(body), error: null };
|
|
301
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// uispec-intake.js -- v1.5.0 audit-MED-design-#12.
|
|
2
|
+
//
|
|
3
|
+
// Accept a `--from-image <path>` or `--from-figma <url>` flag in the
|
|
4
|
+
// ui-spec flow that pre-fills UI-SPEC fields before the user reviews.
|
|
5
|
+
//
|
|
6
|
+
// Behaviour matrix:
|
|
7
|
+
//
|
|
8
|
+
// --from-image <path>
|
|
9
|
+
// - Validates the file exists, is a supported raster format (.png/.jpg/.jpeg/.webp/.gif),
|
|
10
|
+
// and is under 25MB.
|
|
11
|
+
// - Extracts the dimensions from the PNG/JPEG header (pure stdlib, no
|
|
12
|
+
// external lib). For non-PNG/JPEG we return null dims; not fatal.
|
|
13
|
+
// - Returns an UI-SPEC stub with:
|
|
14
|
+
// * `source.kind: image`
|
|
15
|
+
// * `source.path: <abs path>`
|
|
16
|
+
// * `source.bytes: <size>`
|
|
17
|
+
// * `source.dimensions: { width, height } | null`
|
|
18
|
+
// * `advisory: "vision pipeline deferred to v1.6.0"`
|
|
19
|
+
// The user (or downstream skill) fills the actual visual fields after
|
|
20
|
+
// inspecting the image. Token-time vision OCR is NOT done here; that's
|
|
21
|
+
// the v1.6.0 vision pipeline.
|
|
22
|
+
//
|
|
23
|
+
// --from-figma <url>
|
|
24
|
+
// - Validates URL is https + figma.com.
|
|
25
|
+
// - If `FIGMA_TOKEN` env var is set, fetches the file metadata via
|
|
26
|
+
// https://api.figma.com/v1/files/<fileKey> using node:https. No
|
|
27
|
+
// external HTTP library.
|
|
28
|
+
// - Returns an UI-SPEC stub with:
|
|
29
|
+
// * `source.kind: figma`
|
|
30
|
+
// * `source.url: <url>`
|
|
31
|
+
// * `source.fileKey: <key>`
|
|
32
|
+
// * `source.name: <fetched name | null>`
|
|
33
|
+
// * `source.lastModified: <iso | null>`
|
|
34
|
+
// * `advisory: "<reason if degraded>"`
|
|
35
|
+
//
|
|
36
|
+
// Graceful no-op: missing token, network error, non-https, all surface
|
|
37
|
+
// `advisory` strings rather than throwing.
|
|
38
|
+
|
|
39
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
40
|
+
import { resolve as pathResolve, extname } from 'node:path';
|
|
41
|
+
|
|
42
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
43
|
+
const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a `--from-image <path>` request into a UI-SPEC stub.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} imagePath
|
|
49
|
+
* @param {object} [opts]
|
|
50
|
+
* @param {string} [opts.projectRoot] resolves relative paths
|
|
51
|
+
* @returns {{ok: boolean, stub: object|null, error: string|null}}
|
|
52
|
+
*/
|
|
53
|
+
export function fromImage(imagePath, opts = {}) {
|
|
54
|
+
if (typeof imagePath !== 'string' || imagePath.length === 0) {
|
|
55
|
+
return { ok: false, stub: null, error: 'no-path' };
|
|
56
|
+
}
|
|
57
|
+
const abs = imagePath.startsWith('/')
|
|
58
|
+
? imagePath
|
|
59
|
+
: pathResolve(opts.projectRoot || process.cwd(), imagePath);
|
|
60
|
+
|
|
61
|
+
if (!existsSync(abs)) {
|
|
62
|
+
return { ok: false, stub: null, error: `image-not-found: ${abs}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ext = extname(abs).toLowerCase();
|
|
66
|
+
if (!IMAGE_EXTS.has(ext)) {
|
|
67
|
+
return { ok: false, stub: null, error: `unsupported-format: ${ext || '<no-ext>'}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let st;
|
|
71
|
+
try {
|
|
72
|
+
st = statSync(abs);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { ok: false, stub: null, error: `stat-failed: ${e.code || e.message}` };
|
|
75
|
+
}
|
|
76
|
+
if (st.size > MAX_IMAGE_BYTES) {
|
|
77
|
+
return { ok: false, stub: null, error: `image-too-large: ${st.size} bytes (limit ${MAX_IMAGE_BYTES})` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let dimensions = null;
|
|
81
|
+
try {
|
|
82
|
+
const head = readFileSync(abs).subarray(0, 512);
|
|
83
|
+
dimensions = parseDimensions(head, ext);
|
|
84
|
+
} catch {
|
|
85
|
+
// Tolerable; dimensions stay null.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
stub: {
|
|
91
|
+
source: {
|
|
92
|
+
kind: 'image',
|
|
93
|
+
path: abs,
|
|
94
|
+
bytes: st.size,
|
|
95
|
+
dimensions,
|
|
96
|
+
},
|
|
97
|
+
advisory: 'vision pipeline deferred to v1.6.0 — review the image and fill UI-SPEC pillars manually',
|
|
98
|
+
uiSpecHints: {
|
|
99
|
+
layout: '<derive from image: list surfaces visible, breakpoints suggested by aspect ratio>',
|
|
100
|
+
typography: '<derive from image: read visible heading/body weights and sizes>',
|
|
101
|
+
color: '<derive from image: enumerate distinct colours; record hex values>',
|
|
102
|
+
spacing: '<derive from image: rough px spacing scale between visible elements>',
|
|
103
|
+
// v1.5.0 audit-LOW-design-#16: motion / interactions hints. Static
|
|
104
|
+
// images cannot reveal motion design, but the stub still ships an
|
|
105
|
+
// empty `interactions:` block so the user fills it in rather than
|
|
106
|
+
// forgets it.
|
|
107
|
+
interactions: defaultInteractionsBlock('image: motion not visible — declare durations + easing tokens'),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
error: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse a `--from-figma <url>` request.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} figmaUrl
|
|
118
|
+
* @param {object} [opts]
|
|
119
|
+
* @param {string} [opts.token] Override FIGMA_TOKEN env
|
|
120
|
+
* @param {Function} [opts.httpsGetJson] Inject `(url, headers) => Promise<{ok, data, error}>`
|
|
121
|
+
* @returns {Promise<{ok: boolean, stub: object|null, error: string|null}>}
|
|
122
|
+
*/
|
|
123
|
+
export async function fromFigma(figmaUrl, opts = {}) {
|
|
124
|
+
if (typeof figmaUrl !== 'string' || figmaUrl.length === 0) {
|
|
125
|
+
return { ok: false, stub: null, error: 'no-url' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(figmaUrl);
|
|
131
|
+
} catch {
|
|
132
|
+
return { ok: false, stub: null, error: 'invalid-url' };
|
|
133
|
+
}
|
|
134
|
+
if (url.protocol !== 'https:') {
|
|
135
|
+
return { ok: false, stub: null, error: 'https-required' };
|
|
136
|
+
}
|
|
137
|
+
if (!/(^|\.)figma\.com$/i.test(url.hostname)) {
|
|
138
|
+
return { ok: false, stub: null, error: `not-figma-host: ${url.hostname}` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fileKey = extractFigmaFileKey(url);
|
|
142
|
+
if (!fileKey) {
|
|
143
|
+
return { ok: false, stub: null, error: 'cannot-extract-figma-file-key' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const token = opts.token || process.env.FIGMA_TOKEN || null;
|
|
147
|
+
const baseStub = {
|
|
148
|
+
source: {
|
|
149
|
+
kind: 'figma',
|
|
150
|
+
url: figmaUrl,
|
|
151
|
+
fileKey,
|
|
152
|
+
name: null,
|
|
153
|
+
lastModified: null,
|
|
154
|
+
},
|
|
155
|
+
advisory: null,
|
|
156
|
+
uiSpecHints: {
|
|
157
|
+
layout: '<derive from Figma frames: list frame names as surfaces>',
|
|
158
|
+
typography: '<read text styles from Figma local-styles>',
|
|
159
|
+
color: '<read fill styles from Figma local-styles>',
|
|
160
|
+
spacing: '<read auto-layout gap/padding values from frames>',
|
|
161
|
+
// v1.5.0 audit-LOW-design-#16: motion / interactions hints.
|
|
162
|
+
// Figma prototypes carry transition + easing on connections — the
|
|
163
|
+
// hint here directs the user to extract them.
|
|
164
|
+
interactions: defaultInteractionsBlock('figma: read prototype transitions from connection settings + Smart Animate easing'),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!token) {
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
stub: { ...baseStub, advisory: 'FIGMA_TOKEN unset — metadata fetch skipped; URL stored for reference only' },
|
|
172
|
+
error: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const fetcher = typeof opts.httpsGetJson === 'function' ? opts.httpsGetJson : defaultHttpsGetJson;
|
|
177
|
+
const apiUrl = `https://api.figma.com/v1/files/${encodeURIComponent(fileKey)}?depth=1`;
|
|
178
|
+
let res;
|
|
179
|
+
try {
|
|
180
|
+
res = await fetcher(apiUrl, { 'X-Figma-Token': token });
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
stub: { ...baseStub, advisory: `figma-api-error: ${e.code || e.message}` },
|
|
185
|
+
error: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!res || !res.ok || !res.data) {
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
stub: { ...baseStub, advisory: `figma-api-degraded: ${res && res.error ? res.error : 'unknown'}` },
|
|
192
|
+
error: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
stub: {
|
|
199
|
+
...baseStub,
|
|
200
|
+
source: {
|
|
201
|
+
...baseStub.source,
|
|
202
|
+
name: typeof res.data.name === 'string' ? res.data.name : null,
|
|
203
|
+
lastModified: typeof res.data.lastModified === 'string' ? res.data.lastModified : null,
|
|
204
|
+
},
|
|
205
|
+
advisory: 'metadata fetched — review styles + frames in UI-SPEC pillars',
|
|
206
|
+
},
|
|
207
|
+
error: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse argv (without the leading `node script.js`) looking for --from-image
|
|
213
|
+
* and --from-figma flags.
|
|
214
|
+
*
|
|
215
|
+
* @param {string[]} argv
|
|
216
|
+
* @returns {{fromImage: string|null, fromFigma: string|null, rest: string[]}}
|
|
217
|
+
*/
|
|
218
|
+
export function parseIntakeFlags(argv) {
|
|
219
|
+
const rest = [];
|
|
220
|
+
let fromImageVal = null;
|
|
221
|
+
let fromFigmaVal = null;
|
|
222
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
223
|
+
const a = argv[i];
|
|
224
|
+
if (a === '--from-image') {
|
|
225
|
+
fromImageVal = argv[i + 1] || null;
|
|
226
|
+
i += 1;
|
|
227
|
+
} else if (a.startsWith('--from-image=')) {
|
|
228
|
+
fromImageVal = a.slice('--from-image='.length);
|
|
229
|
+
} else if (a === '--from-figma') {
|
|
230
|
+
fromFigmaVal = argv[i + 1] || null;
|
|
231
|
+
i += 1;
|
|
232
|
+
} else if (a.startsWith('--from-figma=')) {
|
|
233
|
+
fromFigmaVal = a.slice('--from-figma='.length);
|
|
234
|
+
} else {
|
|
235
|
+
rest.push(a);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { fromImage: fromImageVal, fromFigma: fromFigmaVal, rest };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Public helpers
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* v1.5.0 audit-LOW-design-#16: canonical interactions block for the
|
|
247
|
+
* UI-SPEC schema.
|
|
248
|
+
*
|
|
249
|
+
* The 7th UI-SPEC pillar (interaction & motion) covers transition
|
|
250
|
+
* durations, easing tokens, and view-transitions usage. Prior to v1.5.0
|
|
251
|
+
* these were tacit; now every UI-SPEC stub carries an explicit
|
|
252
|
+
* `interactions:` block the user can fill in, and the ui-auditor's
|
|
253
|
+
* Interaction pillar checks that source code values match the declared
|
|
254
|
+
* tokens.
|
|
255
|
+
*
|
|
256
|
+
* Returns a structured object with sensible empty-string defaults so the
|
|
257
|
+
* downstream renderer can serialise it directly into YAML/markdown.
|
|
258
|
+
* `hint` is an optional one-liner explaining how to derive the values
|
|
259
|
+
* from the current intake source (image vs figma vs blank).
|
|
260
|
+
*/
|
|
261
|
+
export function defaultInteractionsBlock(hint = null) {
|
|
262
|
+
return {
|
|
263
|
+
hint: hint || '<fill from spec source — see UI-SPEC §interactions>',
|
|
264
|
+
transitions: {
|
|
265
|
+
// Spec format: duration tokens (e.g. {fast: '120ms', base: '200ms'})
|
|
266
|
+
durations: {
|
|
267
|
+
fast: '',
|
|
268
|
+
base: '',
|
|
269
|
+
slow: '',
|
|
270
|
+
},
|
|
271
|
+
// Spec format: easing tokens (cubic-bezier or named timing function)
|
|
272
|
+
easings: {
|
|
273
|
+
standard: '',
|
|
274
|
+
decelerate: '',
|
|
275
|
+
accelerate: '',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
// View Transitions API usage. Whitelist surfaces; "all" prohibited
|
|
279
|
+
// unless explicitly justified in the rationale field.
|
|
280
|
+
view_transitions: {
|
|
281
|
+
enabled: false,
|
|
282
|
+
surfaces: [],
|
|
283
|
+
rationale: '',
|
|
284
|
+
},
|
|
285
|
+
// Reduced-motion fallback policy. Every transition above MUST collapse
|
|
286
|
+
// to no-op or instant when prefers-reduced-motion: reduce is set.
|
|
287
|
+
reduced_motion: {
|
|
288
|
+
fallback: 'instant',
|
|
289
|
+
rationale: 'respect user OS-level motion preferences (WCAG 2.3.3)',
|
|
290
|
+
},
|
|
291
|
+
// Motion budget: the maximum simultaneous animated properties on
|
|
292
|
+
// screen at any time. Defaults to 3 to keep dashboards readable.
|
|
293
|
+
motion_budget: {
|
|
294
|
+
max_concurrent: 3,
|
|
295
|
+
excluded_surfaces: [],
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Internals
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
function parseDimensions(buf, ext) {
|
|
305
|
+
if (!buf || buf.length < 24) return null;
|
|
306
|
+
|
|
307
|
+
if (ext === '.png') {
|
|
308
|
+
// PNG signature: 89 50 4E 47 0D 0A 1A 0A, then IHDR at offset 16:
|
|
309
|
+
// width (4 bytes BE), height (4 bytes BE)
|
|
310
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
311
|
+
const width = buf.readUInt32BE(16);
|
|
312
|
+
const height = buf.readUInt32BE(20);
|
|
313
|
+
return { width, height };
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (ext === '.jpg' || ext === '.jpeg') {
|
|
319
|
+
// JPEG: FF D8 ... SOFn marker (FF C0/C1/C2) gives height/width.
|
|
320
|
+
if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
|
|
321
|
+
let i = 2;
|
|
322
|
+
while (i + 9 < buf.length) {
|
|
323
|
+
if (buf[i] !== 0xff) {
|
|
324
|
+
i += 1;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const marker = buf[i + 1];
|
|
328
|
+
// SOF0/SOF1/SOF2 markers
|
|
329
|
+
if (marker >= 0xc0 && marker <= 0xc3) {
|
|
330
|
+
const height = buf.readUInt16BE(i + 5);
|
|
331
|
+
const width = buf.readUInt16BE(i + 7);
|
|
332
|
+
return { width, height };
|
|
333
|
+
}
|
|
334
|
+
// Skip segment.
|
|
335
|
+
const segLen = buf.readUInt16BE(i + 2);
|
|
336
|
+
i += 2 + segLen;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null; // .webp / .gif intentionally unparsed -- not fatal.
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function extractFigmaFileKey(url) {
|
|
345
|
+
// Supports /file/<key>/... and /design/<key>/...
|
|
346
|
+
const m = url.pathname.match(/\/(?:file|design)\/([A-Za-z0-9]+)(?:\/|$)/);
|
|
347
|
+
if (m) return m[1];
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function defaultHttpsGetJson(url, headers = {}) {
|
|
352
|
+
return new Promise((resolve) => {
|
|
353
|
+
// Lazy-load https so the module stays import-cheap.
|
|
354
|
+
import('node:https')
|
|
355
|
+
.then(({ default: https }) => {
|
|
356
|
+
const req = https.get(url, { headers }, (res) => {
|
|
357
|
+
let body = '';
|
|
358
|
+
res.setEncoding('utf8');
|
|
359
|
+
res.on('data', (chunk) => {
|
|
360
|
+
body += chunk;
|
|
361
|
+
});
|
|
362
|
+
res.on('end', () => {
|
|
363
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
364
|
+
try {
|
|
365
|
+
resolve({ ok: true, data: JSON.parse(body), error: null });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
resolve({ ok: false, data: null, error: `json-parse: ${e.message}` });
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
resolve({ ok: false, data: null, error: `http-${res.statusCode || 0}` });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
req.setTimeout(8000, () => {
|
|
375
|
+
req.destroy(new Error('timeout'));
|
|
376
|
+
});
|
|
377
|
+
req.on('error', (e) => resolve({ ok: false, data: null, error: e.code || e.message }));
|
|
378
|
+
})
|
|
379
|
+
.catch((e) => resolve({ ok: false, data: null, error: e.code || e.message }));
|
|
380
|
+
});
|
|
381
|
+
}
|