@worca/app 1.3.0 → 1.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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/shared/graph/script-icons.mjs
|
|
2
|
+
// The twenty icons a script picks from (script-wizard plan S9), as the SAME SVG
|
|
3
|
+
// fragments a sidecar stores in `icon` — so the canvas, the manifest, the run
|
|
4
|
+
// monitor and a plugin export never learn a second shape. Stroke icons on a
|
|
5
|
+
// 24×24 box; no fill or stroke attribute (the tile and the canvas set the
|
|
6
|
+
// colour). Every fragment passes manifest.mjs's sanitizeIcon unchanged (pinned).
|
|
7
|
+
// Names are plain words that are NOT DOM globals: test/shared-graph-purity.test.mjs
|
|
8
|
+
// scans this file's code for `document` / `window` / `navigator` — so the page icon is `page`.
|
|
9
|
+
export const SCRIPT_ICONS = Object.freeze([
|
|
10
|
+
{ name: 'terminal', svg: '<path d="M4 17l6-5-6-5"/><path d="M12 19h8"/>' },
|
|
11
|
+
{ name: 'code', svg: '<path d="M8 4l-4 8 4 8"/><path d="M16 4l4 8-4 8"/>' },
|
|
12
|
+
{ name: 'flask', svg: '<path d="M9 3h6"/><path d="M10 3v6L4.5 19a1 1 0 0 0 .9 1.5h13.2a1 1 0 0 0 .9-1.5L14 9V3"/><path d="M7 15h10"/>' },
|
|
13
|
+
{ name: 'branch', svg: '<circle cx="6" cy="6" r="2"/><circle cx="6" cy="18" r="2"/><circle cx="18" cy="8" r="2"/><path d="M6 8v8"/><path d="M18 10a6 6 0 0 1-6 6h-2"/>' },
|
|
14
|
+
{ name: 'page', svg: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h6"/>' },
|
|
15
|
+
{ name: 'funnel', svg: '<path d="M4 5h16l-6 7v6l-4 2v-8z"/>' },
|
|
16
|
+
{ name: 'bolt', svg: '<path d="M13 3L5 14h6l-1 7 8-11h-6z"/>' },
|
|
17
|
+
{ name: 'globe', svg: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18z"/>' },
|
|
18
|
+
{ name: 'gear', svg: '<circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="7"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.5 1.5M16.9 16.9l1.5 1.5M5.6 18.4l1.5-1.5M16.9 7.1l1.5-1.5"/>' },
|
|
19
|
+
{ name: 'shield', svg: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/>' },
|
|
20
|
+
{ name: 'database', svg: '<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>' },
|
|
21
|
+
{ name: 'box', svg: '<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9z"/><path d="M4 7.5l8 4.5 8-4.5M12 12v9"/>' },
|
|
22
|
+
{ name: 'clock', svg: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>' },
|
|
23
|
+
{ name: 'bug', svg: '<rect x="8" y="8" width="8" height="11" rx="4"/><path d="M12 8V5M4 13h4M16 13h4M5 19l3-2.5M19 19l-3-2.5M9 5l-1-2M15 5l1-2"/>' },
|
|
24
|
+
{ name: 'chart', svg: '<path d="M5 20v-8M11 20V5M17 20v-5M3 20h18"/>' },
|
|
25
|
+
{ name: 'lock', svg: '<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>' },
|
|
26
|
+
{ name: 'cloud', svg: '<path d="M18 10h-1.3A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>' },
|
|
27
|
+
{ name: 'search', svg: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.3-4.3"/>' },
|
|
28
|
+
{ name: 'mail', svg: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>' },
|
|
29
|
+
{ name: 'tag', svg: '<path d="M3 12V4h8l9 9-8 8z"/><circle cx="7.5" cy="8.5" r="1.2"/>' },
|
|
30
|
+
].map(Object.freeze));
|
|
31
|
+
|
|
32
|
+
/** The glyph a script card wears when its sidecar ships no icon: an ƒ. (Moved from
|
|
33
|
+
* ui/public/graph/view.mjs, which re-exports it.) */
|
|
34
|
+
export const SCRIPT_GLYPH = '<path d="M15 4h-1.2a2.8 2.8 0 0 0-2.8 2.8V9H8M11 9v6.2A2.8 2.8 0 0 1 8.2 18H7" stroke-linecap="round" stroke-linejoin="round"></path>';
|
|
35
|
+
|
|
36
|
+
const byName = new Map(SCRIPT_ICONS.map((i) => [i.name, i.svg]));
|
|
37
|
+
const bySvg = new Map(SCRIPT_ICONS.map((i) => [i.svg, i.name]));
|
|
38
|
+
|
|
39
|
+
/** The set's name for a stored fragment, '' for anything else (a hand-written icon keeps rendering, no tile lights). */
|
|
40
|
+
export function iconNameOf(svg) {
|
|
41
|
+
return bySvg.get(String(svg || '').trim()) || '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function iconSvgOf(name) {
|
|
45
|
+
return byName.get(String(name || '')) || '';
|
|
46
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// src/shared/graph/script-infer.mjs
|
|
2
|
+
// The interface a script DECLARES BY READING IT (script-wizard plan S3–S7): the
|
|
3
|
+
// port and param ids a program mentions, a proposed type for each, a default
|
|
4
|
+
// where the code spells one, and whether it returns a verdict. Pure — the page
|
|
5
|
+
// runs it on every keystroke, the tests feed it strings. The sidecar on disk
|
|
6
|
+
// stays the source of truth: mergeInterface layers the saved sidecar and the
|
|
7
|
+
// user's chip overrides OVER the proposal, and interfaceToMeta turns the rows
|
|
8
|
+
// back into the sidecar shape script-meta.mjs validates.
|
|
9
|
+
import { PORT_ID_RE } from './constants.mjs';
|
|
10
|
+
|
|
11
|
+
export const PORT_TYPES = Object.freeze(['md', 'json', 'void']);
|
|
12
|
+
export const WHEN_CYCLE = Object.freeze(['always', 'clean', 'blocking']);
|
|
13
|
+
export const WHEN_LABEL = Object.freeze({ always: 'always', clean: 'on pass', blocking: 'on fail' });
|
|
14
|
+
export const INPUT_MODES = Object.freeze(['optional', 'required', 'loop']);
|
|
15
|
+
/** The param types a chip may cycle through. `enum` and `code` need the sidecar (options, language). */
|
|
16
|
+
export const CHIP_PARAM_TYPES = Object.freeze(['string', 'number', 'boolean', 'command']);
|
|
17
|
+
export const FIXED_PARAM_TYPES = Object.freeze(['enum', 'code']);
|
|
18
|
+
/** Characters read after a token for its type and default. */
|
|
19
|
+
export const INFER_WINDOW = 240;
|
|
20
|
+
|
|
21
|
+
const KEY_MAX = 64;
|
|
22
|
+
|
|
23
|
+
/** 'Run tests' -> 'runTests'. Lower camel, a letter first, SCRIPT_KEY_RE-sized. */
|
|
24
|
+
export function keyFromName(name) {
|
|
25
|
+
const words = String(name || '').replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean);
|
|
26
|
+
if (!words.length) return '';
|
|
27
|
+
let out = words.map((w, i) => { const l = w.toLowerCase(); return i === 0 ? l : l[0].toUpperCase() + l.slice(1); }).join('');
|
|
28
|
+
if (!/^[A-Za-z]/.test(out)) out = `s${out}`;
|
|
29
|
+
return out.slice(0, KEY_MAX);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The runner's env naming (script-runner.mjs): `planMd` <-> `PLAN_MD`. A digit after an
|
|
33
|
+
* underscore joins bare (`OUT_2` -> `out2`): PORT_ID_RE admits no underscore. */
|
|
34
|
+
export const upperSnake = (id) => String(id).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
35
|
+
export const lowerCamel = (env) => String(env).toLowerCase().replace(/_+([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
36
|
+
|
|
37
|
+
/** Comments are not reads. Crude on purpose (no string parser): a `//` after a colon is a
|
|
38
|
+
* URL, a `#` after a quote, a `$` or a `{` is text or `${#V}`. */
|
|
39
|
+
export function stripComments(source, runtime) {
|
|
40
|
+
const s = String(source || '');
|
|
41
|
+
if (runtime === 'node') return s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1');
|
|
42
|
+
return s.replace(/(^|[^'"\\${])#[^\n]*/g, '$1');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `inputs.x` · `inputs?.x` · `inputs['x']` · `inputs["x"]` · `inputs.get('x')` — and the same
|
|
46
|
+
// three shapes behind `api.` (the `\b` before the word admits the dot). The destructuring
|
|
47
|
+
// header `{ inputs, outputs, params }` has no accessor after the word, so it is not a read.
|
|
48
|
+
const idRe = (word) => new RegExp(`\\b${word}\\.get\\(\\s*(?<q1>['"])(?<a>[A-Za-z_]\\w*)\\k<q1>|\\b${word}(?:\\?\\.|\\.)(?<b>[A-Za-z_]\\w*)|\\b${word}\\s*\\[\\s*(?<q2>['"])(?<c>[A-Za-z_]\\w*)\\k<q2>\\s*\\]`, 'g');
|
|
49
|
+
// `$WORCA_IN_X` · `${WORCA_IN_X}` · `${WORCA_PARAM_X:-default}` · `%WORCA_IN_X%` (cmd.exe).
|
|
50
|
+
const envRe = (prefix) => new RegExp(`(?:\\$\\{?|%)WORCA_${prefix}_(?<a>[A-Z][A-Z0-9_]*)`, 'g');
|
|
51
|
+
|
|
52
|
+
function scan(text, re, map = (x) => x) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const m of text.matchAll(re)) {
|
|
55
|
+
const id = map(m.groups.a ?? m.groups.b ?? m.groups.c ?? '');
|
|
56
|
+
if (PORT_ID_RE.test(id) && !out.includes(id)) out.push(id);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The FIRST read of `<word>.<id>` (any of the three accessor shapes, behind `api.` or not;
|
|
63
|
+
* the env var for shell): 24 characters before it — with a trailing `api.` cut off, so
|
|
64
|
+
* `int(api.params.limit` still ends in `int(` — and the rest of its STATEMENT after it
|
|
65
|
+
* (to the next `;` or blank line for node, the end of the line otherwise, at most
|
|
66
|
+
* INFER_WINDOW characters). The window stops at the statement so the JSON write on
|
|
67
|
+
* the NEXT line can never colour this port json.
|
|
68
|
+
*/
|
|
69
|
+
function readSite(text, word, id, runtime) {
|
|
70
|
+
const re = runtime === 'shell'
|
|
71
|
+
? new RegExp(`(?:\\$\\{?|%)WORCA_${word}_${upperSnake(id)}\\b`)
|
|
72
|
+
: new RegExp(`\\b${word}(?:(?:\\?\\.|\\.)${id}\\b|\\s*\\[\\s*['"]${id}['"]\\s*\\]|\\.get\\(\\s*['"]${id}['"]\\s*\\))`);
|
|
73
|
+
const m = re.exec(text);
|
|
74
|
+
if (!m) return { before: '', after: '' };
|
|
75
|
+
const end = m.index + m[0].length;
|
|
76
|
+
const slice = text.slice(end, end + INFER_WINDOW);
|
|
77
|
+
const stop = runtime === 'node' ? slice.search(/;|\n\s*\n/) : slice.indexOf('\n');
|
|
78
|
+
return {
|
|
79
|
+
before: text.slice(Math.max(0, m.index - 24), m.index).replace(/\b\w+\.$/, ''),
|
|
80
|
+
after: stop < 0 ? slice : slice.slice(0, stop),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `json` when the write in the token's statement is JSON, else `md`. A chip overrides it. */
|
|
85
|
+
function outputType(text, id, runtime) {
|
|
86
|
+
const { after } = readSite(text, runtime === 'shell' ? 'OUT' : 'outputs', id, runtime);
|
|
87
|
+
return /JSON\.stringify|json\.dumps?\s*\(|\bjq\b|\.json\b/.test(after) ? 'json' : 'md';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Type from the read site, default from the fallback spelled beside it (S7). */
|
|
91
|
+
function paramInfo(text, id, runtime) {
|
|
92
|
+
const { before, after } = readSite(text, runtime === 'shell' ? 'PARAM' : 'params', id, runtime);
|
|
93
|
+
let type = 'string';
|
|
94
|
+
let dflt;
|
|
95
|
+
if (runtime === 'shell') {
|
|
96
|
+
const m = after.match(/^:-([^}]*)\}/);
|
|
97
|
+
if (m) dflt = m[1];
|
|
98
|
+
} else {
|
|
99
|
+
const m = after.match(/^\s*(?:\?\?|\|\||\bor\b)\s*(?:(['"`])((?:(?!\1).)*)\1|(-?\d+(?:\.\d+)?)|(true|false|True|False))/);
|
|
100
|
+
if (m) {
|
|
101
|
+
if (m[2] != null) dflt = m[2];
|
|
102
|
+
else if (m[3] != null) { dflt = Number(m[3]); type = 'number'; }
|
|
103
|
+
else { dflt = /^t/i.test(m[4]); type = 'boolean'; }
|
|
104
|
+
}
|
|
105
|
+
if (/(Number|parseInt|parseFloat|int|float)\(\s*$/.test(before)) type = 'number';
|
|
106
|
+
if (/(Boolean|bool)\(\s*$/.test(before) || /^\s*(===?|!==?|is(?: not)?)\s*(true|false|True|False)\b/.test(after)) type = 'boolean';
|
|
107
|
+
}
|
|
108
|
+
const out = { id, type };
|
|
109
|
+
const typed = typedDefault(type, dflt);
|
|
110
|
+
if (typed !== undefined) out.default = typed;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The proposal (S3): ids in code order, a type and a default each, the verdict. */
|
|
115
|
+
export function inferInterface(source, runtime) {
|
|
116
|
+
const text = stripComments(source, runtime);
|
|
117
|
+
let ins; let outs; let prm; let verdict;
|
|
118
|
+
if (runtime === 'shell') {
|
|
119
|
+
ins = scan(text, envRe('IN'), lowerCamel);
|
|
120
|
+
outs = scan(text, envRe('OUT'), lowerCamel);
|
|
121
|
+
prm = scan(text, envRe('PARAM'), lowerCamel);
|
|
122
|
+
verdict = /WORCA_VERDICT\b/.test(text);
|
|
123
|
+
} else {
|
|
124
|
+
ins = scan(text, idRe('inputs'));
|
|
125
|
+
outs = scan(text, idRe('outputs'));
|
|
126
|
+
prm = scan(text, idRe('params'));
|
|
127
|
+
// `verdict:` in a return / a dict, `verdictPath`, `verdict =`, or the SHORTHAND `return { summary, verdict }`
|
|
128
|
+
// (`{ ...base, verdict }`): a verdict a helper built is still returned.
|
|
129
|
+
verdict = /(['"]?)\bverdict\1\s*:|\bverdictPath\b|\bverdict\s*=[^=]|\breturn\s*\{[^;]{0,240}?\bverdict\s*[,}]/.test(text);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
inputs: ins.map((id) => ({ id, type: 'md' })),
|
|
133
|
+
outputs: outs.map((id) => ({ id, type: outputType(text, id, runtime) })),
|
|
134
|
+
params: prm.map((id) => paramInfo(text, id, runtime)),
|
|
135
|
+
verdict,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const paramRows = (meta) => ((meta && Array.isArray(meta.params)) ? meta.params : []).map((p) => {
|
|
140
|
+
const row = { id: p.id, type: p.type, fixed: FIXED_PARAM_TYPES.includes(p.type) };
|
|
141
|
+
if (p.default !== undefined) row.default = p.default;
|
|
142
|
+
if (p.required !== undefined) row.required = !!p.required;
|
|
143
|
+
if (p.label) row.label = p.label;
|
|
144
|
+
if (p.description) row.description = p.description;
|
|
145
|
+
if (Array.isArray(p.options)) row.options = [...p.options];
|
|
146
|
+
if (p.language) row.language = p.language;
|
|
147
|
+
return row;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** The sidecar as rows. A `ports: "config"` script has no port rows here (S8). */
|
|
151
|
+
export function savedRows(meta) {
|
|
152
|
+
if (!meta || meta.ports === 'config') return { inputs: [], outputs: [], params: paramRows(meta) };
|
|
153
|
+
return {
|
|
154
|
+
inputs: (meta.inputs || []).filter((p) => p && p.id && !p.synthetic && p.id !== 'await')
|
|
155
|
+
.map((p) => ({ id: p.id, type: p.type || 'md', mode: p.loop ? 'loop' : (p.required === false ? 'optional' : 'required') })),
|
|
156
|
+
outputs: (meta.outputs || []).filter((p) => p && p.id)
|
|
157
|
+
.map((p) => ({ id: p.id, type: p.type || 'md', when: p.when || 'always', filename: p.filename || '' })),
|
|
158
|
+
params: paramRows(meta),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** An untouched attribute follows the code: `auto` remembers what inference last said. */
|
|
163
|
+
function follow(prev, inf, fields) {
|
|
164
|
+
const next = { ...prev };
|
|
165
|
+
for (const f of fields) if (prev.auto && Object.is(prev[f], prev.auto[f])) next[f] = inf[f];
|
|
166
|
+
next.auto = { ...inf };
|
|
167
|
+
return next;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The rows the panel shows (S3): code ids first, in code order, then the saved
|
|
172
|
+
* ids the code no longer mentions (`inCode: false`) unless removed. Attributes:
|
|
173
|
+
* the previous row (the user's chips) ⊕ the saved sidecar ⊕ inference.
|
|
174
|
+
*/
|
|
175
|
+
export function mergeInterface({ inferred, saved = null, rows = null, removed = new Set() } = {}) {
|
|
176
|
+
const inf = inferred || { inputs: [], outputs: [], params: [] };
|
|
177
|
+
const prev = rows || { inputs: [], outputs: [], params: [] };
|
|
178
|
+
const sv = savedRows(saved);
|
|
179
|
+
const merge = (side, fromCode, fresh, followed) => {
|
|
180
|
+
const out = [];
|
|
181
|
+
const prevBy = new Map((prev[side] || []).map((r) => [r.id, r]));
|
|
182
|
+
const savedBy = new Map(sv[side].map((r) => [r.id, r]));
|
|
183
|
+
for (const p of fromCode) {
|
|
184
|
+
const was = prevBy.get(p.id);
|
|
185
|
+
const auto = { ...p };
|
|
186
|
+
delete auto.id;
|
|
187
|
+
let row;
|
|
188
|
+
if (was) row = follow(was, auto, followed);
|
|
189
|
+
else if (savedBy.has(p.id)) row = { ...savedBy.get(p.id), auto };
|
|
190
|
+
else row = { ...fresh(p), auto };
|
|
191
|
+
out.push({ ...row, id: p.id, inCode: true });
|
|
192
|
+
}
|
|
193
|
+
for (const s of sv[side]) {
|
|
194
|
+
if (out.some((r) => r.id === s.id) || removed.has(`${side}:${s.id}`)) continue;
|
|
195
|
+
out.push({ ...(prevBy.get(s.id) || s), id: s.id, inCode: false });
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
inputs: merge('inputs', inf.inputs, (p) => ({ type: p.type || 'md', mode: 'optional' }), ['type']),
|
|
201
|
+
outputs: merge('outputs', inf.outputs, (p) => ({ type: p.type || 'md', when: 'always', filename: '' }), ['type']),
|
|
202
|
+
params: merge('params', inf.params, (p) => ({ type: p.type || 'string', ...(p.default !== undefined ? { default: p.default } : {}), fixed: false }), ['type', 'default']),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function cycleValue(list, current) {
|
|
207
|
+
const i = list.indexOf(current);
|
|
208
|
+
return list[(i + 1) % list.length];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A default in its declared type — readParams refuses `"40"` on a number param. */
|
|
212
|
+
export function typedDefault(type, raw) {
|
|
213
|
+
if (raw === undefined || raw === null) return undefined;
|
|
214
|
+
if (typeof raw === 'number' && type === 'number') return Number.isFinite(raw) ? raw : undefined;
|
|
215
|
+
if (typeof raw === 'boolean' && type === 'boolean') return raw;
|
|
216
|
+
const s = String(raw);
|
|
217
|
+
if (s.trim() === '') return undefined;
|
|
218
|
+
if (type === 'number') { const n = Number(s); return Number.isFinite(n) ? n : undefined; }
|
|
219
|
+
if (type === 'boolean') return s === 'true' ? true : (s === 'false' ? false : undefined);
|
|
220
|
+
return s;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Rows -> the sidecar's port and param objects (S4, S5, S6, S7). `verdict` is
|
|
225
|
+
* what the code declares (node/python) or `$WORCA_VERDICT`; `routing` is the
|
|
226
|
+
* shell toggle. Filenames are minted from the key unless the row carries one.
|
|
227
|
+
*/
|
|
228
|
+
export function interfaceToMeta(rows, { key = '', runtime = 'node', verdict = false, routing = false, verdictFilename = '' } = {}) {
|
|
229
|
+
const k = key || 'script';
|
|
230
|
+
const r = rows || { inputs: [], outputs: [], params: [] };
|
|
231
|
+
const hasVerdict = runtime === 'shell' ? (routing || verdict) : verdict;
|
|
232
|
+
const inputs = (r.inputs || []).map((row) => {
|
|
233
|
+
const p = { id: row.id, type: row.type || 'md', required: row.mode === 'required' };
|
|
234
|
+
if (row.mode === 'loop') p.loop = true;
|
|
235
|
+
return p;
|
|
236
|
+
});
|
|
237
|
+
const outputs = (r.outputs || []).map((row) => {
|
|
238
|
+
const type = row.type || 'md';
|
|
239
|
+
const p = { id: row.id, type, when: hasVerdict && WHEN_CYCLE.includes(row.when) ? row.when : 'always' };
|
|
240
|
+
if (type !== 'void') p.filename = row.filename || `${k}-${row.id}-cycle{cycle}.${type === 'json' ? 'json' : 'md'}`;
|
|
241
|
+
return p;
|
|
242
|
+
});
|
|
243
|
+
if (runtime === 'shell' && routing) {
|
|
244
|
+
if (!outputs.some((p) => p.id === 'pass')) outputs.push({ id: 'pass', type: 'void', when: 'clean' });
|
|
245
|
+
if (!outputs.some((p) => p.id === 'fail')) outputs.push({ id: 'fail', type: 'md', when: 'blocking', filename: `${k}-fail-cycle{cycle}.md` });
|
|
246
|
+
}
|
|
247
|
+
const params = (r.params || []).map((row) => {
|
|
248
|
+
const p = { id: row.id, type: row.type || 'string' };
|
|
249
|
+
const dflt = typedDefault(p.type, row.default);
|
|
250
|
+
if (dflt !== undefined) p.default = dflt;
|
|
251
|
+
p.required = row.required !== undefined ? !!row.required : dflt === undefined;
|
|
252
|
+
if (row.label) p.label = row.label;
|
|
253
|
+
if (row.description) p.description = row.description;
|
|
254
|
+
if (p.type === 'enum' && Array.isArray(row.options)) p.options = [...row.options];
|
|
255
|
+
if (p.type === 'code') p.language = row.language || 'js';
|
|
256
|
+
return p;
|
|
257
|
+
});
|
|
258
|
+
return { inputs, outputs, params, verdict: hasVerdict ? { filename: verdictFilename || `${k}-cycle{cycle}.json` } : null };
|
|
259
|
+
}
|