@taylorwong/ichartjs 2.0.12 → 2.0.14

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -2
  3. package/docs/agent/charting-scenario.md +3 -1
  4. package/docs/agent/coding-agent-integration.md +1 -1
  5. package/docs/agent/development/2.0-release-readiness.md +4 -0
  6. package/docs/agent/development/iteration-13.md +278 -0
  7. package/docs/agent/development/release-sop.md +1 -1
  8. package/docs/agent/development/roadmap.md +2 -1
  9. package/docs/agent/diagram-scenario.md +4 -0
  10. package/docs/agent/editing-contract.md +3 -0
  11. package/docs/agent/frontend-integration.md +1 -1
  12. package/docs/agent/project-scenario.md +3 -0
  13. package/docs/agent/runtime-contract.md +4 -0
  14. package/docs/agent/usage-scenarios.md +1 -1
  15. package/docs/agent/zh-CN/charting-scenario.md +3 -1
  16. package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
  17. package/docs/agent/zh-CN/diagram-scenario.md +17 -1
  18. package/docs/agent/zh-CN/editing-contract.md +2 -0
  19. package/docs/agent/zh-CN/frontend-integration.md +1 -1
  20. package/docs/agent/zh-CN/project-scenario.md +4 -2
  21. package/docs/agent/zh-CN/runtime-contract.md +3 -1
  22. package/docs/agent/zh-CN/usage-scenarios.md +1 -1
  23. package/docs/manifests/capabilities.json +1310 -32
  24. package/docs/manifests/commands.json +7 -7
  25. package/docs/manifests/schemas.json +182 -10
  26. package/package.json +24 -4
  27. package/skills/ichartjs/SKILL.md +1 -1
  28. package/src/capabilities.mjs +10 -36
  29. package/src/chart-lifecycle.mjs +31 -0
  30. package/src/charts.mjs +49 -19
  31. package/src/command.mjs +3 -2
  32. package/src/contract-registry.mjs +59 -0
  33. package/src/diagram.mjs +30 -0
  34. package/src/edit-controller.mjs +1 -1
  35. package/src/errors.mjs +31 -0
  36. package/src/index.mjs +142 -10
  37. package/src/layout.mjs +26 -1
  38. package/src/project.mjs +45 -16
  39. package/src/schema.mjs +1 -0
  40. package/src/spec.mjs +45 -3
  41. package/types/consumer-fixture.ts +35 -0
  42. package/types/index.d.ts +33 -3
@@ -2,8 +2,8 @@
2
2
  "version": "1.0",
3
3
  "source": "src/command.mjs#operationTypes",
4
4
  "commands": {
5
- "updateField": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node"], "previewRequired": true, "undoable": true },
6
- "updateRecord": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node"], "previewRequired": true, "undoable": true },
5
+ "updateField": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node", "mindmap-edge"], "previewRequired": true, "undoable": true },
6
+ "updateRecord": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node", "mindmap-edge"], "previewRequired": true, "undoable": true },
7
7
  "updateTask": { "models": ["project-task"], "previewRequired": true, "undoable": true },
8
8
  "shiftTask": { "models": ["project-task"], "required": ["taskId", "days"], "previewRequired": true, "undoable": true },
9
9
  "updateProgress": { "models": ["project-task"], "required": ["taskId", "progress"], "previewRequired": true, "undoable": true },
@@ -21,11 +21,11 @@
21
21
  "assignNodesToGroup": { "models": ["flow-node"], "required": ["nodeIds", "groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
22
22
  "duplicateGroup": { "models": ["flow-node", "flow-edge"], "required": ["groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
23
23
  "deleteGroup": { "models": ["flow-node", "flow-edge"], "required": ["groupId", "policy"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
24
- "updateEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["edgeId", "changes"], "previewRequired": true, "undoable": true },
25
- "removeEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["edgeId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
26
- "addEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["from", "to"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
24
+ "updateEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["edgeId", "changes"], "previewRequired": true, "undoable": true },
25
+ "removeEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["edgeId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
26
+ "addEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["from", "to"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
27
27
  "toggleGroupCollapse": { "models": ["flow-node"], "required": ["groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
28
- "duplicateSelection": { "models": ["flow-node", "flow-edge"], "required": ["nodeIds"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
29
- "pasteSelection": { "models": ["flow-node", "flow-edge"], "required": ["nodes"], "previewRequired": true, "confirmationRequired": true, "undoable": true }
28
+ "duplicateSelection": { "models": ["flow-node", "flow-edge", "mindmap-edge"], "required": ["nodeIds"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
29
+ "pasteSelection": { "models": ["flow-node", "flow-edge", "mindmap-edge"], "required": ["nodes"], "previewRequired": true, "confirmationRequired": true, "undoable": true }
30
30
  }
31
31
  }
@@ -2,15 +2,187 @@
2
2
  "version": "1.0",
3
3
  "source": "src/schema.mjs#getBusinessSchema",
4
4
  "models": {
5
- "project-task": { "key": "id", "required": ["id", "name", "start", "end"], "editable": ["name", "start", "end", "progress", "status", "dependencies"] },
6
- "timeline-event": { "key": "id", "required": ["id", "title", "date"], "editable": ["title", "date", "description", "status"] },
7
- "milestone": { "key": "id", "required": ["id", "title", "date"], "editable": ["title", "date", "status"] },
8
- "burndown-sample": { "key": "id", "required": ["id", "date", "remaining"], "editable": ["date", "remaining", "scopeChange"] },
9
- "flow-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "position", "size", "laneId", "description", "status"] },
10
- "flow-edge": { "key": "id", "required": ["id", "from", "to"], "editable": ["from", "to", "fromPort", "toPort", "label", "status", "routing", "curveTension", "waypoints"] },
11
- "swimlane": { "key": "id", "required": ["id", "label"], "editable": ["label"] },
12
- "architecture-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "layerId", "boundaryId", "position", "size", "description", "status"] },
13
- "architecture-edge": { "key": "id", "required": ["id", "from", "to"], "editable": ["from", "to", "label", "relation", "routing", "curveTension", "waypoints", "status"] },
14
- "mindmap-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "parentId", "position", "size", "branch", "description", "status"] }
5
+ "project-task": {
6
+ "key": "id",
7
+ "required": [
8
+ "id",
9
+ "name",
10
+ "start",
11
+ "end"
12
+ ],
13
+ "editable": [
14
+ "name",
15
+ "start",
16
+ "end",
17
+ "baselineStart",
18
+ "baselineEnd",
19
+ "actualStart",
20
+ "actualEnd",
21
+ "baselineDate",
22
+ "actualDate",
23
+ "progress",
24
+ "status",
25
+ "owner",
26
+ "resource",
27
+ "capacity",
28
+ "load",
29
+ "points",
30
+ "sprint",
31
+ "priority",
32
+ "labels",
33
+ "dependencies"
34
+ ]
35
+ },
36
+ "timeline-event": {
37
+ "key": "id",
38
+ "required": [
39
+ "id",
40
+ "title",
41
+ "date"
42
+ ],
43
+ "editable": [
44
+ "title",
45
+ "date",
46
+ "description",
47
+ "status"
48
+ ]
49
+ },
50
+ "milestone": {
51
+ "key": "id",
52
+ "required": [
53
+ "id",
54
+ "title",
55
+ "date"
56
+ ],
57
+ "editable": [
58
+ "title",
59
+ "date",
60
+ "status"
61
+ ]
62
+ },
63
+ "burndown-sample": {
64
+ "key": "id",
65
+ "required": [
66
+ "id",
67
+ "date",
68
+ "remaining"
69
+ ],
70
+ "editable": [
71
+ "date",
72
+ "remaining",
73
+ "scopeChange"
74
+ ]
75
+ },
76
+ "flow-node": {
77
+ "key": "id",
78
+ "required": [
79
+ "id",
80
+ "label"
81
+ ],
82
+ "editable": [
83
+ "label",
84
+ "position",
85
+ "size",
86
+ "laneId",
87
+ "description",
88
+ "status"
89
+ ]
90
+ },
91
+ "flow-edge": {
92
+ "key": "id",
93
+ "required": [
94
+ "id",
95
+ "from",
96
+ "to"
97
+ ],
98
+ "editable": [
99
+ "from",
100
+ "to",
101
+ "fromPort",
102
+ "toPort",
103
+ "label",
104
+ "status",
105
+ "routing",
106
+ "curveTension",
107
+ "waypoints"
108
+ ]
109
+ },
110
+ "swimlane": {
111
+ "key": "id",
112
+ "required": [
113
+ "id",
114
+ "label"
115
+ ],
116
+ "editable": [
117
+ "label"
118
+ ]
119
+ },
120
+ "architecture-node": {
121
+ "key": "id",
122
+ "required": [
123
+ "id",
124
+ "label"
125
+ ],
126
+ "editable": [
127
+ "label",
128
+ "layerId",
129
+ "boundaryId",
130
+ "position",
131
+ "size",
132
+ "description",
133
+ "status"
134
+ ]
135
+ },
136
+ "architecture-edge": {
137
+ "key": "id",
138
+ "required": [
139
+ "id",
140
+ "from",
141
+ "to"
142
+ ],
143
+ "editable": [
144
+ "from",
145
+ "to",
146
+ "label",
147
+ "relation",
148
+ "routing",
149
+ "curveTension",
150
+ "waypoints",
151
+ "status"
152
+ ]
153
+ },
154
+ "mindmap-edge": {
155
+ "key": "id",
156
+ "required": [
157
+ "id",
158
+ "from",
159
+ "to"
160
+ ],
161
+ "editable": [
162
+ "from",
163
+ "to",
164
+ "label",
165
+ "routing",
166
+ "curveTension",
167
+ "waypoints",
168
+ "status"
169
+ ]
170
+ },
171
+ "mindmap-node": {
172
+ "key": "id",
173
+ "required": [
174
+ "id",
175
+ "label"
176
+ ],
177
+ "editable": [
178
+ "label",
179
+ "parentId",
180
+ "position",
181
+ "size",
182
+ "branch",
183
+ "description",
184
+ "status"
185
+ ]
186
+ }
15
187
  }
16
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taylorwong/ichartjs",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "description": "Agent-first, renderer-independent charting and project visualization runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -35,21 +35,41 @@
35
35
  },
36
36
  "scripts": {
37
37
  "test": "node --test tests/*.test.mjs",
38
+ "test:browser": "node --test tests/browser/*.test.mjs",
38
39
  "check": "for file in src/*.mjs; do node --check \"$file\" || exit 1; done",
40
+ "contracts:generate": "node scripts/generate-contract-manifests.mjs",
41
+ "contracts:check": "node scripts/check-contracts.mjs",
42
+ "types:check": "node scripts/check-types.mjs",
43
+ "deps:check": "node scripts/check-cycles.mjs",
44
+ "docs:examples": "node examples/agent-workflow.mjs",
39
45
  "docs:check": "node scripts/check-agent-docs.mjs",
40
46
  "example:agent": "node examples/agent-workflow.mjs",
41
47
  "playground": "node scripts/serve-playground.mjs",
42
- "agent:check": "npm run docs:check && npm run check && npm test",
48
+ "agent:check": "npm run contracts:check && npm run types:check && npm run deps:check && npm run docs:check && npm run docs:examples && npm run check && npm test",
43
49
  "rc:check": "npm test && npm run check && git diff --check"
44
50
  },
51
+ "devDependencies": {
52
+ "playwright-core": "^1.63.0",
53
+ "typescript": "^5.9.3"
54
+ },
45
55
  "repository": {
46
56
  "type": "git",
47
57
  "url": "git+https://github.com/wanghetommy/ichartjs.git"
48
58
  },
49
59
  "homepage": "https://github.com/wanghetommy/ichartjs#readme",
50
60
  "bugs": "https://github.com/wanghetommy/ichartjs/issues",
51
- "keywords": ["chart", "visualization", "agent", "svg", "canvas", "gantt", "diagram"],
52
- "engines": { "node": ">=18" },
61
+ "keywords": [
62
+ "chart",
63
+ "visualization",
64
+ "agent",
65
+ "svg",
66
+ "canvas",
67
+ "gantt",
68
+ "diagram"
69
+ ],
70
+ "engines": {
71
+ "node": ">=18"
72
+ },
53
73
  "author": "taylorwong <wanghetommy@gmail.com>",
54
74
  "license": "Apache-2.0"
55
75
  }
@@ -21,7 +21,7 @@ Recommended installation:
21
21
  npx skills add wanghetommy/ichartjs --skill ichartjs
22
22
  ```
23
23
 
24
- Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.11/skills/ichartjs` when reproducibility matters. WorkBuddy can import the same directory through its Skill interface; do not assume a `--agent workbuddy` adapter unless the installed CLI declares it.
24
+ Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.14/skills/ichartjs` when reproducibility matters. WorkBuddy can import the same directory through its Skill interface; do not assume a `--agent workbuddy` adapter unless the installed CLI declares it.
25
25
 
26
26
  The Skill is a workflow adapter, not the chart runtime. If the current JavaScript or TypeScript project does not already depend on iChart.js, install the matching runtime from GitHub:
27
27
 
@@ -4,44 +4,13 @@
4
4
  import { inspectData } from './data.mjs';
5
5
  import { planStyle, styleCapabilities } from './theme.mjs';
6
6
  import { defaultPreferences, preferenceDensities, preferenceMotions, preferenceTriStates } from './preferences.mjs';
7
+ import { businessModels, chartProfiles, chartTypes, commandTypes, contractVersion, diagramEdgeModels, diagramOperations, exportTypes, interactionDefaults, renderers } from './contract-registry.mjs';
7
8
 
8
- export const chartTypes = ['line', 'area', 'bar', 'column', 'pie', 'scatter', 'funnel', 'gauge', 'heatmap', 'radar', 'gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'];
9
+ export { chartProfiles, chartTypes };
9
10
 
10
11
  const cartesian = ['line', 'area', 'bar', 'column', 'scatter'];
11
12
  const project = ['gantt', 'timeline', 'milestone', 'burndown'];
12
13
  const diagrams = ['flow', 'swimlane', 'architecture', 'mindmap'];
13
- const status = (supported, notApplicable = []) => Object.fromEntries(supported.map(name => [name, 'supported']).concat(notApplicable.map(name => [name, 'not-applicable'])));
14
- const commonPresentation = ['title', 'subtitle', 'theme', 'responsive', 'empty-state', 'invalid-data-state', 'export', 'branding'];
15
-
16
- const definitions = {
17
- line: { family: 'cartesian', intents: ['trend', 'time-series'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'dual-axis']) },
18
- area: { family: 'cartesian', intents: ['trend', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
19
- bar: { family: 'cartesian', intents: ['comparison', 'ranking'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
20
- column: { family: 'cartesian', intents: ['comparison', 'distribution', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked', 'mixed-line-column', 'dual-axis', 'bin']) },
21
- pie: { family: 'part-to-whole', intents: ['part-to-whole'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'donut', 'zero-total'], ['axes', 'grid', 'zoom', 'pan']) },
22
- scatter: { family: 'cartesian', intents: ['relationship', 'correlation'], required: ['measure', 'measure'], optional: ['dimension'], interactions: ['hover', 'tooltip', 'selection', 'brush', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'labels', 'formatting'], ['stacked']) },
23
- funnel: { family: 'stage', intents: ['funnel', 'conversion'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting'], ['axes', 'grid', 'zoom', 'pan']) },
24
- gauge: { family: 'indicator', intents: ['progress', 'single-value'], required: ['measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'domain'], ['axes', 'grid', 'legend', 'zoom', 'pan']) },
25
- heatmap: { family: 'matrix', intents: ['matrix', 'correlation-grid'], required: ['dimension', 'dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'color-scale', 'missing-values'], ['zoom', 'pan']) },
26
- radar: { family: 'radial', intents: ['multidimensional', 'profile'], required: ['three-or-more-measures'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'multi-series', 'indicator-domains', 'mixed-unit-warning'], ['axes', 'grid', 'zoom', 'pan']) },
27
- gantt: { family: 'project', intents: ['schedule', 'variance'], required: ['id', 'start', 'end'], optional: ['dependencies', 'progress', 'baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard', 'linked-filter', 'linked-selection'], features: status([...commonPresentation, 'labels', 'formatting', 'critical-path', 'slack', 'baseline-actual-variance', 'working-calendar', 'dependencies']) },
28
- timeline: { family: 'project', intents: ['timeline', 'milestone'], required: ['date', 'label'], optional: ['status'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting']) },
29
- milestone: { family: 'project', intents: ['milestone'], required: ['date', 'label'], optional: ['baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'baseline-actual-variance']) },
30
- burndown: { family: 'project', intents: ['progress', 'release'], required: ['date', 'remaining'], optional: ['ideal', 'scopeChange'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'scope-change', 'release-forecast']) },
31
- flow: { family: 'diagram', intents: ['workflow'], required: ['nodes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
32
- swimlane: { family: 'diagram', intents: ['responsibility'], required: ['nodes', 'lanes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'lanes', 'history'], ['axes', 'grid', 'legend']) },
33
- architecture: { family: 'diagram', intents: ['architecture', 'business-architecture', 'data-architecture', 'technical-architecture'], required: ['nodes'], optional: ['edges', 'layers', 'boundaries', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'layers', 'boundaries', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
34
- mindmap: { family: 'diagram', intents: ['mindmap', 'hierarchy', 'brainstorm'], required: ['nodes'], optional: ['parentId', 'edges', 'groups'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'cubic-bezier', 'curve-tension', 'obstacle-fallback', 'manual-routing', 'layout', 'tree-layout', 'radial-layout', 'history'], ['axes', 'grid', 'legend']) }
35
- };
36
-
37
- export const chartProfiles = Object.fromEntries(chartTypes.map(type => [type, {
38
- type,
39
- ...definitions[type],
40
- dataShapes: definitions[type].family === 'diagram' ? ['diagram-spec'] : definitions[type].family === 'project' ? ['records'] : ['records', 'data-table'],
41
- renderers: ['canvas', 'svg'],
42
- exports: ['png', 'svg', 'json'],
43
- limits: type === 'pie' ? { recommendedCategories: 8 } : type === 'radar' ? { minimumIndicators: 3, recommendedIndicators: 8 } : type === 'heatmap' ? { recommendedCells: 2500 } : {}
44
- }]));
45
14
 
46
15
  const intentMap = {
47
16
  schedule: 'gantt', variance: 'gantt', timeline: 'timeline', milestone: 'milestone', progress: 'burndown', release: 'burndown', capacity: 'column', risk: 'scatter', aging: 'bar', matrix: 'heatmap', 'correlation-grid': 'heatmap', multidimensional: 'radar', profile: 'radar', workflow: 'flow', responsibility: 'swimlane', architecture: 'architecture', 'business-architecture': 'architecture', 'data-architecture': 'architecture', 'technical-architecture': 'architecture', mindmap: 'mindmap', hierarchy: 'mindmap', brainstorm: 'mindmap', trend: 'line', 'time-series': 'line', 'part-to-whole': 'pie', distribution: 'column', relationship: 'scatter', correlation: 'scatter', funnel: 'funnel', conversion: 'funnel', 'single-value': 'gauge', ranking: 'bar', comparison: 'bar', composition: 'column'
@@ -225,7 +194,7 @@ export function getCapabilities() {
225
194
  };
226
195
  return {
227
196
  version: '2.0',
228
- contractVersion: '1.0',
197
+ contractVersion,
229
198
  chartTypes,
230
199
  charts: JSON.parse(JSON.stringify(chartProfiles)),
231
200
  intents,
@@ -240,9 +209,10 @@ export function getCapabilities() {
240
209
  },
241
210
  diagrams,
242
211
  excluded: ['map', '3d'],
243
- renderers: ['canvas', 'svg'],
212
+ renderers: [...renderers],
213
+ interactionDefaults: { ...interactionDefaults },
244
214
  interactions: [...new Set(Object.values(chartProfiles).flatMap(profile => profile.interactions))],
245
- exports: ['png', 'svg', 'json'],
215
+ exports: [...exportTypes],
246
216
  headless: {
247
217
  preview: true,
248
218
  json: true,
@@ -263,6 +233,10 @@ export function getCapabilities() {
263
233
  precedence: preferenceCapabilities.precedence,
264
234
  schema: preferenceCapabilities
265
235
  },
236
+ commands: [...commandTypes],
237
+ diagramOperations: [...diagramOperations],
238
+ businessModels: [...businessModels],
239
+ diagramEdgeModels: [...diagramEdgeModels],
266
240
  plugins: true,
267
241
  branding: { defaultEnabled: true, signature: 'Powered by iChart.js', options: [{ name: 'enabled', type: 'boolean', default: true }] },
268
242
  };
@@ -0,0 +1,31 @@
1
+ /** Renderer-independent chart lifecycle cleanup. */
2
+ export function destroyChart(chart) {
3
+ if (chart._destroyed) return;
4
+ chart._destroyed = true;
5
+ chart._preferencesUnsubscribe?.();
6
+ chart._resizeObserver?.disconnect();
7
+ chart._colorSchemeQuery?.removeEventListener?.('change', chart._colorSchemeHandler);
8
+ if (chart._eventsBound) {
9
+ const { target, handler, start, move, end, leave, touchStart, touchMove, touchEnd, wheel } = chart._eventsBound;
10
+ target.removeEventListener('mousemove', handler);
11
+ target.removeEventListener('click', handler);
12
+ target.removeEventListener('pointerdown', start);
13
+ target.removeEventListener('pointermove', move);
14
+ target.removeEventListener('pointerup', end);
15
+ target.removeEventListener('pointercancel', end);
16
+ target.removeEventListener('pointerleave', leave);
17
+ target.removeEventListener('touchstart', touchStart);
18
+ target.removeEventListener('touchmove', touchMove);
19
+ target.removeEventListener('touchend', touchEnd);
20
+ target.removeEventListener('wheel', wheel);
21
+ }
22
+ const target = chart.renderer.svg || chart.renderer.canvas;
23
+ if (chart._keyboardHandler) target?.removeEventListener('keydown', chart._keyboardHandler);
24
+ chart._tooltip?.remove();
25
+ chart.plugins.destroy();
26
+ chart.renderer.destroy();
27
+ chart.listeners.clear();
28
+ chart._eventsBound = null;
29
+ chart._selected.clear();
30
+ chart._clipboard = { nodes: [], edges: [] };
31
+ }
package/src/charts.mjs CHANGED
@@ -8,7 +8,7 @@ import { applyTransforms } from './transforms.mjs';
8
8
  import { buildProjectScene } from './project.mjs';
9
9
  import { formatValue } from './format.mjs';
10
10
  import { contrastRatio, resolveTheme } from './theme.mjs';
11
- import { axisLabelLayout, boxesOverlap, estimateTextWidth, fontSize, titleLayout } from './layout.mjs';
11
+ import { axisLabelLayout, boxesOverlap, estimateTextWidth, fontSize, titleLayout, truncateText } from './layout.mjs';
12
12
 
13
13
  const pick = (row, encoding, fallback) => row?.[encoding?.field || fallback];
14
14
  const font = (spec, role, fallback = '12px system-ui') => spec.theme?.typography?.[role]?.font || fallback;
@@ -72,7 +72,11 @@ function chromeLayout(spec, entries = []) {
72
72
  const size = fontSize(spec, 'legend', 12), lineHeight = Math.max(18, size * Number(spec.theme?.typography?.legend?.lineHeight || 1.4));
73
73
  const swatchSize = Math.max(8, Math.round(size * 0.75)), gap = Math.max(12, Math.round(size));
74
74
  const availableWidth = Math.max(1, spec.width - spec.padding.left - spec.padding.right);
75
- const measured = entries.map((entry, index) => ({ ...entry, index, width: Math.min(availableWidth, swatchSize + 6 + String(entry.name).length * size * 0.58 + gap) }));
75
+ const maxLabelWidth = Math.max(size, availableWidth - swatchSize - 6 - gap);
76
+ const measured = entries.map((entry, index) => {
77
+ const rawName = String(entry.name), name = truncateText(rawName, maxLabelWidth, size);
78
+ return { ...entry, name, rawName, truncated: name !== rawName, index, width: swatchSize + 6 + estimateTextWidth(name, size) + gap };
79
+ });
76
80
  const rows = [];
77
81
  measured.forEach(item => {
78
82
  let row = rows.at(-1);
@@ -88,7 +92,7 @@ function chromeLayout(spec, entries = []) {
88
92
  row.items.forEach(item => { items.push({ ...item, x, y, swatchSize }); x += item.width; });
89
93
  });
90
94
  const bottom = top + rows.length * lineHeight;
91
- return { title, legend: { visible: true, items, bottom }, bottom: Math.max(title.bottom, bottom) };
95
+ return { title, legend: { visible: true, items, bottom, truncatedCount: measured.filter(item => item.truncated).length }, bottom: Math.max(title.bottom, bottom) };
92
96
  }
93
97
 
94
98
  function styledSpec(spec) {
@@ -107,18 +111,6 @@ function layout(spec, data, legendEntries = []) {
107
111
  const dates = temporal ? data.rows.map(row => new Date(row[xField]).getTime()).filter(Number.isFinite) : [];
108
112
  const xNumbers = quantitativeX ? data.rows.map(row => Number(row[xField])).filter(Number.isFinite) : [];
109
113
  const xMin = dates.length ? Math.min(...dates) : xNumbers.length ? Math.min(...xNumbers) : 0, xMax = dates.length ? Math.max(...dates) : xNumbers.length ? Math.max(...xNumbers) : 1;
110
- const categoryField = spec.encoding.x?.field || 'name';
111
- const categoryLabelLength = spec.type === 'bar' ? Math.max(0, ...data.rows.map(row => String(row[categoryField] ?? '').length)) : 0;
112
- const matrixYField = spec.encoding.y?.field || 'y', matrixLabelLength = spec.type === 'heatmap' ? Math.max(0, ...data.rows.map(row => String(row[matrixYField] ?? '').length)) : 0;
113
- const sideLabelLength = Math.max(categoryLabelLength, matrixLabelLength);
114
- const centeredPlot = ['pie', 'funnel', 'gauge', 'radar'].includes(spec.type), plotLeft = centeredPlot ? Math.max(p.left, p.right) : ['bar', 'heatmap'].includes(spec.type) ? Math.min(width * 0.35, Math.max(p.left, sideLabelLength * fontSize(spec, 'axis', 12) * 0.62 + 24)) : p.left, plotRight = centeredPlot ? plotLeft : p.right;
115
- const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
116
- const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
117
- const plotWidth = Math.max(1, width - plotLeft - plotRight);
118
- const axisLabelTexts = temporal ? timeTicks(xMin, xMax, 5).map(value => formatTime(value, xMax - xMin, spec.xAxis?.format, spec.locale)) : quantitativeX ? timeTicks(xMin, xMax, 5).map(value => formatValue(value, spec.xAxis?.format, spec.locale)) : categories;
119
- const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
120
- const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
121
- const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, height - plotTop - bottomReserve) };
122
114
  const yEncodings = Array.isArray(spec.encoding.y) ? spec.encoding.y : [spec.encoding.y || { field: 'value', type: 'quantitative' }];
123
115
  const yField = yEncodings[0]?.field || 'value';
124
116
  const stackMode = typeof spec.stack === 'string' ? spec.stack : spec.stack?.mode;
@@ -140,17 +132,52 @@ function layout(spec, data, legendEntries = []) {
140
132
  const rawRightMin = Math.min(0, ...(yRightNumbers.length ? yRightNumbers : [0])), rawRightMax = Math.max(1, ...(yRightNumbers.length ? yRightNumbers : [1]));
141
133
  const rightType = spec.yAxis?.right?.type || yEncodings[1]?.type || 'linear';
142
134
  const [rightMin, rightMax] = resolveAxisDomain(rawRightMin, rawRightMax, spec.yAxis?.right || {}, rightType);
135
+ const yTicks = axisTicks(min, max, spec.yAxis || {}, axisType), rightTicks = axisTicks(rightMin, rightMax, spec.yAxis?.right || {}, rightType);
136
+ const axisSize = fontSize(spec, 'axis', 12);
137
+ const maxWidth = values => Math.max(0, ...values.map(value => estimateTextWidth(value, axisSize)));
138
+ const categoryField = spec.encoding.x?.field || 'name';
139
+ const categoryTexts = data.rows.map(row => String(row[categoryField] ?? ''));
140
+ const matrixYField = spec.encoding.y?.field || 'y', matrixTexts = spec.type === 'heatmap' ? data.rows.map(row => String(row[matrixYField] ?? '')) : [];
141
+ const leftTexts = spec.type === 'bar' ? categoryTexts : yTicks.map(value => formatValue(value, spec.yAxis?.format, spec.locale));
142
+ const leftLabelWidth = maxWidth(leftTexts), rightLabelWidth = yEncodings.length > 1 && !stackMode ? maxWidth(rightTicks.map(value => formatValue(value, spec.yAxis?.right?.format, spec.locale))) : 0;
143
+ const centeredPlot = ['pie', 'funnel', 'gauge', 'radar'].includes(spec.type);
144
+ const sideLabelWidth = Math.max(spec.type === 'bar' ? leftLabelWidth : 0, spec.type === 'heatmap' ? maxWidth(matrixTexts) : 0);
145
+ const leftTitleBand = spec.yAxis?.title ? axisSize + 16 : 0;
146
+ const rightTitleBand = spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? axisSize + 16 : 0;
147
+ const cartesianLeft = 8 + leftTitleBand + leftLabelWidth + 10;
148
+ const plotLeft = centeredPlot
149
+ ? Math.max(p.left, p.right)
150
+ : spec.type === 'bar'
151
+ ? Math.max(p.left, spec.yAxis?.title ? cartesianLeft : sideLabelWidth + 24)
152
+ : spec.type === 'heatmap'
153
+ ? Math.min(width * 0.35, Math.max(p.left, sideLabelWidth + 24))
154
+ : Math.max(p.left, cartesianLeft);
155
+ const plotRight = centeredPlot ? plotLeft : Math.max(p.right, rightLabelWidth ? rightLabelWidth + rightTitleBand + 18 : 0);
156
+ const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
157
+ const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
158
+ const plotWidth = Math.max(1, width - plotLeft - plotRight);
159
+ const axisLabelTexts = temporal ? timeTicks(xMin, xMax, 5).map(value => formatTime(value, xMax - xMin, spec.xAxis?.format, spec.locale)) : quantitativeX ? timeTicks(xMin, xMax, 5).map(value => formatValue(value, spec.xAxis?.format, spec.locale)) : categories;
160
+ const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
161
+ const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
162
+ const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, height - plotTop - bottomReserve) };
163
+ const axisTitles = {
164
+ y: spec.yAxis?.title ? truncateText(spec.yAxis.title, plot.height, axisSize) : null,
165
+ right: spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? truncateText(spec.yAxis.right.title, plot.height, axisSize) : null
166
+ };
167
+ const axisTitleWarnings = [];
168
+ if (spec.yAxis?.title && axisTitles.y !== String(spec.yAxis.title)) axisTitleWarnings.push({ path: 'yAxis.title', title: String(spec.yAxis.title), availableWidth: plot.height });
169
+ if (spec.yAxis?.right?.title && axisTitles.right !== String(spec.yAxis.right.title) && yEncodings.length > 1 && !stackMode) axisTitleWarnings.push({ path: 'yAxis.right.title', title: String(spec.yAxis.right.title), availableWidth: plot.height });
143
170
  const rightTransform = value => rightType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
144
171
  const yRight = value => plot.y + plot.height - ((rightTransform(value) - rightTransform(rightMin)) / (rightTransform(rightMax) - rightTransform(rightMin) || 1)) * plot.height;
145
- const yTicks = axisTicks(min, max, spec.yAxis || {}, axisType), rightTicks = axisTicks(rightMin, rightMax, spec.yAxis?.right || {}, rightType);
146
172
  const axisInfo = (rawDomain, domain, ticks, axis) => ({ rawDomain, domain, ticks, step: ticks.length > 1 ? (ticks[1] - ticks[0]) : null, policy: validDomain(axis?.domain) ? 'explicit' : axis?.nice === false ? 'raw' : 'nice' });
147
- return { plot, chrome, xLabels, compact: width < 360 || height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, xField, xEncoding, temporal, quantitativeX, xMin, xMax, yField, yEncodings, categories, rawMin, rawMax, min, max, rawRightMin, rawRightMax, rightMin, rightMax, yTicks, rightTicks, axes: { y: axisInfo([rawMin, rawMax], [min, max], yTicks, spec.yAxis || {}), right: axisInfo([rawRightMin, rawRightMax], [rightMin, rightMax], rightTicks, spec.yAxis?.right || {}) }, x, y, yRight, axisType, rightType };
173
+ return { plot, chrome, xLabels, axisTitles, axisTitleWarnings, axisLayout: { size: axisSize, leftLabelWidth, rightLabelWidth }, compact: width < 360 || height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, xField, xEncoding, temporal, quantitativeX, xMin, xMax, yField, yEncodings, categories, rawMin, rawMax, min, max, rawRightMin, rawRightMax, rightMin, rightMax, yTicks, rightTicks, axes: { y: axisInfo([rawMin, rawMax], [min, max], yTicks, spec.yAxis || {}), right: axisInfo([rawRightMin, rawRightMax], [rightMin, rightMax], rightTicks, spec.yAxis?.right || {}) }, x, y, yRight, axisType, rightType };
148
174
  }
149
175
 
150
176
  function addText(scene, id, text, x, y, style = {}, dataRef = null) { scene.add(new SceneNode({ id, type: 'text', geometry: { text: String(text), x, y }, style, dataRef })); }
151
177
 
152
178
  function addAxes(scene, spec, state) {
153
179
  const { plot, categories, min, max, x, y, yRight } = state;
180
+ const axisSize = state.axisLayout.size;
154
181
  scene.add(new SceneNode({ id: 'axis-x', type: 'line', geometry: { x1: plot.x, y1: plot.y + plot.height, x2: plot.x + plot.width, y2: plot.y + plot.height }, style: { stroke: spec.theme.axis } }));
155
182
  scene.add(new SceneNode({ id: 'axis-y', type: 'line', geometry: { x1: plot.x, y1: plot.y, x2: plot.x, y2: plot.y + plot.height }, style: { stroke: spec.theme.axis } }));
156
183
  if (spec.type === 'bar') {
@@ -159,7 +186,7 @@ function addAxes(scene, spec, state) {
159
186
  ticks.forEach(value => addText(scene, `label-x-value-${value}`, formatValue(value, spec.xAxis?.format || spec.yAxis?.format, spec.locale), numericX(value), plot.y + plot.height + 22, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'middle' }));
160
187
  categories.forEach((category, index) => addText(scene, `label-y-category-${index}`, category, plot.x - 10, plot.y + (index + .5) * plot.height / Math.max(1, categories.length) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
161
188
  if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + 42, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
162
- if (spec.yAxis?.title) addText(scene, 'axis-y-title', spec.yAxis.title, 12, plot.y - 4, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'start' });
189
+ if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
163
190
  return;
164
191
  }
165
192
  const labels = state.temporal || state.quantitativeX ? timeTicks(state.xMin, state.xMax, 5) : categories;
@@ -172,7 +199,8 @@ function addAxes(scene, spec, state) {
172
199
  yTicks.forEach(value => addText(scene, `label-y-${value}`, formatValue(value, spec.yAxis?.format, spec.locale), plot.x - 10, y(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
173
200
  if (state.yEncodings.length > 1 && !spec.stack) state.rightTicks.forEach(value => addText(scene, `label-y-right-${value}`, formatValue(value, spec.yAxis?.right?.format, spec.locale), plot.x + plot.width + 10, yRight(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis') }));
174
201
  if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + (state.xLabels.rotation ? state.xLabels.projectedHeight + 20 : 42), { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
175
- if (spec.yAxis?.title) addText(scene, 'axis-y-title', spec.yAxis.title, 8, plot.y + 12, { fill: spec.theme.text, font: font(spec, 'axis') });
202
+ if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
203
+ if (state.axisTitles.right) addText(scene, 'axis-y-right-title', state.axisTitles.right, spec.width - 8 - axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: 90 });
176
204
  }
177
205
 
178
206
  function addTitles(scene, spec, positions) {
@@ -354,6 +382,8 @@ export function buildScene(spec) {
354
382
  const series = Array.isArray(spec.encoding.y) ? spec.encoding.y : [{ ...spec.encoding.y, name: spec.encoding.y?.name || spec.encoding.y?.field }];
355
383
  const entries = legendEntries(spec, data, series, colors);
356
384
  const state = layout(spec, data, entries);
385
+ if (state.chrome.legend.truncatedCount) data.warnings.push({ code: 'LEGEND_OVERFLOW', path: 'legend', count: state.chrome.legend.truncatedCount, message: `${state.chrome.legend.truncatedCount} legend label${state.chrome.legend.truncatedCount === 1 ? '' : 's'} exceeded the available width and was truncated.`, suggestion: 'Use shorter category names or a wider chart.' });
386
+ state.axisTitleWarnings.forEach(({ path, title, availableWidth }) => data.warnings.push({ code: 'TITLE_TRUNCATED', path, availableWidth, message: `Axis title "${title}" exceeded the available vertical plot space and was truncated.`, suggestion: 'Use a taller chart or a shorter axis title.' }));
357
387
  scene._labelArea = state.plot;
358
388
  addTitles(scene, spec, state.chrome.title);
359
389
  if (!['pie', 'funnel', 'gauge', 'heatmap', 'radar'].includes(spec.type)) addAxes(scene, spec, state);
package/src/command.mjs CHANGED
@@ -3,9 +3,10 @@
3
3
  * Commands are JSON-safe, stable-ID based, and do not mutate input data.
4
4
  */
5
5
  import { copyJSON, issue } from './schema.mjs';
6
+ import { commandTypes } from './contract-registry.mjs';
6
7
 
7
8
  export const commandVersion = '1.0';
8
- export const operationTypes = ['updateField', 'updateRecord', 'updateTask', 'shiftTask', 'updateProgress', 'addDependency', 'removeDependency', 'updateMilestone', 'moveNode', 'moveNodes', 'moveNodeToLane', 'resizeNode', 'alignNodes', 'snapNodes', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'toggleGroupCollapse', 'addEdge', 'duplicateSelection', 'pasteSelection'];
9
+ export const operationTypes = [...commandTypes];
9
10
 
10
11
  export function normalizeCommand(input) {
11
12
  const command = copyJSON(input);
@@ -63,7 +64,7 @@ export function commandCapabilities(schema) {
63
64
  const fields = schema?.fields || {};
64
65
  const can = name => fields[name]?.editable !== false && fields[name]?.agentEditable !== false;
65
66
  const flowNode = ['flow-node', 'architecture-node', 'mindmap-node'].includes(schema?.name);
66
- const flowEdge = ['flow-edge', 'architecture-edge'].includes(schema?.name);
67
+ const flowEdge = ['flow-edge', 'architecture-edge', 'mindmap-edge'].includes(schema?.name);
67
68
  return {
68
69
  updateField: Object.keys(fields).filter(can),
69
70
  updateRecord: true,