feat: TypeScript views generated from JSON lens files via panproto The codegen pipeline now reads packages/lenses/*.lens.json as the single source of truth for DB projection transforms. Each lens file contains both structural transforms (remove_field, rename_field, add_field) and DDL metadata (table section). The pipeline: 1. Load human-readable lens JSON files 2. Convert steps to panproto combinators → ProtolensChain 3. Instantiate chain against Lexicon schema → target schema 4. Walk target schema vertices to emit TypeScript interfaces RecordConfig is kept as a fallback but no longer drives TypeScript generation. All 15 view types are now generated from lens files.

Author: Aaron Steven White
Commit 5320e288d1721f915ec574f74dc6566186d43f27
Parent: 9e088dce24
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
18 files changed +493 -259
@@ -1,137 +1,16 @@
1-// Auto-generated by cospan-codegen via panproto protolens combinators.
2-// Source: Lexicon schemas transformed through DB projection lens.
1+// Auto-generated by cospan-codegen via panproto protolens (from JSON lens files).
2+// Source: packages/lenses/*.lens.json
33 // Do not edit manually.
44 
5-// dev.cospan.node (via panproto combinators)
6-export interface NodeView {
7-  did: string;
8-  rkey: string;
9-  createdAt: string;
10-  publicEndpoint: string | null;
11-  indexedAt: string;
12-}
13-
14-export function normalizeNodeView(raw: Partial<NodeView>): NodeView {
15-  return {
16-    did: raw.did ?? '',
17-    rkey: raw.rkey ?? '',
18-    createdAt: raw.createdAt ?? '',
19-    publicEndpoint: raw.publicEndpoint ?? null,
20-    indexedAt: raw.indexedAt ?? '',
21-  };
22-}
23-
24-// dev.cospan.actor.profile (via panproto combinators)
25-export interface ActorProfileView {
26-  did: string;
27-  bluesky: string;
28-  description: string | null;
29-  displayName: string | null;
30-  avatarCid: string;
31-  indexedAt: string;
32-}
33-
34-export function normalizeActorProfileView(raw: Partial<ActorProfileView>): ActorProfileView {
35-  return {
36-    did: raw.did ?? '',
37-    bluesky: raw.bluesky ?? '',
38-    description: raw.description ?? null,
39-    displayName: raw.displayName ?? null,
40-    avatarCid: raw.avatarCid ?? '',
41-    indexedAt: raw.indexedAt ?? '',
42-  };
43-}
44-
45-// dev.cospan.repo (via panproto combinators)
46-export interface RepoView {
47-  did: string;
48-  rkey: string;
49-  createdAt: string;
50-  description: string | null;
51-  name: string;
52-  protocol: string;
53-  sourceRepo: string | null;
54-  defaultBranch: string;
55-  forkCount: number;
56-  nodeDid: string;
57-  nodeUrl: string;
58-  openIssueCount: number;
59-  openMrCount: number;
60-  source: string;
61-  sourceUri: string;
62-  starCount: number;
63-  visibility: string;
64-  indexedAt: string;
65-}
66-
67-export function normalizeRepoView(raw: Partial<RepoView>): RepoView {
68-  return {
69-    did: raw.did ?? '',
70-    rkey: raw.rkey ?? '',
71-    createdAt: raw.createdAt ?? '',
72-    description: raw.description ?? null,
73-    name: raw.name ?? '',
74-    protocol: raw.protocol ?? '',
75-    sourceRepo: raw.sourceRepo ?? null,
76-    defaultBranch: raw.defaultBranch ?? '',
77-    forkCount: raw.forkCount ?? 0,
78-    nodeDid: raw.nodeDid ?? '',
79-    nodeUrl: raw.nodeUrl ?? '',
80-    openIssueCount: raw.openIssueCount ?? 0,
81-    openMrCount: raw.openMrCount ?? 0,
82-    source: raw.source ?? '',
83-    sourceUri: raw.sourceUri ?? '',
84-    starCount: raw.starCount ?? 0,
85-    visibility: raw.visibility ?? 'public',
86-    indexedAt: raw.indexedAt ?? '',
87-  };
88-}
89-
90-// dev.cospan.vcs.refUpdate (via panproto combinators)
91-export interface RefUpdateView {
92-  rkey: string;
93-  committerDid: string;
94-  createdAt: string;
95-  lensId: string | null;
96-  migrationId: string | null;
97-  newTarget: string;
98-  oldTarget: string | null;
99-  protocol: string;
100-  ref: string;
101-  breakingChangeCount: number;
102-  commitCount: number;
103-  lensQuality: number;
104-  repoDid: string;
105-  repoName: string;
106-  indexedAt: string;
107-}
108-
109-export function normalizeRefUpdateView(raw: Partial<RefUpdateView>): RefUpdateView {
110-  return {
111-    rkey: raw.rkey ?? '',
112-    committerDid: raw.committerDid ?? '',
113-    createdAt: raw.createdAt ?? '',
114-    lensId: raw.lensId ?? null,
115-    migrationId: raw.migrationId ?? null,
116-    newTarget: raw.newTarget ?? '',
117-    oldTarget: raw.oldTarget ?? null,
118-    protocol: raw.protocol ?? '',
119-    ref: raw.ref ?? '',
120-    breakingChangeCount: raw.breakingChangeCount ?? 0,
121-    commitCount: raw.commitCount ?? 0,
122-    lensQuality: raw.lensQuality ?? 0,
123-    repoDid: raw.repoDid ?? '',
124-    repoName: raw.repoName ?? '',
125-    indexedAt: raw.indexedAt ?? '',
126-  };
127-}
128-
129-// dev.cospan.repo.issue (via panproto combinators)
130-export interface IssueView {
5+// dev.cospan.repo.pull (via lens file)
6+export interface PullView {
1317   did: string;
1328   rkey: string;
1339   body: string | null;
13410   createdAt: string;
11+  sourceRef: string;
12+  sourceRepo: string | null;
13+  targetRef: string;
13514   title: string;
13615   commentCount: number;
13716   repoDid: string;
@@ -140,12 +19,15 @@ export interface IssueView {
14019   indexedAt: string;
14120 }
14221 
143-export function normalizeIssueView(raw: Partial<IssueView>): IssueView {
22+export function normalizePullView(raw: Partial<PullView>): PullView {
14423   return {
14524     did: raw.did ?? '',
14625     rkey: raw.rkey ?? '',
14726     body: raw.body ?? null,
14827     createdAt: raw.createdAt ?? '',
28+    sourceRef: raw.sourceRef ?? '',
29+    sourceRepo: raw.sourceRepo ?? null,
30+    targetRef: raw.targetRef ?? '',
14931     title: raw.title ?? '',
15032     commentCount: raw.commentCount ?? 0,
15133     repoDid: raw.repoDid ?? '',
@@ -155,30 +37,12 @@ export function normalizeIssueView(raw: Partial<IssueView>): IssueView {
15537   };
15638 }
15739 
158-// dev.cospan.repo.issue.comment (via panproto combinators)
159-export interface IssueCommentView {
160-  did: string;
161-  rkey: string;
162-  body: string;
163-  createdAt: string;
164-  indexedAt: string;
165-}
166-
167-export function normalizeIssueCommentView(raw: Partial<IssueCommentView>): IssueCommentView {
168-  return {
169-    did: raw.did ?? '',
170-    rkey: raw.rkey ?? '',
171-    body: raw.body ?? '',
172-    createdAt: raw.createdAt ?? '',
173-    indexedAt: raw.indexedAt ?? '',
174-  };
175-}
176-
177-// dev.cospan.repo.issue.state (via panproto combinators)
40+// dev.cospan.repo.issue.state (via lens file)
17841 export interface IssueStateView {
17942   did: string;
18043   rkey: string;
18144   createdAt: string;
45+  issueUri: string | null;
18246   reason: string | null;
18347   state: string;
18448   indexedAt: string;
@@ -189,21 +53,19 @@ export function normalizeIssueStateView(raw: Partial<IssueStateView>): IssueStat
18953     did: raw.did ?? '',
19054     rkey: raw.rkey ?? '',
19155     createdAt: raw.createdAt ?? '',
56+    issueUri: raw.issueUri ?? null,
19257     reason: raw.reason ?? null,
19358     state: raw.state ?? '',
19459     indexedAt: raw.indexedAt ?? '',
19560   };
19661 }
19762 
198-// dev.cospan.repo.pull (via panproto combinators)
199-export interface PullView {
63+// dev.cospan.repo.issue (via lens file)
64+export interface IssueView {
20065   did: string;
20166   rkey: string;
20267   body: string | null;
20368   createdAt: string;
204-  sourceRef: string;
205-  sourceRepo: string | null;
206-  targetRef: string;
20769   title: string;
20870   commentCount: number;
20971   repoDid: string;
@@ -212,15 +74,12 @@ export interface PullView {
21274   indexedAt: string;
21375 }
21476 
215-export function normalizePullView(raw: Partial<PullView>): PullView {
77+export function normalizeIssueView(raw: Partial<IssueView>): IssueView {
21678   return {
21779     did: raw.did ?? '',
21880     rkey: raw.rkey ?? '',
21981     body: raw.body ?? null,
22082     createdAt: raw.createdAt ?? '',
221-    sourceRef: raw.sourceRef ?? '',
222-    sourceRepo: raw.sourceRepo ?? null,
223-    targetRef: raw.targetRef ?? '',
22483     title: raw.title ?? '',
22584     commentCount: raw.commentCount ?? 0,
22685     repoDid: raw.repoDid ?? '',
@@ -230,108 +89,96 @@ export function normalizePullView(raw: Partial<PullView>): PullView {
23089   };
23190 }
23291 
233-// dev.cospan.repo.pull.comment (via panproto combinators)
234-export interface PullCommentView {
92+// dev.cospan.repo.issue.comment (via lens file)
93+export interface IssueCommentView {
23594   did: string;
23695   rkey: string;
23796   body: string;
23897   createdAt: string;
239-  reviewDecision: string | null;
98+  issueUri: string | null;
24099   indexedAt: string;
241100 }
242101 
243-export function normalizePullCommentView(raw: Partial<PullCommentView>): PullCommentView {
102+export function normalizeIssueCommentView(raw: Partial<IssueCommentView>): IssueCommentView {
244103   return {
245104     did: raw.did ?? '',
246105     rkey: raw.rkey ?? '',
247106     body: raw.body ?? '',
248107     createdAt: raw.createdAt ?? '',
249-    reviewDecision: raw.reviewDecision ?? null,
250-    indexedAt: raw.indexedAt ?? '',
251-  };
252-}
253-
254-// dev.cospan.repo.pull.state (via panproto combinators)
255-export interface PullStateView {
256-  did: string;
257-  rkey: string;
258-  createdAt: string;
259-  mergeCommitId: string | null;
260-  state: string;
261-  indexedAt: string;
262-}
263-
264-export function normalizePullStateView(raw: Partial<PullStateView>): PullStateView {
265-  return {
266-    did: raw.did ?? '',
267-    rkey: raw.rkey ?? '',
268-    createdAt: raw.createdAt ?? '',
269-    mergeCommitId: raw.mergeCommitId ?? null,
270-    state: raw.state ?? '',
108+    issueUri: raw.issueUri ?? null,
271109     indexedAt: raw.indexedAt ?? '',
272110   };
273111 }
274112 
275-// dev.cospan.feed.star (via panproto combinators)
276-export interface StarView {
277-  did: string;
278-  rkey: string;
279-  createdAt: string;
280-  subject: string;
281-  indexedAt: string;
282-}
283-
284-export function normalizeStarView(raw: Partial<StarView>): StarView {
285-  return {
286-    did: raw.did ?? '',
287-    rkey: raw.rkey ?? '',
288-    createdAt: raw.createdAt ?? '',
289-    subject: raw.subject ?? '',
290-    indexedAt: raw.indexedAt ?? '',
291-  };
292-}
293-
294-// dev.cospan.feed.reaction (via panproto combinators)
295-export interface ReactionView {
113+// dev.cospan.org (via lens file)
114+export interface OrgView {
296115   did: string;
297116   rkey: string;
298117   createdAt: string;
299-  emoji: string;
300-  subject: string;
118+  description: string | null;
119+  name: string;
120+  avatarCid: string;
301121   indexedAt: string;
302122 }
303123 
304-export function normalizeReactionView(raw: Partial<ReactionView>): ReactionView {
124+export function normalizeOrgView(raw: Partial<OrgView>): OrgView {
305125   return {
306126     did: raw.did ?? '',
307127     rkey: raw.rkey ?? '',
308128     createdAt: raw.createdAt ?? '',
309-    emoji: raw.emoji ?? '',
310-    subject: raw.subject ?? '',
129+    description: raw.description ?? null,
130+    name: raw.name ?? '',
131+    avatarCid: raw.avatarCid ?? '',
311132     indexedAt: raw.indexedAt ?? '',
312133   };
313134 }
314135 
315-// dev.cospan.graph.follow (via panproto combinators)
316-export interface FollowView {
136+// dev.cospan.repo (via lens file)
137+export interface RepoView {
317138   did: string;
318139   rkey: string;
319140   createdAt: string;
320-  subject: string;
141+  defaultBranch: string | null;
142+  description: string | null;
143+  name: string;
144+  protocol: string;
145+  sourceRepo: string | null;
146+  visibility: string | null;
147+  forkCount: number;
148+  nodeDid: string;
149+  nodeUrl: string;
150+  openIssueCount: number;
151+  openMrCount: number;
152+  source: string;
153+  sourceUri: string;
154+  starCount: number;
321155   indexedAt: string;
322156 }
323157 
324-export function normalizeFollowView(raw: Partial<FollowView>): FollowView {
158+export function normalizeRepoView(raw: Partial<RepoView>): RepoView {
325159   return {
326160     did: raw.did ?? '',
327161     rkey: raw.rkey ?? '',
328162     createdAt: raw.createdAt ?? '',
329-    subject: raw.subject ?? '',
163+    defaultBranch: raw.defaultBranch ?? null,
164+    description: raw.description ?? null,
165+    name: raw.name ?? '',
166+    protocol: raw.protocol ?? '',
167+    sourceRepo: raw.sourceRepo ?? null,
168+    visibility: raw.visibility ?? null,
169+    forkCount: raw.forkCount ?? 0,
170+    nodeDid: raw.nodeDid ?? '',
171+    nodeUrl: raw.nodeUrl ?? '',
172+    openIssueCount: raw.openIssueCount ?? 0,
173+    openMrCount: raw.openMrCount ?? 0,
174+    source: raw.source ?? '',
175+    sourceUri: raw.sourceUri ?? '',
176+    starCount: raw.starCount ?? 0,
330177     indexedAt: raw.indexedAt ?? '',
331178   };
332179 }
333180 
334-// dev.cospan.label.definition (via panproto combinators)
181+// dev.cospan.label.definition (via lens file)
335182 export interface LabelView {
336183   did: string;
337184   rkey: string;
@@ -358,53 +205,56 @@ export function normalizeLabelView(raw: Partial<LabelView>): LabelView {
358205   };
359206 }
360207 
361-// dev.cospan.org (via panproto combinators)
362-export interface OrgView {
208+// dev.cospan.org.member (via lens file)
209+export interface OrgMemberView {
363210   did: string;
364211   rkey: string;
365212   createdAt: string;
366-  description: string | null;
367-  name: string;
368-  avatarCid: string;
213+  memberDid: string | null;
214+  orgUri: string | null;
215+  role: string;
369216   indexedAt: string;
370217 }
371218 
372-export function normalizeOrgView(raw: Partial<OrgView>): OrgView {
219+export function normalizeOrgMemberView(raw: Partial<OrgMemberView>): OrgMemberView {
373220   return {
374221     did: raw.did ?? '',
375222     rkey: raw.rkey ?? '',
376223     createdAt: raw.createdAt ?? '',
377-    description: raw.description ?? null,
378-    name: raw.name ?? '',
379-    avatarCid: raw.avatarCid ?? '',
224+    memberDid: raw.memberDid ?? null,
225+    orgUri: raw.orgUri ?? null,
226+    role: raw.role ?? '',
380227     indexedAt: raw.indexedAt ?? '',
381228   };
382229 }
383230 
384-// dev.cospan.org.member (via panproto combinators)
385-export interface OrgMemberView {
231+// dev.cospan.actor.profile (via lens file)
232+export interface ActorProfileView {
386233   did: string;
387-  rkey: string;
388-  createdAt: string;
389-  role: string;
234+  bluesky: string;
235+  description: string | null;
236+  displayName: string | null;
237+  avatarCid: string;
390238   indexedAt: string;
391239 }
392240 
393-export function normalizeOrgMemberView(raw: Partial<OrgMemberView>): OrgMemberView {
241+export function normalizeActorProfileView(raw: Partial<ActorProfileView>): ActorProfileView {
394242   return {
395243     did: raw.did ?? '',
396-    rkey: raw.rkey ?? '',
397-    createdAt: raw.createdAt ?? '',
398-    role: raw.role ?? '',
244+    bluesky: raw.bluesky ?? '',
245+    description: raw.description ?? null,
246+    displayName: raw.displayName ?? null,
247+    avatarCid: raw.avatarCid ?? '',
399248     indexedAt: raw.indexedAt ?? '',
400249   };
401250 }
402251 
403-// dev.cospan.repo.collaborator (via panproto combinators)
252+// dev.cospan.repo.collaborator (via lens file)
404253 export interface CollaboratorView {
405254   did: string;
406255   rkey: string;
407256   createdAt: string;
257+  did: string;
408258   role: string;
409259   repoDid: string;
410260   repoName: string;
@@ -416,6 +266,7 @@ export function normalizeCollaboratorView(raw: Partial<CollaboratorView>): Colla
416266     did: raw.did ?? '',
417267     rkey: raw.rkey ?? '',
418268     createdAt: raw.createdAt ?? '',
269+    did: raw.did ?? '',
419270     role: raw.role ?? '',
420271     repoDid: raw.repoDid ?? '',
421272     repoName: raw.repoName ?? '',
@@ -423,7 +274,7 @@ export function normalizeCollaboratorView(raw: Partial<CollaboratorView>): Colla
423274   };
424275 }
425276 
426-// dev.cospan.repo.dependency (via panproto combinators)
277+// dev.cospan.repo.dependency (via lens file)
427278 export interface DependencyView {
428279   did: string;
429280   rkey: string;
@@ -456,7 +307,92 @@ export function normalizeDependencyView(raw: Partial<DependencyView>): Dependenc
456307   };
457308 }
458309 
459-// dev.cospan.pipeline (via panproto combinators)
310+// dev.cospan.vcs.refUpdate (via lens file)
311+export interface RefUpdateView {
312+  rkey: string;
313+  commitCount: number | null;
314+  committerDid: string;
315+  createdAt: string;
316+  lensId: string | null;
317+  lensQuality: Record<string, unknown> | null;
318+  migrationId: string | null;
319+  newTarget: string;
320+  oldTarget: string | null;
321+  protocol: string;
322+  ref: string;
323+  breakingChangeCount: number;
324+  repoDid: string;
325+  repoName: string;
326+  indexedAt: string;
327+}
328+
329+export function normalizeRefUpdateView(raw: Partial<RefUpdateView>): RefUpdateView {
330+  return {
331+    rkey: raw.rkey ?? '',
332+    commitCount: raw.commitCount ?? null,
333+    committerDid: raw.committerDid ?? '',
334+    createdAt: raw.createdAt ?? '',
335+    lensId: raw.lensId ?? null,
336+    lensQuality: raw.lensQuality ?? null,
337+    migrationId: raw.migrationId ?? null,
338+    newTarget: raw.newTarget ?? '',
339+    oldTarget: raw.oldTarget ?? null,
340+    protocol: raw.protocol ?? '',
341+    ref: raw.ref ?? '',
342+    breakingChangeCount: raw.breakingChangeCount ?? 0,
343+    repoDid: raw.repoDid ?? '',
344+    repoName: raw.repoName ?? '',
345+    indexedAt: raw.indexedAt ?? '',
346+  };
347+}
348+
349+// dev.cospan.repo.pull.state (via lens file)
350+export interface PullStateView {
351+  did: string;
352+  rkey: string;
353+  createdAt: string;
354+  mergeCommitId: string | null;
355+  pullUri: string | null;
356+  state: string;
357+  indexedAt: string;
358+}
359+
360+export function normalizePullStateView(raw: Partial<PullStateView>): PullStateView {
361+  return {
362+    did: raw.did ?? '',
363+    rkey: raw.rkey ?? '',
364+    createdAt: raw.createdAt ?? '',
365+    mergeCommitId: raw.mergeCommitId ?? null,
366+    pullUri: raw.pullUri ?? null,
367+    state: raw.state ?? '',
368+    indexedAt: raw.indexedAt ?? '',
369+  };
370+}
371+
372+// dev.cospan.repo.pull.comment (via lens file)
373+export interface PullCommentView {
374+  did: string;
375+  rkey: string;
376+  body: string;
377+  createdAt: string;
378+  pullUri: string | null;
379+  reviewDecision: string | null;
380+  indexedAt: string;
381+}
382+
383+export function normalizePullCommentView(raw: Partial<PullCommentView>): PullCommentView {
384+  return {
385+    did: raw.did ?? '',
386+    rkey: raw.rkey ?? '',
387+    body: raw.body ?? '',
388+    createdAt: raw.createdAt ?? '',
389+    pullUri: raw.pullUri ?? null,
390+    reviewDecision: raw.reviewDecision ?? null,
391+    indexedAt: raw.indexedAt ?? '',
392+  };
393+}
394+
395+// dev.cospan.pipeline (via lens file)
460396 export interface PipelineView {
461397   did: string;
462398   rkey: string;
@@ -465,6 +401,7 @@ export interface PipelineView {
465401   createdAt: string;
466402   ref: string | null;
467403   status: string;
404+  workflows: unknown[] | null;
468405   breakingChangeCheck: string;
469406   equationVerification: string;
470407   gatTypeCheck: string;
@@ -483,6 +420,7 @@ export function normalizePipelineView(raw: Partial<PipelineView>): PipelineView
483420     createdAt: raw.createdAt ?? '',
484421     ref: raw.ref ?? null,
485422     status: raw.status ?? 'pending',
423+    workflows: raw.workflows ?? null,
486424     breakingChangeCheck: raw.breakingChangeCheck ?? '',
487425     equationVerification: raw.equationVerification ?? '',
488426     gatTypeCheck: raw.gatTypeCheck ?? '',
@@ -238,13 +238,192 @@ fn emit_view_from_target(target: &Schema, nsid: &str, config: &RecordConfig) ->
238238     out
239239 }
240240 
241+/// Emit TypeScript from target schema with explicit config (lens-file path).
242+fn emit_view_from_target_with(
243+    target: &Schema,
244+    nsid: &str,
245+    view_name: &str,
246+    include_did: bool,
247+    include_rkey: bool,
248+    column_defaults: &std::collections::HashMap<String, String>,
249+) -> String {
250+    let body_id = find_record_body(target, nsid);
251+    let mut out = String::new();
252+    out.push_str(&format!("// {nsid} (via lens file)\n"));
253+    out.push_str(&format!("export interface {view_name} {{\n"));
254+
255+    if include_did { out.push_str("  did: string;\n"); }
256+    if include_rkey { out.push_str("  rkey: string;\n"); }
257+
258+    let props = children_by_edge(target, &body_id, "prop");
259+    for (edge, prop_vertex) in &props {
260+        let field_name = edge.name.as_ref().map(|n| n.as_str()).unwrap_or("unknown");
261+        let ts_type = vertex_kind_to_ts(&prop_vertex.kind);
262+        let is_required = is_field_required(target, &body_id, field_name);
263+        if is_required {
264+            out.push_str(&format!("  {field_name}: {ts_type};\n"));
265+        } else {
266+            out.push_str(&format!("  {field_name}: {ts_type} | null;\n"));
267+        }
268+    }
269+
270+    let body_prefix = format!("{body_id}.");
271+    let prop_targets: std::collections::HashSet<String> =
272+        props.iter().map(|(_, v)| v.id.to_string()).collect();
273+    let mut extra_vertices: Vec<_> = target
274+        .vertices
275+        .iter()
276+        .filter(|(id, _)| {
277+            let id_str = id.to_string();
278+            id_str.starts_with(&body_prefix)
279+                && !id_str[body_prefix.len()..].contains('.')
280+                && !id_str[body_prefix.len()..].contains(':')
281+                && !prop_targets.contains(&id_str)
282+        })
283+        .collect();
284+    extra_vertices.sort_by_key(|(id, _)| id.to_string());
285+
286+    for (id, v) in &extra_vertices {
287+        let field_name = id.as_str().strip_prefix(&body_prefix).unwrap_or(id.as_str());
288+        let ts_type = vertex_kind_to_ts(&v.kind);
289+        out.push_str(&format!("  {field_name}: {ts_type};\n"));
290+    }
291+
292+    out.push_str("  indexedAt: string;\n");
293+    out.push_str("}\n\n");
294+
295+    // Normalization
296+    out.push_str(&format!("export function normalize{view_name}(raw: Partial<{view_name}>): {view_name} {{\n"));
297+    out.push_str("  return {\n");
298+    if include_did { out.push_str("    did: raw.did ?? '',\n"); }
299+    if include_rkey { out.push_str("    rkey: raw.rkey ?? '',\n"); }
300+    for (edge, prop_vertex) in &props {
301+        let field_name = edge.name.as_ref().map(|n| n.as_str()).unwrap_or("unknown");
302+        let is_required = is_field_required(target, &body_id, field_name);
303+        let default = if !is_required { "null" } else {
304+            default_for_kind_with_map(&prop_vertex.kind, field_name, column_defaults)
305+        };
306+        out.push_str(&format!("    {field_name}: raw.{field_name} ?? {default},\n"));
307+    }
308+    for (id, v) in &extra_vertices {
309+        let field_name = id.as_str().strip_prefix(&body_prefix).unwrap_or(id.as_str());
310+        let default = default_for_kind_with_map(&v.kind, field_name, column_defaults);
311+        out.push_str(&format!("    {field_name}: raw.{field_name} ?? {default},\n"));
312+    }
313+    out.push_str("    indexedAt: raw.indexedAt ?? '',\n");
314+    out.push_str("  };\n");
315+    out.push_str("}\n\n");
316+
317+    out
318+}
319+
320+fn default_for_kind_with_map(kind: &Name, field_name: &str, defaults: &std::collections::HashMap<String, String>) -> &'static str {
321+    if let Some(expr) = defaults.get(field_name) {
322+        return match expr.as_str() {
323+            "'open'" => "'open'",
324+            "'pending'" => "'pending'",
325+            "'public'" => "'public'",
326+            "'main'" => "'main'",
327+            "0" => "0",
328+            _ => "''",
329+        };
330+    }
331+    match kind.as_str() {
332+        "string" | "cid-link" | "ref" | "token" | "bytes" => "''",
333+        "integer" | "number" | "float" => "0",
334+        "boolean" => "false",
335+        _ => "''",
336+    }
337+}
338+
339+fn nsid_to_pascal(nsid: &str) -> String {
340+    nsid.split('.').map(|s| {
341+        let mut c = s.chars();
342+        match c.next() {
343+            None => String::new(),
344+            Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
345+        }
346+    }).collect()
347+}
348+
241349 fn emit_list_response(type_name: &str, wrapper_key: &str) -> String {
242350     format!(
243351         "export interface {type_name}ListResponse {{\n  {wrapper_key}: {type_name}View[];\n  cursor: string | null;\n}}\n\n"
244352     )
245353 }
246354 
247-/// Emit the full generated TypeScript views file.
355+/// Emit views from JSON lens files (primary path).
356+pub fn emit_all_views_from_lenses(
357+    schemas: &[(Schema, String)],
358+    lenses: &[crate::lens_config::LensFile],
359+) -> String {
360+    let mut out = String::new();
361+    out.push_str("// Auto-generated by cospan-codegen via panproto protolens (from JSON lens files).\n");
362+    out.push_str("// Source: packages/lenses/*.lens.json\n");
363+    out.push_str("// Do not edit manually.\n\n");
364+
365+    for lens in crate::lens_config::db_projection_lenses(lenses) {
366+        if let Some((schema, _)) = schemas.iter().find(|(_, nsid)| nsid == &lens.source) {
367+            let body_id = find_record_body(schema, &lens.source);
368+            let chain = crate::lens_config::steps_to_protolens_chain(&lens.steps, &body_id);
369+            let protocol = Protocol::default();
370+            let target = match chain.instantiate(schema, &protocol) {
371+                Ok(l) => l.tgt_schema,
372+                Err(e) => {
373+                    eprintln!("  warn: lens instantiation for {}: {e:?}", lens.source);
374+                    schema.clone()
375+                }
376+            };
377+
378+            let table = lens.table.as_ref();
379+            let view_name = table
380+                .map(|t| {
381+                    t.row_struct
382+                        .strip_suffix("Row")
383+                        .unwrap_or(&t.row_struct)
384+                        .to_string()
385+                        + "View"
386+                })
387+                .unwrap_or_else(|| format!("{}View", nsid_to_pascal(&lens.source)));
388+            let include_did = table.map(|t| t.include_did).unwrap_or(true);
389+            let include_rkey = table.map(|t| t.include_rkey).unwrap_or(true);
390+
391+            out.push_str(&emit_view_from_target_with(
392+                &target,
393+                &lens.source,
394+                &view_name,
395+                include_did,
396+                include_rkey,
397+                table.map(|t| &t.column_defaults).unwrap_or(&std::collections::HashMap::new()),
398+            ));
399+        }
400+    }
401+
402+    let list_endpoints = [
403+        ("Repo", "repos"),
404+        ("Issue", "issues"),
405+        ("IssueComment", "comments"),
406+        ("Pull", "pulls"),
407+        ("PullComment", "comments"),
408+        ("Star", "stars"),
409+        ("Follow", "follows"),
410+        ("Node", "nodes"),
411+        ("Org", "orgs"),
412+        ("OrgMember", "members"),
413+        ("Collaborator", "collaborators"),
414+        ("RefUpdate", "refUpdates"),
415+        ("Label", "labels"),
416+        ("Pipeline", "pipelines"),
417+        ("Reaction", "reactions"),
418+    ];
419+    for (type_name, wrapper_key) in &list_endpoints {
420+        out.push_str(&emit_list_response(type_name, wrapper_key));
421+    }
422+
423+    out
424+}
425+
426+/// Emit views from RecordConfig (legacy path, kept for backward compat).
248427 pub fn emit_all_views(schemas: &[(Schema, String)], configs: &[RecordConfig]) -> String {
249428     let mut out = String::new();
250429     out.push_str("// Auto-generated by cospan-codegen via panproto protolens combinators.\n");
@@ -260,26 +260,35 @@ fn main() -> Result<()> {
260260         rmp_serde::to_vec(&db_projections)?,
261261     )?;
262262 
263-    // --- Generate TypeScript view types (API response shapes) ---
263+    // --- Generate TypeScript view types from lens files ---
264264     {
265-        let configs = record_config::all_record_configs();
265+        let lenses_dir = workspace_root.join("packages/lenses");
266+        let lenses = lens_config::load_all_lenses(&lenses_dir)?;
267+
268+        // Collect schemas for all lens source NSIDs
266269         let mut schema_pairs: Vec<(panproto_schema::Schema, String)> = Vec::new();
267-        for config in &configs {
270+        for lens in &lenses {
268271             for lexicon_path in &lexicon_files {
269272                 let json_str = fs::read_to_string(lexicon_path)?;
270273                 let json: serde_json::Value = serde_json::from_str(&json_str)?;
271274                 let nsid = json.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
272-                if nsid == config.nsid {
275+                if nsid == lens.source && !schema_pairs.iter().any(|(_, n)| n == nsid) {
273276                     let schema = atproto::parse_lexicon(&json)?;
274277                     schema_pairs.push((schema, nsid.to_string()));
275278                     break;
276279                 }
277280             }
278281         }
279-        let views_ts = emit_typescript_views::emit_all_views(&schema_pairs, &configs);
282+
283+        // Try lens-file path first, fall back to RecordConfig
284+        let views_ts = if lenses.iter().any(|l| l.table.is_some()) {
285+            emit_typescript_views::emit_all_views_from_lenses(&schema_pairs, &lenses)
286+        } else {
287+            let configs = record_config::all_record_configs();
288+            emit_typescript_views::emit_all_views(&schema_pairs, &configs)
289+        };
280290         fs::write(generated_dir.join("typescript/views.ts"), &views_ts)?;
281291 
282-        // Copy to web app for direct import
283292         let web_gen_dir = workspace_root.join("apps/web/src/lib/generated");
284293         fs::create_dir_all(&web_gen_dir)?;
285294         fs::write(web_gen_dir.join("views.ts"), &views_ts)?;
@@ -15,5 +15,11 @@
1515         "expr": "path_extract(avatar, ['ref', '$link'])"
1616       }
1717     }
18-  ]
18+  ],
19+  "table": {
20+    "name": "actor_profiles",
21+    "row_struct": "ActorProfileRow",
22+    "conflict_keys": ["did"],
23+    "include_rkey": false
24+  }
1925 }
@@ -21,5 +21,10 @@
2121         "expr": "index(split(replace(repo, 'at://', ''), '/'), 2)"
2222       }
2323     }
24-  ]
24+  ],
25+  "table": {
26+    "name": "labels",
27+    "row_struct": "LabelRow",
28+    "conflict_keys": ["did", "rkey"]
29+  }
2530 }
cospan · schematic version control on atproto built on AT Protocol