Skip to content

Commit 7f2c2fd

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat: persist incremental design analysis facts and cloud index
1 parent b3b25a5 commit 7f2c2fd

27 files changed

Lines changed: 1852 additions & 365 deletions

.github/workflows/design-index.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Design index maintenance
2+
3+
on:
4+
push:
5+
branches: [main]
6+
schedule:
7+
- cron: '*/30 * * * *'
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: design-index-maintenance
15+
cancel-in-progress: true
16+
17+
jobs:
18+
refresh:
19+
if: vars.DESIGN_DIFF_INDEX_ENABLED == 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
20+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
21+
timeout-minutes: 15
22+
steps:
23+
- name: Checkout immutable trusted engine
24+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
25+
with:
26+
ref: ${{ github.sha }}
27+
fetch-depth: 0
28+
persist-credentials: false
29+
30+
- name: Setup Bun
31+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
32+
with:
33+
bun-version: 1.4.1
34+
35+
- name: Install trusted dependencies
36+
run: bun install --frozen-lockfile --ignore-scripts
37+
38+
- name: Resolve current staging source
39+
id: staging
40+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
41+
with:
42+
script: |
43+
const { owner, repo } = context.repo;
44+
const sha = (await github.rest.repos.getCommit({ owner, repo, ref: 'staging' })).data.sha;
45+
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error('Invalid staging identity');
46+
core.setOutput('sha', sha);
47+
48+
- name: Identify compatible index
49+
id: index
50+
run: echo "identity=$(bun --no-env-file scripts/design-diff/index-cli.ts --identity)" >> "$GITHUB_OUTPUT"
51+
52+
- name: Restore prior facts
53+
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
54+
with:
55+
path: ${{ runner.temp }}/design-index
56+
key: design-index-v1-${{ steps.index.outputs.identity }}-${{ steps.staging.outputs.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
57+
restore-keys: design-index-v1-${{ steps.index.outputs.identity }}-
58+
59+
- name: Fetch source as data
60+
env:
61+
SOURCE_SHA: ${{ steps.staging.outputs.sha }}
62+
GH_TOKEN: ${{ github.token }}
63+
run: |
64+
AUTH_HEADER="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
65+
git -c "http.extraheader=AUTHORIZATION: basic $AUTH_HEADER" fetch --no-tags origin "$SOURCE_SHA" >/dev/null 2>&1
66+
test "$(git rev-parse --verify "$SOURCE_SHA^{commit}")" = "$SOURCE_SHA"
67+
68+
- name: Refresh and checkpoint staging snapshot
69+
env:
70+
SOURCE_SHA: ${{ steps.staging.outputs.sha }}
71+
CACHE_DIRECTORY: ${{ runner.temp }}/design-index
72+
METRICS_PATH: ${{ runner.temp }}/design-index-metrics.json
73+
run: bun run design:index --ref "$SOURCE_SHA" --cache-dir "$CACHE_DIRECTORY" --metrics "$METRICS_PATH" > /dev/null 2>&1
74+
75+
- name: Save completed immutable cache
76+
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
77+
with:
78+
path: ${{ runner.temp }}/design-index
79+
key: design-index-v1-${{ steps.index.outputs.identity }}-${{ steps.staging.outputs.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
80+
81+
- name: Retain execution metrics
82+
if: always()
83+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
84+
with:
85+
name: design-index-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
86+
path: ${{ runner.temp }}/design-index-metrics.json
87+
retention-days: 7
88+
if-no-files-found: warn

.github/workflows/design-review.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ jobs:
6464
- name: Install trusted dependencies
6565
run: bun install --frozen-lockfile --ignore-scripts
6666

67+
- name: Identify compatible index
68+
if: vars.DESIGN_DIFF_INDEX_ENABLED == 'true'
69+
id: index
70+
run: echo "identity=$(bun --no-env-file scripts/design-diff/index-cli.ts --identity)" >> "$GITHUB_OUTPUT"
71+
72+
- name: Restore shared facts into isolated job storage
73+
if: vars.DESIGN_DIFF_INDEX_ENABLED == 'true'
74+
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
75+
with:
76+
path: ${{ runner.temp }}/design-index
77+
key: design-index-v1-${{ steps.index.outputs.identity }}-pr-${{ steps.revisions.outputs.head }}-${{ github.run_id }}-${{ github.run_attempt }}
78+
restore-keys: design-index-v1-${{ steps.index.outputs.identity }}-
79+
6780
- name: Fetch PR source as Git objects
6881
env:
6982
BASE_SHA: ${{ steps.revisions.outputs.base }}
@@ -82,13 +95,20 @@ jobs:
8295
DESIGN_DIFF_PR: ${{ steps.revisions.outputs.pr }}
8396
DESIGN_DIFF_ENGINE_SHA: ${{ steps.revisions.outputs.engine }}
8497
REPORT_PATH: ${{ runner.temp }}/design-diff-${{ steps.revisions.outputs.pr }}-${{ steps.revisions.outputs.head }}.json
85-
run: bun run design:diff --base "$BASE_SHA" --head "$HEAD_SHA" --output "$REPORT_PATH" > /dev/null 2>&1
98+
INDEX_ENABLED: ${{ vars.DESIGN_DIFF_INDEX_ENABLED }}
99+
CACHE_DIRECTORY: ${{ runner.temp }}/design-index
100+
run: |
101+
CACHE_ARGS=(--no-cache)
102+
if [ "$INDEX_ENABLED" = true ]; then CACHE_ARGS=(--cache-dir "$CACHE_DIRECTORY"); fi
103+
bun run design:diff --base "$BASE_SHA" --head "$HEAD_SHA" "${CACHE_ARGS[@]}" --output "$REPORT_PATH" --metrics "${REPORT_PATH%.json}.metrics.json" > /dev/null 2>&1
86104
87105
- name: Retain JSON report
88106
if: ${{ always() && steps.revisions.outcome == 'success' }}
89107
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
90108
with:
91109
name: design-diff-${{ steps.revisions.outputs.pr }}-${{ steps.revisions.outputs.head }}
92-
path: ${{ runner.temp }}/design-diff-${{ steps.revisions.outputs.pr }}-${{ steps.revisions.outputs.head }}.json
110+
path: |
111+
${{ runner.temp }}/design-diff-${{ steps.revisions.outputs.pr }}-${{ steps.revisions.outputs.head }}.json
112+
${{ runner.temp }}/design-diff-${{ steps.revisions.outputs.pr }}-${{ steps.revisions.outputs.head }}.metrics.json
93113
retention-days: 7
94114
if-no-files-found: error

bun.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"type-check": "turbo run type-check",
116116
"release": "bun run scripts/create-single-release.ts",
117117
"test:scripts": "vitest run --config scripts/vitest.config.ts",
118+
"design:index": "bun --no-env-file scripts/design-diff/index-cli.ts",
118119
"design:diff": "bun --no-env-file scripts/design-diff/cli.ts",
119120
"check:design-diff-types": "tsc --noEmit --project scripts/design-diff/tsconfig.json"
120121
},
@@ -158,6 +159,7 @@
158159
"@octokit/rest": "^21.0.0",
159160
"@sim/utils": "workspace:*",
160161
"@types/babel__traverse": "7.28.0",
162+
"@types/bun": "1.4.1",
161163
"@types/node": "24.2.1",
162164
"@types/opentype.js": "1.3.10",
163165
"@typescript/native": "npm:typescript@^7.0.2",

scripts/design-diff/README.md

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ only the runtime predicate, handler or label does not qualify.
5555
Media exclusion applies to recognized JSX/HTML/MDX media elements and asset files. Repository
5656
conventions also identify social-card image generators, landing artwork and the named illustration
5757
functions inside empty-state components. Their surrounding product controls remain in scope.
58-
The policy does not treat an arbitrary wrapper around an image as media. A wrapper's custom padding or layout still qualifies.
58+
A passive `div`, `span` or `figure` containing only native media is also exempt. Mixed control/media layouts, event handlers, roles, spreads and unknown children prevent this exemption.
5959
CSS asset URL substitutions and generated copy are exempt. Source-only analysis cannot
6060
reliably identify every project-specific media wrapper.
6161

@@ -103,10 +103,9 @@ an Icon does not break resolution of Button through the same barrel.
103103

104104
## Report contract
105105

106-
Schema **3.0.0**, engine **0.5.2**, policy **5.0.0**. The schema remains compatible; the policy
107-
meaning changes. Readers must inspect versions when comparing historical qualification rates.
106+
Schema **3.0.0**, engine **0.6.0**, policy **5.0.0**. The schema and notification policy remain compatible. Readers must inspect versions when comparing historical qualification rates.
108107
All decisions and identifiers are deterministic for the same engine/configuration and commits.
109-
Execution timing and peak memory are recorded separately by the benchmark, never in engine JSON.
108+
Execution timing and peak memory are recorded separately with `--metrics`, never in engine JSON.
110109

111110
Each top-level finding groups evidence by **changed source file**. A shared Button edit produces
112111
one group, rather than one notification per use. Fields include:
@@ -142,7 +141,10 @@ inputs retain diagnostic evidence. Lockfile rendering-dependency changes retain
142141
design-diff.config.json Repository scope, themes and conventions
143142
.github/workflows/design-review.yml Trusted cloud execution; artifact only
144143
scripts/design-diff/
145-
cli.ts, index.ts Entry points and operational status
144+
cli.ts, index-cli.ts, index.ts Entry points and operational status
145+
store.ts, lazy-source.ts SQLite facts and lazy bounded source reads
146+
metrics.ts Separate stage timings and cache counters
147+
properties.ts Literal nested-property projections
146148
analyze.ts, git.ts, source.ts Git snapshots and affected-source analysis
147149
dependencies.ts Binding graph and partial usage counts
148150
extract/{tsx,css,documents,assets}.ts Syntax extraction
@@ -209,3 +211,87 @@ The runner verifies exact commits and GitHub file sets. Its cache identity inclu
209211
engine SHA, configuration, lockfile, runtime and comparison commits. It records elapsed time,
210212
peak RSS, report size and failures separately. `/usr/bin/time` and Bun 1.4.1 are required. The default
211213
per-comparison deadline is 900 seconds; failed cases remain explicit failures in rate reporting.
214+
215+
216+
## Incremental index (schema 1)
217+
218+
```sh
219+
bun run design:index --ref origin/staging --cache-dir /tmp/sim-design-index
220+
bun run design:diff --base origin/staging --head HEAD \
221+
--cache-dir /tmp/sim-design-index --output /tmp/report.json --metrics /tmp/metrics.json
222+
bun run design:diff --base origin/staging --head HEAD --no-cache --output /tmp/fresh.json
223+
```
224+
225+
`--no-cache` bypasses disk storage. Both modes use the same analyzer and bounded in-process facts;
226+
there is no completed-report cache in either CLI. Cache files are disposable and should stay outside
227+
source control. `design:index --identity` emits the compatible trusted-tooling identity for CI.
228+
229+
SQLite stores three kinds of derived data:
230+
231+
| Layer | Contents | Validity |
232+
| --- | --- | --- |
233+
| File facts | Module definitions, import/export specifiers, raw references, property projections and parse failures | Git blob, source path/language and trusted tooling identity |
234+
| Revision model | Immutable commit inventory, resolved edges and resolution limitations; on-demand binding summaries | Routing configuration and observed source identities |
235+
| Appearance queries | Normalized ordered styles/variants, ownership, locations and evidence | Observed source blobs plus the affected-context probes actually consulted |
236+
237+
Parser trees remain temporary. Source text has a bounded 96 MiB LRU per revision and serialized
238+
facts have a 64 MiB LRU. Blob reads are lazy and batched. The persistent identity includes index
239+
schema, implementation hashes, trusted dependency lockfile, config, Bun version, OS and architecture.
240+
File paths are part of fact keys because locations are path-dependent; a rename safely recomputes
241+
those facts. Import routing includes the file inventory and project package/alias configuration,
242+
so added files and formerly missing imports invalidate derived queries. CSS theme dependencies and
243+
comparison-specific resolver context are tracked even when an in-process helper was already cached.
244+
245+
Literal exported object properties are projected independently, including nested selectors. Changes
246+
to an unrelated sibling do not propagate into a control that reads another property. Spreads, dynamic
247+
keys, aliases that escape, mutation and changed local helpers retain conservative candidate traversal.
248+
Supported equivalent values still compare cleanly. Cache invalidation is deliberately coarser than
249+
notification decisions: an observed module edit may recompute a query even when its selected appearance
250+
value ultimately stays unchanged. This is not a complete JavaScript type checker or runtime UI model.
251+
252+
Complete snapshots retain the most recent ten commits. Inventory distinguishes indexed source,
253+
intentional exclusions, unsupported formats and unreadable inputs; “indexed” records supported source
254+
coverage, not proof that every runtime expression was resolved. Appearance and usage facts are computed
255+
on demand. The default storage budget is 1 GiB; eviction discards reusable work, not source analysis.
256+
Writes use transactions and checksums. Closing checkpoints WAL; oversized stores are reclaimed.
257+
Corrupt/incompatible databases rebuild, and lock/unavailable-storage failures fall back to transient
258+
analysis. Source/history failures still fail analysis explicitly. The warming command fails if it
259+
cannot persist a baseline. Independent PR jobs never share writable database files.
260+
261+
This follows the disposable persisted-analysis pattern used by
262+
[TypeScript](https://www.typescriptlang.org/tsconfig/incremental.html), dependency-aware lazy queries in
263+
[rust-analyzer](https://rust-analyzer.github.io/book/contributing/architecture.html), and dependency
264+
selection in [Chromatic TurboSnap](https://www.chromatic.com/docs/turbosnap/). It uses
265+
[Bun SQLite](https://bun.com/docs/runtime/sqlite); it does not use those products to render Sim.
266+
267+
## Index maintenance and performance acceptance
268+
269+
`design-index.yml` runs on default-branch manual dispatch, main pushes and a 30-minute schedule.
270+
It refreshes staging using the trusted default-branch engine. Its checkpointed cache key includes
271+
compatible tooling identity, staging SHA, run ID and attempt. PR jobs use only `actions/cache/restore`
272+
and update a disposable local copy. They never save to the shared baseline. GitHub's
273+
[cache restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
274+
remain in force. Unavailable or stale caches must not change reports. Both production workflows
275+
require activation on main. Set `DESIGN_DIFF_INDEX_ENABLED=true` to opt into warming/restoring;
276+
keep it unset if correctness or measured performance acceptance fails.
277+
278+
Reproduce paired measurements, always starting a PR with a baseline-only cache:
279+
280+
```sh
281+
bun --no-env-file scripts/design-diff/benchmark.ts \
282+
--engine /path/to/clean/checkout --sha <immutable-commit> \
283+
--manifest /path/to/frozen-manifest.json --output /tmp/design-index-evaluation \
284+
--workers 2 --profile-index --no-results-cache
285+
```
286+
287+
Each case runs uncached analysis, separately indexes its merge-base in a new cache, then analyzes
288+
its previously unprocessed head. It requires byte-identical reports and deletes that case's disposable
289+
index afterward. Cold/warm/setup metrics, Git read counters, parsing, resolution, comparison, peak
290+
memory and cache hits remain separate files. Stage times are inclusive and must not be summed.
291+
The runtime target is at least 3x median improvement on the same hardware and concurrency; setup
292+
cost and tail latency must be reported separately. The worker cap reserves roughly 3 GiB per worker
293+
and respects available CPU capacity. `--cache-dir` supports ordinary shared local benchmark reuse;
294+
`--profile-index` deliberately uses isolated baseline-only caches instead.
295+
296+
The frozen 180 cases remain development data. Preserve their incomplete/provisional policy-5 labels;
297+
review every changed decision with source evidence and make no claim of unseen accuracy.

0 commit comments

Comments
 (0)