@tea-agent/loop-agent 0.25.6 → 0.26.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/AGENTS.md +2 -1
- package/CHANGELOG.md +1020 -1006
- package/bin/loop-agent.js +21 -21
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/dag-pi-executor.js +26 -20
- package/dist/executors/model-routing.js +34 -18
- package/dist/governance/manifest-types.js +33 -5
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/harness.json +1 -2
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/model-routing.md +2 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
export type DagLayoutNodeInput = { nodeId: string };
|
|
2
|
-
export type DagLayoutEdgeInput = { from: string; to: string };
|
|
3
|
-
export type DagLayoutNode = {
|
|
4
|
-
nodeId: string;
|
|
5
|
-
rank: number;
|
|
6
|
-
index: number;
|
|
7
|
-
x: number;
|
|
8
|
-
y: number;
|
|
9
|
-
width: number;
|
|
10
|
-
height: number;
|
|
11
|
-
};
|
|
12
|
-
export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
|
|
13
|
-
export type DagLayout = {
|
|
14
|
-
width: number;
|
|
15
|
-
height: number;
|
|
16
|
-
nodes: DagLayoutNode[];
|
|
17
|
-
edges: DagLayoutEdge[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function layoutDag(
|
|
21
|
-
nodes?: DagLayoutNodeInput[],
|
|
22
|
-
edges?: DagLayoutEdgeInput[],
|
|
23
|
-
ranks?: string[][],
|
|
24
|
-
options?: Partial<{
|
|
25
|
-
nodeWidth: number;
|
|
26
|
-
nodeHeight: number;
|
|
27
|
-
rankGap: number;
|
|
28
|
-
nodeGap: number;
|
|
29
|
-
padding: number;
|
|
30
|
-
}>,
|
|
31
|
-
): DagLayout;
|
|
1
|
+
export type DagLayoutNodeInput = { nodeId: string };
|
|
2
|
+
export type DagLayoutEdgeInput = { from: string; to: string };
|
|
3
|
+
export type DagLayoutNode = {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
rank: number;
|
|
6
|
+
index: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
|
|
13
|
+
export type DagLayout = {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
nodes: DagLayoutNode[];
|
|
17
|
+
edges: DagLayoutEdge[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function layoutDag(
|
|
21
|
+
nodes?: DagLayoutNodeInput[],
|
|
22
|
+
edges?: DagLayoutEdgeInput[],
|
|
23
|
+
ranks?: string[][],
|
|
24
|
+
options?: Partial<{
|
|
25
|
+
nodeWidth: number;
|
|
26
|
+
nodeHeight: number;
|
|
27
|
+
rankGap: number;
|
|
28
|
+
nodeGap: number;
|
|
29
|
+
padding: number;
|
|
30
|
+
}>,
|
|
31
|
+
): DagLayout;
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
const DEFAULTS = {
|
|
2
|
-
nodeWidth: 216,
|
|
3
|
-
nodeHeight: 92,
|
|
4
|
-
rankGap: 104,
|
|
5
|
-
nodeGap: 28,
|
|
6
|
-
padding: 32,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
|
|
10
|
-
const config = { ...DEFAULTS, ...options };
|
|
11
|
-
const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
|
|
12
|
-
if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
|
|
13
|
-
const known = new Set(nodeIds);
|
|
14
|
-
const validEdges = edges
|
|
15
|
-
.filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
|
|
16
|
-
.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
17
|
-
const layers = normalizeLayers(nodeIds, validEdges, ranks);
|
|
18
|
-
const positioned = [];
|
|
19
|
-
for (let rank = 0; rank < layers.length; rank++) {
|
|
20
|
-
const ids = layers[rank];
|
|
21
|
-
for (let index = 0; index < ids.length; index++) {
|
|
22
|
-
positioned.push({
|
|
23
|
-
nodeId: ids[index], rank, index,
|
|
24
|
-
x: config.padding + rank * (config.nodeWidth + config.rankGap),
|
|
25
|
-
y: config.padding + index * (config.nodeHeight + config.nodeGap),
|
|
26
|
-
width: config.nodeWidth, height: config.nodeHeight,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const byId = new Map(positioned.map((node) => [node.nodeId, node]));
|
|
31
|
-
const routed = validEdges.map((edge) => {
|
|
32
|
-
const from = byId.get(edge.from);
|
|
33
|
-
const to = byId.get(edge.to);
|
|
34
|
-
const x1 = from.x + from.width;
|
|
35
|
-
const y1 = from.y + from.height / 2;
|
|
36
|
-
const x2 = to.x;
|
|
37
|
-
const y2 = to.y + to.height / 2;
|
|
38
|
-
const bend = Math.max(36, (x2 - x1) / 2);
|
|
39
|
-
return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
|
|
40
|
-
});
|
|
41
|
-
const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
|
|
42
|
-
return {
|
|
43
|
-
width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
|
|
44
|
-
height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
|
|
45
|
-
nodes: positioned,
|
|
46
|
-
edges: routed,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function normalizeLayers(nodeIds, edges, ranks) {
|
|
51
|
-
const known = new Set(nodeIds);
|
|
52
|
-
const seen = new Set();
|
|
53
|
-
const layers = [];
|
|
54
|
-
if (Array.isArray(ranks)) {
|
|
55
|
-
for (const rank of ranks) {
|
|
56
|
-
if (!Array.isArray(rank)) continue;
|
|
57
|
-
const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
|
|
58
|
-
ids.forEach((id) => seen.add(id));
|
|
59
|
-
if (ids.length > 0) layers.push(ids);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (layers.length === 0 && edges.length > 0) {
|
|
63
|
-
const level = new Map(nodeIds.map((id) => [id, 0]));
|
|
64
|
-
for (let pass = 0; pass < nodeIds.length; pass++) {
|
|
65
|
-
let changed = false;
|
|
66
|
-
for (const edge of edges) {
|
|
67
|
-
const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
|
|
68
|
-
if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
|
|
69
|
-
}
|
|
70
|
-
if (!changed) break;
|
|
71
|
-
}
|
|
72
|
-
for (const id of nodeIds) {
|
|
73
|
-
const rank = level.get(id);
|
|
74
|
-
(layers[rank] ??= []).push(id);
|
|
75
|
-
seen.add(id);
|
|
76
|
-
}
|
|
77
|
-
for (const layer of layers) layer?.sort();
|
|
78
|
-
}
|
|
79
|
-
for (const id of nodeIds) {
|
|
80
|
-
if (!seen.has(id)) (layers[0] ??= []).push(id);
|
|
81
|
-
}
|
|
82
|
-
return layers.filter(Boolean);
|
|
83
|
-
}
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
nodeWidth: 216,
|
|
3
|
+
nodeHeight: 92,
|
|
4
|
+
rankGap: 104,
|
|
5
|
+
nodeGap: 28,
|
|
6
|
+
padding: 32,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
|
|
10
|
+
const config = { ...DEFAULTS, ...options };
|
|
11
|
+
const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
|
|
12
|
+
if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
|
|
13
|
+
const known = new Set(nodeIds);
|
|
14
|
+
const validEdges = edges
|
|
15
|
+
.filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
|
|
16
|
+
.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
17
|
+
const layers = normalizeLayers(nodeIds, validEdges, ranks);
|
|
18
|
+
const positioned = [];
|
|
19
|
+
for (let rank = 0; rank < layers.length; rank++) {
|
|
20
|
+
const ids = layers[rank];
|
|
21
|
+
for (let index = 0; index < ids.length; index++) {
|
|
22
|
+
positioned.push({
|
|
23
|
+
nodeId: ids[index], rank, index,
|
|
24
|
+
x: config.padding + rank * (config.nodeWidth + config.rankGap),
|
|
25
|
+
y: config.padding + index * (config.nodeHeight + config.nodeGap),
|
|
26
|
+
width: config.nodeWidth, height: config.nodeHeight,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const byId = new Map(positioned.map((node) => [node.nodeId, node]));
|
|
31
|
+
const routed = validEdges.map((edge) => {
|
|
32
|
+
const from = byId.get(edge.from);
|
|
33
|
+
const to = byId.get(edge.to);
|
|
34
|
+
const x1 = from.x + from.width;
|
|
35
|
+
const y1 = from.y + from.height / 2;
|
|
36
|
+
const x2 = to.x;
|
|
37
|
+
const y2 = to.y + to.height / 2;
|
|
38
|
+
const bend = Math.max(36, (x2 - x1) / 2);
|
|
39
|
+
return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
|
|
40
|
+
});
|
|
41
|
+
const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
|
|
42
|
+
return {
|
|
43
|
+
width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
|
|
44
|
+
height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
|
|
45
|
+
nodes: positioned,
|
|
46
|
+
edges: routed,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeLayers(nodeIds, edges, ranks) {
|
|
51
|
+
const known = new Set(nodeIds);
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const layers = [];
|
|
54
|
+
if (Array.isArray(ranks)) {
|
|
55
|
+
for (const rank of ranks) {
|
|
56
|
+
if (!Array.isArray(rank)) continue;
|
|
57
|
+
const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
|
|
58
|
+
ids.forEach((id) => seen.add(id));
|
|
59
|
+
if (ids.length > 0) layers.push(ids);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (layers.length === 0 && edges.length > 0) {
|
|
63
|
+
const level = new Map(nodeIds.map((id) => [id, 0]));
|
|
64
|
+
for (let pass = 0; pass < nodeIds.length; pass++) {
|
|
65
|
+
let changed = false;
|
|
66
|
+
for (const edge of edges) {
|
|
67
|
+
const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
|
|
68
|
+
if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
|
|
69
|
+
}
|
|
70
|
+
if (!changed) break;
|
|
71
|
+
}
|
|
72
|
+
for (const id of nodeIds) {
|
|
73
|
+
const rank = level.get(id);
|
|
74
|
+
(layers[rank] ??= []).push(id);
|
|
75
|
+
seen.add(id);
|
|
76
|
+
}
|
|
77
|
+
for (const layer of layers) layer?.sort();
|
|
78
|
+
}
|
|
79
|
+
for (const id of nodeIds) {
|
|
80
|
+
if (!seen.has(id)) (layers[0] ??= []).push(id);
|
|
81
|
+
}
|
|
82
|
+
return layers.filter(Boolean);
|
|
83
|
+
}
|
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
/** DOM helpers (Observe UI R5). */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Stable key → node maps, kept per (parent, scope) so reconcileKeyed never
|
|
5
|
-
* depends on dataset support. A WeakMap keyed by parent holds a map of
|
|
6
|
-
* scope → Map<key, node>. A stale parent (e.g. after view teardown) releases
|
|
7
|
-
* its bookkeeping automatically. The default scope "default" is used when a
|
|
8
|
-
* caller reconciles all keyed children of a parent in a single pass; multiple
|
|
9
|
-
* reconcilers that share one parent must pass distinct scope strings so they
|
|
10
|
-
* do not clobber each other's keys.
|
|
11
|
-
*/
|
|
12
|
-
const reconcileMaps = new WeakMap();
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Keyed DOM reconciliation: preserve node identity when a stable key recurs
|
|
16
|
-
* across renders; create a fresh node (with fresh closures) when the key is
|
|
17
|
-
* new; remove nodes whose key dropped out of the item set.
|
|
18
|
-
*
|
|
19
|
-
* Callers drive node construction via `create` and content sync via optional
|
|
20
|
-
* `update`. The parent is not bulk-cleared, so headings/headers placed outside
|
|
21
|
-
* this call survive, and scrolling/focus/pointer state on reused nodes is kept.
|
|
22
|
-
*
|
|
23
|
-
* @param {Node} parent
|
|
24
|
-
* @param {Array} items
|
|
25
|
-
* @param {{ getKey: (item: any, index: number) => string,
|
|
26
|
-
* create: (item: any, index: number) => Node,
|
|
27
|
-
* update?: (node: any, item: any, index: number) => void,
|
|
28
|
-
* scope?: string }} opts
|
|
29
|
-
* @returns {Array<Node>} the (possibly reused) nodes in item order
|
|
30
|
-
*/
|
|
31
|
-
export function reconcileKeyed(parent, items, opts) {
|
|
32
|
-
const { getKey, create, update, scope = "default" } = opts;
|
|
33
|
-
let scopes = reconcileMaps.get(parent);
|
|
34
|
-
if (!scopes) {
|
|
35
|
-
scopes = new Map();
|
|
36
|
-
reconcileMaps.set(parent, scopes);
|
|
37
|
-
}
|
|
38
|
-
let map = scopes.get(scope);
|
|
39
|
-
if (!map) {
|
|
40
|
-
map = new Map();
|
|
41
|
-
scopes.set(scope, map);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const nextKeys = new Set();
|
|
45
|
-
const result = [];
|
|
46
|
-
for (let i = 0; i < items.length; i++) {
|
|
47
|
-
const item = items[i];
|
|
48
|
-
const key = String(getKey(item, i));
|
|
49
|
-
nextKeys.add(key);
|
|
50
|
-
let node = map.get(key);
|
|
51
|
-
if (node) {
|
|
52
|
-
if (update) update(node, item, i);
|
|
53
|
-
} else {
|
|
54
|
-
node = create(item, i);
|
|
55
|
-
map.set(key, node);
|
|
56
|
-
// Sync freshly created nodes so callers can split structure (create) from
|
|
57
|
-
// content (update) and still populate the first paint.
|
|
58
|
-
if (update) update(node, item, i);
|
|
59
|
-
}
|
|
60
|
-
result.push(node);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Remove nodes whose key is no longer present in THIS scope.
|
|
64
|
-
for (const [key, node] of map) {
|
|
65
|
-
if (!nextKeys.has(key)) {
|
|
66
|
-
parent.removeChild(node);
|
|
67
|
-
map.delete(key);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const resultSet = new Set(result);
|
|
72
|
-
const currentOrder = childNodesOf(parent).filter((node) => resultSet.has(node));
|
|
73
|
-
const orderChanged =
|
|
74
|
-
currentOrder.length !== result.length ||
|
|
75
|
-
currentOrder.some((node, index) => node !== result[index]);
|
|
76
|
-
if (orderChanged) {
|
|
77
|
-
for (const node of result) parent.appendChild(node);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function childNodesOf(node) {
|
|
84
|
-
return Array.from(node.childNodes ?? node.children ?? []);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function nodeKind(node) {
|
|
88
|
-
if (node.nodeType === 3) return "#text";
|
|
89
|
-
return node.nodeName ?? node.tagName ?? "";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function syncElementAttributes(current, next) {
|
|
93
|
-
if (current.getAttributeNames && next.getAttributeNames) {
|
|
94
|
-
const nextNames = new Set(next.getAttributeNames());
|
|
95
|
-
for (const name of current.getAttributeNames()) {
|
|
96
|
-
if (!nextNames.has(name)) current.removeAttribute(name);
|
|
97
|
-
}
|
|
98
|
-
for (const name of nextNames) {
|
|
99
|
-
current.setAttribute(name, next.getAttribute(name));
|
|
100
|
-
}
|
|
101
|
-
} else if (current.attrs && next.attrs) {
|
|
102
|
-
for (const name of Object.keys(current.attrs)) {
|
|
103
|
-
if (!(name in next.attrs)) delete current.attrs[name];
|
|
104
|
-
}
|
|
105
|
-
for (const [name, value] of Object.entries(next.attrs)) {
|
|
106
|
-
current.setAttribute(name, value);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const prop of [
|
|
111
|
-
"className",
|
|
112
|
-
"href",
|
|
113
|
-
"title",
|
|
114
|
-
"target",
|
|
115
|
-
"type",
|
|
116
|
-
"value",
|
|
117
|
-
"hidden",
|
|
118
|
-
"tabIndex",
|
|
119
|
-
]) {
|
|
120
|
-
if (prop in next) current[prop] = next[prop];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Recursively sync a structurally compatible node without replacing compatible
|
|
126
|
-
* descendants. Existing event listeners and focusable node identity survive;
|
|
127
|
-
* incompatible tag/node kinds are reported to the caller for replacement.
|
|
128
|
-
*/
|
|
129
|
-
export function syncCompatibleNode(current, next) {
|
|
130
|
-
if (!current || !next || nodeKind(current) !== nodeKind(next)) return false;
|
|
131
|
-
if (current.nodeType === 3 || nodeKind(current) === "#TEXT") {
|
|
132
|
-
current.textContent = next.textContent;
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
syncElementAttributes(current, next);
|
|
137
|
-
const currentChildren = childNodesOf(current);
|
|
138
|
-
const nextChildren = childNodesOf(next);
|
|
139
|
-
if (nextChildren.length === 0) {
|
|
140
|
-
current.textContent = next.textContent;
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
for (let index = 0; index < Math.max(currentChildren.length, nextChildren.length); index++) {
|
|
145
|
-
const currentChild = currentChildren[index];
|
|
146
|
-
const nextChild = nextChildren[index];
|
|
147
|
-
if (!nextChild && currentChild) {
|
|
148
|
-
current.removeChild(currentChild);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (nextChild && !currentChild) {
|
|
152
|
-
current.appendChild(nextChild);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (!syncCompatibleNode(currentChild, nextChild)) {
|
|
156
|
-
current.replaceChild(nextChild, currentChild);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function clearNode(node) {
|
|
163
|
-
while (node.firstChild) node.removeChild(node.firstChild);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function el(tag, className, text) {
|
|
167
|
-
const node = document.createElement(tag);
|
|
168
|
-
if (className) node.className = className;
|
|
169
|
-
if (text !== undefined) node.textContent = text;
|
|
170
|
-
return node;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function metaGrid(entries) {
|
|
174
|
-
const dl = el("dl", "meta-grid");
|
|
175
|
-
for (const [label, value] of entries) {
|
|
176
|
-
const item = document.createElement("div");
|
|
177
|
-
item.className = "meta-item";
|
|
178
|
-
const dt = document.createElement("dt");
|
|
179
|
-
dt.textContent = label;
|
|
180
|
-
const dd = document.createElement("dd");
|
|
181
|
-
if (value instanceof Node) dd.appendChild(value);
|
|
182
|
-
else dd.textContent = value ?? "—";
|
|
183
|
-
item.appendChild(dt);
|
|
184
|
-
item.appendChild(dd);
|
|
185
|
-
dl.appendChild(item);
|
|
186
|
-
}
|
|
187
|
-
return dl;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function buildTable(headers, rows) {
|
|
191
|
-
const table = document.createElement("table");
|
|
192
|
-
const thead = document.createElement("thead");
|
|
193
|
-
const headRow = document.createElement("tr");
|
|
194
|
-
for (const h of headers) {
|
|
195
|
-
const th = document.createElement("th");
|
|
196
|
-
th.textContent = h;
|
|
197
|
-
headRow.appendChild(th);
|
|
198
|
-
}
|
|
199
|
-
thead.appendChild(headRow);
|
|
200
|
-
table.appendChild(thead);
|
|
201
|
-
|
|
202
|
-
const tbody = document.createElement("tbody");
|
|
203
|
-
for (const row of rows) {
|
|
204
|
-
const tr = document.createElement("tr");
|
|
205
|
-
if (row.className) tr.className = row.className;
|
|
206
|
-
if (row.onClick) {
|
|
207
|
-
tr.classList.add("clickable");
|
|
208
|
-
tr.addEventListener("click", row.onClick);
|
|
209
|
-
}
|
|
210
|
-
for (const cell of row.cells) {
|
|
211
|
-
const td = document.createElement("td");
|
|
212
|
-
if (cell instanceof Node) td.appendChild(cell);
|
|
213
|
-
else td.textContent = cell ?? "—";
|
|
214
|
-
tr.appendChild(td);
|
|
215
|
-
}
|
|
216
|
-
tbody.appendChild(tr);
|
|
217
|
-
}
|
|
218
|
-
table.appendChild(tbody);
|
|
219
|
-
return table;
|
|
220
|
-
}
|
|
1
|
+
/** DOM helpers (Observe UI R5). */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable key → node maps, kept per (parent, scope) so reconcileKeyed never
|
|
5
|
+
* depends on dataset support. A WeakMap keyed by parent holds a map of
|
|
6
|
+
* scope → Map<key, node>. A stale parent (e.g. after view teardown) releases
|
|
7
|
+
* its bookkeeping automatically. The default scope "default" is used when a
|
|
8
|
+
* caller reconciles all keyed children of a parent in a single pass; multiple
|
|
9
|
+
* reconcilers that share one parent must pass distinct scope strings so they
|
|
10
|
+
* do not clobber each other's keys.
|
|
11
|
+
*/
|
|
12
|
+
const reconcileMaps = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keyed DOM reconciliation: preserve node identity when a stable key recurs
|
|
16
|
+
* across renders; create a fresh node (with fresh closures) when the key is
|
|
17
|
+
* new; remove nodes whose key dropped out of the item set.
|
|
18
|
+
*
|
|
19
|
+
* Callers drive node construction via `create` and content sync via optional
|
|
20
|
+
* `update`. The parent is not bulk-cleared, so headings/headers placed outside
|
|
21
|
+
* this call survive, and scrolling/focus/pointer state on reused nodes is kept.
|
|
22
|
+
*
|
|
23
|
+
* @param {Node} parent
|
|
24
|
+
* @param {Array} items
|
|
25
|
+
* @param {{ getKey: (item: any, index: number) => string,
|
|
26
|
+
* create: (item: any, index: number) => Node,
|
|
27
|
+
* update?: (node: any, item: any, index: number) => void,
|
|
28
|
+
* scope?: string }} opts
|
|
29
|
+
* @returns {Array<Node>} the (possibly reused) nodes in item order
|
|
30
|
+
*/
|
|
31
|
+
export function reconcileKeyed(parent, items, opts) {
|
|
32
|
+
const { getKey, create, update, scope = "default" } = opts;
|
|
33
|
+
let scopes = reconcileMaps.get(parent);
|
|
34
|
+
if (!scopes) {
|
|
35
|
+
scopes = new Map();
|
|
36
|
+
reconcileMaps.set(parent, scopes);
|
|
37
|
+
}
|
|
38
|
+
let map = scopes.get(scope);
|
|
39
|
+
if (!map) {
|
|
40
|
+
map = new Map();
|
|
41
|
+
scopes.set(scope, map);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextKeys = new Set();
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let i = 0; i < items.length; i++) {
|
|
47
|
+
const item = items[i];
|
|
48
|
+
const key = String(getKey(item, i));
|
|
49
|
+
nextKeys.add(key);
|
|
50
|
+
let node = map.get(key);
|
|
51
|
+
if (node) {
|
|
52
|
+
if (update) update(node, item, i);
|
|
53
|
+
} else {
|
|
54
|
+
node = create(item, i);
|
|
55
|
+
map.set(key, node);
|
|
56
|
+
// Sync freshly created nodes so callers can split structure (create) from
|
|
57
|
+
// content (update) and still populate the first paint.
|
|
58
|
+
if (update) update(node, item, i);
|
|
59
|
+
}
|
|
60
|
+
result.push(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove nodes whose key is no longer present in THIS scope.
|
|
64
|
+
for (const [key, node] of map) {
|
|
65
|
+
if (!nextKeys.has(key)) {
|
|
66
|
+
parent.removeChild(node);
|
|
67
|
+
map.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resultSet = new Set(result);
|
|
72
|
+
const currentOrder = childNodesOf(parent).filter((node) => resultSet.has(node));
|
|
73
|
+
const orderChanged =
|
|
74
|
+
currentOrder.length !== result.length ||
|
|
75
|
+
currentOrder.some((node, index) => node !== result[index]);
|
|
76
|
+
if (orderChanged) {
|
|
77
|
+
for (const node of result) parent.appendChild(node);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function childNodesOf(node) {
|
|
84
|
+
return Array.from(node.childNodes ?? node.children ?? []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function nodeKind(node) {
|
|
88
|
+
if (node.nodeType === 3) return "#text";
|
|
89
|
+
return node.nodeName ?? node.tagName ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncElementAttributes(current, next) {
|
|
93
|
+
if (current.getAttributeNames && next.getAttributeNames) {
|
|
94
|
+
const nextNames = new Set(next.getAttributeNames());
|
|
95
|
+
for (const name of current.getAttributeNames()) {
|
|
96
|
+
if (!nextNames.has(name)) current.removeAttribute(name);
|
|
97
|
+
}
|
|
98
|
+
for (const name of nextNames) {
|
|
99
|
+
current.setAttribute(name, next.getAttribute(name));
|
|
100
|
+
}
|
|
101
|
+
} else if (current.attrs && next.attrs) {
|
|
102
|
+
for (const name of Object.keys(current.attrs)) {
|
|
103
|
+
if (!(name in next.attrs)) delete current.attrs[name];
|
|
104
|
+
}
|
|
105
|
+
for (const [name, value] of Object.entries(next.attrs)) {
|
|
106
|
+
current.setAttribute(name, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const prop of [
|
|
111
|
+
"className",
|
|
112
|
+
"href",
|
|
113
|
+
"title",
|
|
114
|
+
"target",
|
|
115
|
+
"type",
|
|
116
|
+
"value",
|
|
117
|
+
"hidden",
|
|
118
|
+
"tabIndex",
|
|
119
|
+
]) {
|
|
120
|
+
if (prop in next) current[prop] = next[prop];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recursively sync a structurally compatible node without replacing compatible
|
|
126
|
+
* descendants. Existing event listeners and focusable node identity survive;
|
|
127
|
+
* incompatible tag/node kinds are reported to the caller for replacement.
|
|
128
|
+
*/
|
|
129
|
+
export function syncCompatibleNode(current, next) {
|
|
130
|
+
if (!current || !next || nodeKind(current) !== nodeKind(next)) return false;
|
|
131
|
+
if (current.nodeType === 3 || nodeKind(current) === "#TEXT") {
|
|
132
|
+
current.textContent = next.textContent;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
syncElementAttributes(current, next);
|
|
137
|
+
const currentChildren = childNodesOf(current);
|
|
138
|
+
const nextChildren = childNodesOf(next);
|
|
139
|
+
if (nextChildren.length === 0) {
|
|
140
|
+
current.textContent = next.textContent;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (let index = 0; index < Math.max(currentChildren.length, nextChildren.length); index++) {
|
|
145
|
+
const currentChild = currentChildren[index];
|
|
146
|
+
const nextChild = nextChildren[index];
|
|
147
|
+
if (!nextChild && currentChild) {
|
|
148
|
+
current.removeChild(currentChild);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (nextChild && !currentChild) {
|
|
152
|
+
current.appendChild(nextChild);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!syncCompatibleNode(currentChild, nextChild)) {
|
|
156
|
+
current.replaceChild(nextChild, currentChild);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function clearNode(node) {
|
|
163
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function el(tag, className, text) {
|
|
167
|
+
const node = document.createElement(tag);
|
|
168
|
+
if (className) node.className = className;
|
|
169
|
+
if (text !== undefined) node.textContent = text;
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function metaGrid(entries) {
|
|
174
|
+
const dl = el("dl", "meta-grid");
|
|
175
|
+
for (const [label, value] of entries) {
|
|
176
|
+
const item = document.createElement("div");
|
|
177
|
+
item.className = "meta-item";
|
|
178
|
+
const dt = document.createElement("dt");
|
|
179
|
+
dt.textContent = label;
|
|
180
|
+
const dd = document.createElement("dd");
|
|
181
|
+
if (value instanceof Node) dd.appendChild(value);
|
|
182
|
+
else dd.textContent = value ?? "—";
|
|
183
|
+
item.appendChild(dt);
|
|
184
|
+
item.appendChild(dd);
|
|
185
|
+
dl.appendChild(item);
|
|
186
|
+
}
|
|
187
|
+
return dl;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function buildTable(headers, rows) {
|
|
191
|
+
const table = document.createElement("table");
|
|
192
|
+
const thead = document.createElement("thead");
|
|
193
|
+
const headRow = document.createElement("tr");
|
|
194
|
+
for (const h of headers) {
|
|
195
|
+
const th = document.createElement("th");
|
|
196
|
+
th.textContent = h;
|
|
197
|
+
headRow.appendChild(th);
|
|
198
|
+
}
|
|
199
|
+
thead.appendChild(headRow);
|
|
200
|
+
table.appendChild(thead);
|
|
201
|
+
|
|
202
|
+
const tbody = document.createElement("tbody");
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
const tr = document.createElement("tr");
|
|
205
|
+
if (row.className) tr.className = row.className;
|
|
206
|
+
if (row.onClick) {
|
|
207
|
+
tr.classList.add("clickable");
|
|
208
|
+
tr.addEventListener("click", row.onClick);
|
|
209
|
+
}
|
|
210
|
+
for (const cell of row.cells) {
|
|
211
|
+
const td = document.createElement("td");
|
|
212
|
+
if (cell instanceof Node) td.appendChild(cell);
|
|
213
|
+
else td.textContent = cell ?? "—";
|
|
214
|
+
tr.appendChild(td);
|
|
215
|
+
}
|
|
216
|
+
tbody.appendChild(tr);
|
|
217
|
+
}
|
|
218
|
+
table.appendChild(tbody);
|
|
219
|
+
return table;
|
|
220
|
+
}
|