@sanity/ailf 4.2.0 → 4.3.1

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 (105) hide show
  1. package/config/package-surface.ts +37 -0
  2. package/config/preflight-scoring.ts +26 -0
  3. package/dist/_vendor/ailf-core/artifact-registry.d.ts +1 -1
  4. package/dist/_vendor/ailf-core/artifact-registry.js +47 -0
  5. package/dist/_vendor/ailf-core/config-helpers.d.ts +35 -0
  6. package/dist/_vendor/ailf-core/config-helpers.js +67 -0
  7. package/dist/_vendor/ailf-core/index.d.ts +1 -1
  8. package/dist/_vendor/ailf-core/index.js +1 -1
  9. package/dist/_vendor/ailf-core/ports/context.d.ts +18 -0
  10. package/dist/_vendor/ailf-core/ports/doc-fetcher.d.ts +30 -0
  11. package/dist/_vendor/ailf-core/ports/index.d.ts +3 -1
  12. package/dist/_vendor/ailf-core/ports/index.js +1 -0
  13. package/dist/_vendor/ailf-core/ports/mode-handler.d.ts +23 -0
  14. package/dist/_vendor/ailf-core/ports/package-surface-resolver.d.ts +71 -0
  15. package/dist/_vendor/ailf-core/ports/package-surface-resolver.js +36 -0
  16. package/dist/_vendor/ailf-core/schemas/eval-config.d.ts +6 -0
  17. package/dist/_vendor/ailf-core/schemas/eval-config.js +14 -0
  18. package/dist/_vendor/ailf-core/schemas/index.d.ts +1 -0
  19. package/dist/_vendor/ailf-core/schemas/index.js +1 -0
  20. package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +4 -0
  21. package/dist/_vendor/ailf-core/schemas/pipeline-request.js +7 -0
  22. package/dist/_vendor/ailf-core/schemas/symbol-preflight-report.d.ts +51 -0
  23. package/dist/_vendor/ailf-core/schemas/symbol-preflight-report.js +57 -0
  24. package/dist/_vendor/ailf-core/types/index.d.ts +12 -0
  25. package/dist/_vendor/ailf-core/types/index.js +1 -0
  26. package/dist/_vendor/ailf-core/types/package-surface.d.ts +36 -0
  27. package/dist/_vendor/ailf-core/types/package-surface.js +13 -0
  28. package/dist/_vendor/ailf-core/types/pipeline-request.d.ts +1 -0
  29. package/dist/_vendor/ailf-core/types/preflight-scoring.d.ts +52 -0
  30. package/dist/_vendor/ailf-core/types/preflight-scoring.js +18 -0
  31. package/dist/_vendor/ailf-core/types/repo-config.d.ts +14 -0
  32. package/dist/_vendor/ailf-core/types/symbol-preflight-report.d.ts +66 -0
  33. package/dist/_vendor/ailf-core/types/symbol-preflight-report.js +25 -0
  34. package/dist/adapters/api-client/build-request.d.ts +1 -0
  35. package/dist/adapters/api-client/build-request.js +3 -0
  36. package/dist/adapters/config-sources/file-config-adapter.js +1 -0
  37. package/dist/adapters/doc-fetchers/sanity-doc-fetcher.d.ts +4 -0
  38. package/dist/adapters/doc-fetchers/sanity-doc-fetcher.js +159 -82
  39. package/dist/adapters/index.d.ts +1 -0
  40. package/dist/adapters/index.js +1 -0
  41. package/dist/adapters/package-surface/dts-package-surface.d.ts +46 -0
  42. package/dist/adapters/package-surface/dts-package-surface.js +173 -0
  43. package/dist/adapters/package-surface/in-memory-package-surface.d.ts +15 -0
  44. package/dist/adapters/package-surface/in-memory-package-surface.js +28 -0
  45. package/dist/adapters/package-surface/index.d.ts +9 -0
  46. package/dist/adapters/package-surface/index.js +8 -0
  47. package/dist/adapters/package-surface/parse-dts-exports.d.ts +31 -0
  48. package/dist/adapters/package-surface/parse-dts-exports.js +54 -0
  49. package/dist/adapters/task-sources/repo-schemas.d.ts +6 -0
  50. package/dist/adapters/task-sources/repo-schemas.js +15 -0
  51. package/dist/commands/pipeline-action.d.ts +2 -0
  52. package/dist/commands/pipeline-action.js +12 -0
  53. package/dist/commands/remote-pipeline.js +10 -2
  54. package/dist/commands/remote-results.d.ts +12 -1
  55. package/dist/commands/remote-results.js +25 -5
  56. package/dist/composition-root.js +9 -0
  57. package/dist/config/package-surface.ts +37 -0
  58. package/dist/config/preflight-scoring.ts +26 -0
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +1 -1
  61. package/dist/orchestration/build-app-context.js +1 -0
  62. package/dist/orchestration/pipeline-orchestrator.d.ts +19 -1
  63. package/dist/orchestration/pipeline-orchestrator.js +38 -0
  64. package/dist/orchestration/steps/calculate-scores-step.js +11 -0
  65. package/dist/orchestration/steps/generate-configs-step.js +16 -1
  66. package/dist/orchestration/steps/run-eval-step.js +27 -0
  67. package/dist/pipeline/calculate-scores.d.ts +66 -5
  68. package/dist/pipeline/calculate-scores.js +141 -27
  69. package/dist/pipeline/compiler/index.d.ts +1 -1
  70. package/dist/pipeline/compiler/index.js +1 -1
  71. package/dist/pipeline/compiler/literacy-bridge.d.ts +9 -0
  72. package/dist/pipeline/compiler/literacy-bridge.js +2 -0
  73. package/dist/pipeline/compiler/mode-handlers/literacy/assertions.d.ts +1 -1
  74. package/dist/pipeline/compiler/mode-handlers/literacy/assertions.js +31 -4
  75. package/dist/pipeline/compiler/mode-handlers/literacy/compiler.js +146 -1
  76. package/dist/pipeline/compiler/mode-handlers/literacy/index.js +2 -0
  77. package/dist/pipeline/compiler/mode-handlers/literacy/types.d.ts +17 -2
  78. package/dist/pipeline/compiler/rubric-resolution.d.ts +17 -1
  79. package/dist/pipeline/compiler/rubric-resolution.js +78 -2
  80. package/dist/pipeline/compiler/scoring-bridge.d.ts +49 -2
  81. package/dist/pipeline/compiler/scoring-bridge.js +104 -10
  82. package/dist/pipeline/eval-fingerprint.d.ts +9 -0
  83. package/dist/pipeline/eval-fingerprint.js +7 -1
  84. package/dist/pipeline/map-request-to-config.js +1 -0
  85. package/dist/pipeline/preflight/compute-preflight.d.ts +67 -0
  86. package/dist/pipeline/preflight/compute-preflight.js +118 -0
  87. package/dist/pipeline/preflight/emit-symbol-preflight.d.ts +51 -0
  88. package/dist/pipeline/preflight/emit-symbol-preflight.js +102 -0
  89. package/dist/pipeline/preflight/load-package-surface.d.ts +14 -0
  90. package/dist/pipeline/preflight/load-package-surface.js +19 -0
  91. package/dist/pipeline/preflight/load-preflight-context.d.ts +13 -0
  92. package/dist/pipeline/preflight/load-preflight-context.js +25 -0
  93. package/dist/pipeline/preflight/load-preflight-scoring.d.ts +12 -0
  94. package/dist/pipeline/preflight/load-preflight-scoring.js +17 -0
  95. package/dist/pipeline/preflight/parse-imports.d.ts +62 -0
  96. package/dist/pipeline/preflight/parse-imports.js +125 -0
  97. package/dist/report-store.d.ts +8 -0
  98. package/dist/report-store.js +55 -6
  99. package/dist/sanity/document-renderers.d.ts +45 -7
  100. package/dist/sanity/document-renderers.js +99 -13
  101. package/dist/sanity/queries.d.ts +11 -11
  102. package/dist/sanity/queries.js +7 -0
  103. package/dist/sanity/symbol-index.d.ts +98 -0
  104. package/dist/sanity/symbol-index.js +615 -0
  105. package/package.json +2 -1
@@ -0,0 +1,12 @@
1
+ /**
2
+ * load-preflight-scoring — read the W0198 preflight scoring config
3
+ * (`config/preflight-scoring.ts`) authored via `definePreflightScoring()`.
4
+ *
5
+ * Returns `undefined` when the file is absent so callers fall back to
6
+ * `DEFAULT_PREFLIGHT_CODE_CORRECTNESS_WEIGHT`. The eval package itself
7
+ * ships a config so the live pipeline always finds one; the optional
8
+ * return path exists for downstream / external callers that may not
9
+ * have authored one yet.
10
+ */
11
+ import type { PreflightScoringConfig } from "../../_vendor/ailf-core/index.d.ts";
12
+ export declare function loadPreflightScoring(rootDir: string): Promise<PreflightScoringConfig | undefined>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * load-preflight-scoring — read the W0198 preflight scoring config
3
+ * (`config/preflight-scoring.ts`) authored via `definePreflightScoring()`.
4
+ *
5
+ * Returns `undefined` when the file is absent so callers fall back to
6
+ * `DEFAULT_PREFLIGHT_CODE_CORRECTNESS_WEIGHT`. The eval package itself
7
+ * ships a config so the live pipeline always finds one; the optional
8
+ * return path exists for downstream / external callers that may not
9
+ * have authored one yet.
10
+ */
11
+ import { tryLoadConfigFile } from "../compiler/config-loader.js";
12
+ export async function loadPreflightScoring(rootDir) {
13
+ const result = tryLoadConfigFile("preflight-scoring", rootDir);
14
+ if (!result)
15
+ return undefined;
16
+ return result.data;
17
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * parse-imports — pure function that extracts every `import` declaration
3
+ * from a candidate code block as a flat list of per-binding entries.
4
+ *
5
+ * Output shape is intentionally flat: the W0198 preflight checks each
6
+ * `(source, imported)` pair against the resolved package surface, so a
7
+ * flat list is the natural input. Multi-binding declarations
8
+ * (`import { a, b as c } from "pkg"`) produce one entry per binding;
9
+ * default + named combos (`import def, { a } from "pkg"`) likewise.
10
+ *
11
+ * Implementation: delegates to `oxc-parser`'s `staticImports` view, which
12
+ * already decomposes each import statement into entries with explicit
13
+ * `importName` / `localName` / `isType` fields. The TS-aware grammar
14
+ * means dynamic `import(...)`, `import.meta`, and malformed statements
15
+ * are all handled by the parser itself — we just translate the entries
16
+ * into our `CandidateImportBinding` shape.
17
+ *
18
+ * Recognized grammar (handled by oxc-parser):
19
+ * - `import { a, b as c } from "pkg"`
20
+ * - `import def from "pkg"`
21
+ * - `import * as ns from "pkg"`
22
+ * - `import def, { a } from "pkg"` and `import def, * as ns from "pkg"`
23
+ * - `import type { ... } from "pkg"` and `import { type a, b } from "pkg"`
24
+ * - `import "pkg"` (side-effect; surfaced as a single `side-effect` entry)
25
+ * - Multi-line variants of all of the above.
26
+ *
27
+ * Out of scope (intentionally — both regex and oxc-parser ignore these):
28
+ * - Dynamic `import("pkg")` — runtime, not statically resolvable here.
29
+ * - `export { a } from "pkg"` re-exports from candidate code.
30
+ * - TypeScript `import = require()` and `import("...").Type` ambient
31
+ * references — neither pattern shows up in the App SDK / Studio
32
+ * candidate corpus the preflight grades against.
33
+ */
34
+ export type CandidateImportKind = "named" | "default" | "namespace" | "side-effect";
35
+ export interface CandidateImportBinding {
36
+ /** Source specifier as written by the candidate (e.g. `"@sanity/sdk-react"`). */
37
+ source: string;
38
+ /** Which import-clause shape this binding came from. */
39
+ kind: CandidateImportKind;
40
+ /**
41
+ * Name to look up against the package surface:
42
+ * - `kind: "named"` — the imported identifier.
43
+ * - `kind: "default"` — the literal string `"default"`.
44
+ * - `kind: "namespace"` — the literal string `"*"`.
45
+ * - `kind: "side-effect"` — empty string (no binding).
46
+ */
47
+ imported: string;
48
+ /** Local alias used in the candidate's body. Same as `imported` when no alias. */
49
+ local: string;
50
+ /**
51
+ * Whether this binding is type-only — either the whole declaration was
52
+ * `import type`, or this specifier was prefixed with `type`
53
+ * (`import { type X, Y } from "pkg"`).
54
+ */
55
+ isType: boolean;
56
+ /**
57
+ * 1-based line number where the import declaration starts in the
58
+ * source. Useful for surfacing findings back to a reviewer.
59
+ */
60
+ line: number;
61
+ }
62
+ export declare function parseImports(src: string): CandidateImportBinding[];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * parse-imports — pure function that extracts every `import` declaration
3
+ * from a candidate code block as a flat list of per-binding entries.
4
+ *
5
+ * Output shape is intentionally flat: the W0198 preflight checks each
6
+ * `(source, imported)` pair against the resolved package surface, so a
7
+ * flat list is the natural input. Multi-binding declarations
8
+ * (`import { a, b as c } from "pkg"`) produce one entry per binding;
9
+ * default + named combos (`import def, { a } from "pkg"`) likewise.
10
+ *
11
+ * Implementation: delegates to `oxc-parser`'s `staticImports` view, which
12
+ * already decomposes each import statement into entries with explicit
13
+ * `importName` / `localName` / `isType` fields. The TS-aware grammar
14
+ * means dynamic `import(...)`, `import.meta`, and malformed statements
15
+ * are all handled by the parser itself — we just translate the entries
16
+ * into our `CandidateImportBinding` shape.
17
+ *
18
+ * Recognized grammar (handled by oxc-parser):
19
+ * - `import { a, b as c } from "pkg"`
20
+ * - `import def from "pkg"`
21
+ * - `import * as ns from "pkg"`
22
+ * - `import def, { a } from "pkg"` and `import def, * as ns from "pkg"`
23
+ * - `import type { ... } from "pkg"` and `import { type a, b } from "pkg"`
24
+ * - `import "pkg"` (side-effect; surfaced as a single `side-effect` entry)
25
+ * - Multi-line variants of all of the above.
26
+ *
27
+ * Out of scope (intentionally — both regex and oxc-parser ignore these):
28
+ * - Dynamic `import("pkg")` — runtime, not statically resolvable here.
29
+ * - `export { a } from "pkg"` re-exports from candidate code.
30
+ * - TypeScript `import = require()` and `import("...").Type` ambient
31
+ * references — neither pattern shows up in the App SDK / Studio
32
+ * candidate corpus the preflight grades against.
33
+ */
34
+ import { parseSync } from "oxc-parser";
35
+ export function parseImports(src) {
36
+ // Use the `.tsx` filename hint so the parser tolerates JSX in candidate
37
+ // code (App SDK literacy tasks frequently include JSX in their answers).
38
+ const result = parseSync("input.tsx", src, { lang: "tsx" });
39
+ const lineStarts = computeLineStarts(src);
40
+ const offsetToLine = (offset) => binarySearchLine(lineStarts, offset) + 1;
41
+ const out = [];
42
+ for (const imp of result.module.staticImports) {
43
+ const source = imp.moduleRequest.value;
44
+ // The parser recovers from malformed specifiers (e.g. backtick-quoted
45
+ // module requests) by emitting an empty source; the W0198 preflight
46
+ // can't do anything useful with that, so drop those entries.
47
+ if (!source)
48
+ continue;
49
+ const line = offsetToLine(imp.start);
50
+ if (imp.entries.length === 0) {
51
+ // Side-effect form: `import "pkg"`.
52
+ out.push({
53
+ source,
54
+ kind: "side-effect",
55
+ imported: "",
56
+ local: "",
57
+ isType: false,
58
+ line,
59
+ });
60
+ continue;
61
+ }
62
+ for (const entry of imp.entries) {
63
+ const local = entry.localName.value;
64
+ const isType = entry.isType;
65
+ switch (entry.importName.kind) {
66
+ case "Name":
67
+ out.push({
68
+ source,
69
+ kind: "named",
70
+ imported: entry.importName.name ?? local,
71
+ local,
72
+ isType,
73
+ line,
74
+ });
75
+ break;
76
+ case "Default":
77
+ out.push({
78
+ source,
79
+ kind: "default",
80
+ imported: "default",
81
+ local,
82
+ isType,
83
+ line,
84
+ });
85
+ break;
86
+ case "NamespaceObject":
87
+ out.push({
88
+ source,
89
+ kind: "namespace",
90
+ imported: "*",
91
+ local,
92
+ isType,
93
+ line,
94
+ });
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Line-number helpers — oxc-parser returns byte offsets; the W0198
103
+ // preflight surfaces 1-based line numbers in findings so reviewers can
104
+ // jump straight to the citing line.
105
+ // ---------------------------------------------------------------------------
106
+ function computeLineStarts(src) {
107
+ const offsets = [0];
108
+ for (let i = 0; i < src.length; i++) {
109
+ if (src[i] === "\n")
110
+ offsets.push(i + 1);
111
+ }
112
+ return offsets;
113
+ }
114
+ function binarySearchLine(offsets, target) {
115
+ let lo = 0;
116
+ let hi = offsets.length - 1;
117
+ while (lo < hi) {
118
+ const mid = (lo + hi + 1) >>> 1;
119
+ if (offsets[mid] <= target)
120
+ lo = mid;
121
+ else
122
+ hi = mid - 1;
123
+ }
124
+ return lo;
125
+ }
@@ -65,6 +65,10 @@ export declare class ReportStore {
65
65
  * matching `evalFingerprint`. Used by the pipeline to skip the expensive
66
66
  * eval step when identical inputs have already been evaluated.
67
67
  *
68
+ * Advisory lookup: a `ReportSchemaValidationError` from a corrupt prior
69
+ * doc is logged + counted, then null is returned so the current eval
70
+ * proceeds. Use `read(id)` when callers ask for a specific document by id.
71
+ *
68
72
  * @returns The cached report, or null if no match or on error
69
73
  * @see docs/design-docs/content-lake-eval-caching.md
70
74
  */
@@ -78,6 +82,10 @@ export declare class ReportStore {
78
82
  * "Comparable" means: same evaluation mode + same source name.
79
83
  * More granular matching (areas, models) can be added as needed.
80
84
  *
85
+ * Advisory lookup: see `findByFingerprint` — a malformed prior baseline
86
+ * is logged + counted and null is returned so the current run still
87
+ * publishes its report.
88
+ *
81
89
  * @see docs/design-docs/report-store/architecture.md — Auto-comparison
82
90
  */
83
91
  findComparableBaseline(query: LineageQuery): Promise<null | Report>;
@@ -119,6 +119,10 @@ export class ReportStore {
119
119
  * matching `evalFingerprint`. Used by the pipeline to skip the expensive
120
120
  * eval step when identical inputs have already been evaluated.
121
121
  *
122
+ * Advisory lookup: a `ReportSchemaValidationError` from a corrupt prior
123
+ * doc is logged + counted, then null is returned so the current eval
124
+ * proceeds. Use `read(id)` when callers ask for a specific document by id.
125
+ *
122
126
  * @returns The cached report, or null if no match or on error
123
127
  * @see docs/design-docs/content-lake-eval-caching.md
124
128
  */
@@ -131,9 +135,19 @@ export class ReportStore {
131
135
  return doc ? toReport(doc) : null;
132
136
  }
133
137
  catch (error) {
134
- // W0191: schema-validation errors are bugs, not outages surface them.
135
- if (error instanceof ReportSchemaValidationError)
136
- throw error;
138
+ // Advisory lookup a single corrupt prior doc must not break the
139
+ // current eval. Log loudly + emit a counter so ops can alert, and
140
+ // return null so the caller treats it as "no comparable cache hit".
141
+ // Direct read(id) keeps the rethrow behavior because the caller asked
142
+ // for that specific document and silent-null would mask the bug.
143
+ if (error instanceof ReportSchemaValidationError) {
144
+ logAdvisoryQuerySchemaFailure({
145
+ query: "findByFingerprint",
146
+ context: { fingerprint },
147
+ error,
148
+ });
149
+ return null;
150
+ }
137
151
  console.warn(` ⚠️ Failed to query cached report by fingerprint: ${error instanceof Error ? error.message : String(error)}`);
138
152
  return null;
139
153
  }
@@ -147,6 +161,10 @@ export class ReportStore {
147
161
  * "Comparable" means: same evaluation mode + same source name.
148
162
  * More granular matching (areas, models) can be added as needed.
149
163
  *
164
+ * Advisory lookup: see `findByFingerprint` — a malformed prior baseline
165
+ * is logged + counted and null is returned so the current run still
166
+ * publishes its report.
167
+ *
150
168
  * @see docs/design-docs/report-store/architecture.md — Auto-comparison
151
169
  */
152
170
  async findComparableBaseline(query) {
@@ -170,9 +188,21 @@ export class ReportStore {
170
188
  return doc ? toReport(doc) : null;
171
189
  }
172
190
  catch (error) {
173
- // W0191: schema-validation errors are bugs, not outages surface them.
174
- if (error instanceof ReportSchemaValidationError)
175
- throw error;
191
+ // Advisory lookup see findByFingerprint for rationale. A malformed
192
+ // prior baseline returns null + counter so the current run still
193
+ // publishes; direct read(id) preserves the rethrow.
194
+ if (error instanceof ReportSchemaValidationError) {
195
+ logAdvisoryQuerySchemaFailure({
196
+ query: "findComparableBaseline",
197
+ context: {
198
+ mode: query.mode,
199
+ sourceName: query.source?.name,
200
+ before: query.before,
201
+ },
202
+ error,
203
+ });
204
+ return null;
205
+ }
176
206
  console.warn(` ⚠️ Failed to query comparable baseline: ${error instanceof Error ? error.message : String(error)}`);
177
207
  return null;
178
208
  }
@@ -299,6 +329,25 @@ export class ReportSchemaValidationError extends Error {
299
329
  this.name = "ReportSchemaValidationError";
300
330
  }
301
331
  }
332
+ /**
333
+ * Stable log marker for log-aggregator counters. Operators alert on the
334
+ * count of `[report-store.advisory] schema_validation_error` lines per
335
+ * window; the trailing JSON carries enough context (which advisory query,
336
+ * what filter values, error message) to find the offending document
337
+ * without spelunking through GROQ.
338
+ *
339
+ * Emitted via console.error rather than console.warn so it surfaces in
340
+ * the same severity tier as a real failure even though we're swallowing
341
+ * it for the current run.
342
+ */
343
+ function logAdvisoryQuerySchemaFailure(input) {
344
+ const payload = {
345
+ query: input.query,
346
+ context: input.context,
347
+ error: input.error.message,
348
+ };
349
+ console.error(`[report-store.advisory] schema_validation_error ${JSON.stringify(payload)}`);
350
+ }
302
351
  export function toSanityReportDoc(report) {
303
352
  const comparison = report.comparison
304
353
  ? stripComparisonBulk(report.comparison)
@@ -1,11 +1,22 @@
1
1
  /**
2
2
  * document-renderers.ts
3
3
  *
4
- * Renderer registry that turns a Sanity document fetched by `_id` into
5
- * Markdown for inclusion in a literacy task's grader context.
4
+ * Renderer registry that turns a Sanity document into both:
6
5
  *
7
- * The resolver fetches docs without an `_type` filter and dispatches here.
8
- * Two tiers of fidelity:
6
+ * - Markdown content for inclusion in a literacy task's grader/candidate
7
+ * context (existing surface, used by the doc fetcher).
8
+ * - A symbol-reference index for the W0197 grader-context pathway —
9
+ * a flat list of identifiers the doc legitimizes, with provenance.
10
+ * The grader prefers this over the rendered markdown when available
11
+ * (smaller, deterministic, harder for the grader's prior to override).
12
+ *
13
+ * Both surfaces dispatch through the same registry. Articles and
14
+ * typesReference docs have hand-written renderers; everything else falls
15
+ * through to the default walker. This keeps "what to do with a document
16
+ * of type X" a single decision point regardless of whether the doc was
17
+ * looked up by slug, path, perspective, or id.
18
+ *
19
+ * Two tiers of fidelity (rendered output):
9
20
  *
10
21
  * 1. Registered renderers (high fidelity) — `article`, `typesReference`.
11
22
  * Hand-written for the document shapes we care about most.
@@ -14,9 +25,11 @@
14
25
  * skips framework-internal fields. Lets pinning a `marketingPage`,
15
26
  * `glossaryEntry`, etc. work without AILF code changes.
16
27
  *
17
- * Adding a new high-fidelity renderer: implement a `DocumentRenderer` and
18
- * register it in `BUILT_IN_RENDERERS` keyed by `_type`.
28
+ * Adding a new high-fidelity renderer: implement a `DocumentRenderer`
29
+ * (both `render` and `extractSymbols`) and register it in
30
+ * `BUILT_IN_RENDERERS` keyed by `_type`.
19
31
  */
32
+ import { type SymbolIndex } from "./symbol-index.js";
20
33
  /**
21
34
  * A Sanity document plus any references we've already resolved for it.
22
35
  * The resolver fetches the doc once and may include common deref payloads
@@ -57,12 +70,37 @@ export interface RenderResult {
57
70
  */
58
71
  slug: string;
59
72
  }
60
- export type DocumentRenderer = (doc: DocumentForRender, ctx: RenderContext) => Promise<RenderResult> | RenderResult;
73
+ export interface DocumentRenderer {
74
+ /**
75
+ * Produce rendered Markdown for the doc's content surface (for grader
76
+ * + candidate context inclusion).
77
+ */
78
+ render(doc: DocumentForRender, ctx: RenderContext): Promise<RenderResult> | RenderResult;
79
+ /**
80
+ * Produce a symbol-reference index for the doc — a flat list of
81
+ * identifiers the doc legitimizes plus provenance snippets. Used by
82
+ * the grader-context pathway (W0197) instead of injecting the full
83
+ * rendered doc. Returning an empty index signals the caller to fall
84
+ * back to the rendered markdown.
85
+ */
86
+ extractSymbols(doc: DocumentForRender, ctx: RenderContext): Promise<SymbolIndex> | SymbolIndex;
87
+ }
61
88
  /**
62
89
  * Render a document using the registered renderer for its `_type`, falling
63
90
  * back to the default walker. The returned `fidelity` flag tells callers
64
91
  * whether to emit the "info: dedicated renderer would help" log.
65
92
  */
66
93
  export declare function renderDocument(doc: DocumentForRender, ctx?: RenderContext): Promise<RenderResult>;
94
+ /**
95
+ * Extract a symbol-reference index for a document using its registered
96
+ * renderer (or the default walker for unknown types). Used by the
97
+ * grader-context pathway (W0197) to feed the LLM judge a compact
98
+ * deterministic recognition reference instead of the full rendered doc.
99
+ *
100
+ * Returns an empty `SymbolIndex` (`{ symbols: [] }`) when extraction
101
+ * yields nothing — callers interpret this as the signal to fall back to
102
+ * the rendered markdown.
103
+ */
104
+ export declare function extractSymbolsForDoc(doc: DocumentForRender, ctx?: RenderContext): Promise<SymbolIndex>;
67
105
  /** Exported for tests and consumers that want the registered set. */
68
106
  export declare const REGISTERED_RENDERER_TYPES: string[];
@@ -1,11 +1,22 @@
1
1
  /**
2
2
  * document-renderers.ts
3
3
  *
4
- * Renderer registry that turns a Sanity document fetched by `_id` into
5
- * Markdown for inclusion in a literacy task's grader context.
4
+ * Renderer registry that turns a Sanity document into both:
6
5
  *
7
- * The resolver fetches docs without an `_type` filter and dispatches here.
8
- * Two tiers of fidelity:
6
+ * - Markdown content for inclusion in a literacy task's grader/candidate
7
+ * context (existing surface, used by the doc fetcher).
8
+ * - A symbol-reference index for the W0197 grader-context pathway —
9
+ * a flat list of identifiers the doc legitimizes, with provenance.
10
+ * The grader prefers this over the rendered markdown when available
11
+ * (smaller, deterministic, harder for the grader's prior to override).
12
+ *
13
+ * Both surfaces dispatch through the same registry. Articles and
14
+ * typesReference docs have hand-written renderers; everything else falls
15
+ * through to the default walker. This keeps "what to do with a document
16
+ * of type X" a single decision point regardless of whether the doc was
17
+ * looked up by slug, path, perspective, or id.
18
+ *
19
+ * Two tiers of fidelity (rendered output):
9
20
  *
10
21
  * 1. Registered renderers (high fidelity) — `article`, `typesReference`.
11
22
  * Hand-written for the document shapes we care about most.
@@ -14,10 +25,12 @@
14
25
  * skips framework-internal fields. Lets pinning a `marketingPage`,
15
26
  * `glossaryEntry`, etc. work without AILF code changes.
16
27
  *
17
- * Adding a new high-fidelity renderer: implement a `DocumentRenderer` and
18
- * register it in `BUILT_IN_RENDERERS` keyed by `_type`.
28
+ * Adding a new high-fidelity renderer: implement a `DocumentRenderer`
29
+ * (both `render` and `extractSymbols`) and register it in
30
+ * `BUILT_IN_RENDERERS` keyed by `_type`.
19
31
  */
20
32
  import { toMarkdown } from "./portable-text.js";
33
+ import { extractSymbolIndex, extractSymbolsFromTypedoc, mergeSymbolIndexes, } from "./symbol-index.js";
21
34
  // ---------------------------------------------------------------------------
22
35
  // Helpers
23
36
  // ---------------------------------------------------------------------------
@@ -56,7 +69,7 @@ function slugForDoc(doc) {
56
69
  return slugField;
57
70
  return `${doc._type}:${doc._id}`;
58
71
  }
59
- function articleRenderer(doc) {
72
+ function renderArticle(doc) {
60
73
  const title = doc.title ?? "(untitled)";
61
74
  const description = doc.description;
62
75
  const section = doc.section;
@@ -70,7 +83,16 @@ function articleRenderer(doc) {
70
83
  slug: slugForDoc(doc),
71
84
  };
72
85
  }
73
- async function typesReferenceRenderer(doc, ctx) {
86
+ const articleRenderer = {
87
+ render: renderArticle,
88
+ extractSymbols(doc) {
89
+ const content = doc.content;
90
+ if (!Array.isArray(content))
91
+ return { symbols: [] };
92
+ return extractSymbolIndex(content);
93
+ },
94
+ };
95
+ async function renderTypesReference(doc, ctx) {
74
96
  const title = doc.title ?? "(untitled)";
75
97
  const slug = slugForDoc(doc);
76
98
  const library = doc.library;
@@ -109,6 +131,24 @@ async function typesReferenceRenderer(doc, ctx) {
109
131
  lines.push("```");
110
132
  return { content: lines.join("\n"), fidelity: "high", slug };
111
133
  }
134
+ async function extractTypesReferenceSymbols(doc, ctx) {
135
+ const library = doc.library;
136
+ const latestVersion = doc.latestVersion;
137
+ const asset = latestVersion?.attachment?.asset;
138
+ // Re-fetches the same URL the renderer would; bounded to ≤ a handful of
139
+ // typesReference docs per eval run, so the duplicate I/O is acceptable.
140
+ // Fold into a single registry method later if profiling shows it bites.
141
+ if (!asset?.url || !ctx.fetchUrl)
142
+ return { symbols: [] };
143
+ const body = await ctx.fetchUrl(asset.url);
144
+ if (body === null)
145
+ return { symbols: [] };
146
+ return extractSymbolsFromTypedoc(body, library?.npmName);
147
+ }
148
+ const typesReferenceRenderer = {
149
+ render: renderTypesReference,
150
+ extractSymbols: extractTypesReferenceSymbols,
151
+ };
112
152
  // ---------------------------------------------------------------------------
113
153
  // formatDefault — generic walker for any unknown `_type`.
114
154
  //
@@ -174,7 +214,7 @@ function renderField(key, value, depth = 0) {
174
214
  }
175
215
  return null;
176
216
  }
177
- function defaultRenderer(doc) {
217
+ function renderDefault(doc) {
178
218
  const title = doc.title ?? `(${doc._type})`;
179
219
  const slug = slugForDoc(doc);
180
220
  const lines = [`## ${title}`, "", `Type: \`${doc._type}\``];
@@ -199,6 +239,39 @@ function defaultRenderer(doc) {
199
239
  }
200
240
  return { content: lines.join("\n"), fidelity: "default", slug };
201
241
  }
242
+ function extractDefaultSymbols(doc) {
243
+ // For unknown types we don't know the doc's intent — but if it has any
244
+ // Portable Text fields, those probably contain prose-with-inline-code
245
+ // that names symbols. Walk top-level fields, run the PT extractor on
246
+ // each PT array, and tag each extracted entry with the originating
247
+ // field name so reviewers can trace which field a symbol came from.
248
+ // Returns empty for shapes with no PT content (purely scalar docs like
249
+ // `marketingPage`); caller falls back to the rendered markdown.
250
+ const indexes = [];
251
+ for (const [key, value] of Object.entries(doc)) {
252
+ if (SKIP_FIELDS.has(key))
253
+ continue;
254
+ if (isPortableTextArray(value)) {
255
+ indexes.push(tagWithFieldName(extractSymbolIndex(value), key));
256
+ }
257
+ }
258
+ return mergeSymbolIndexes(indexes);
259
+ }
260
+ function tagWithFieldName(index, fieldName) {
261
+ return {
262
+ symbols: index.symbols.map((entry) => ({
263
+ symbol: entry.symbol,
264
+ provenance: {
265
+ ...entry.provenance,
266
+ snippet: `[${fieldName}] ${entry.provenance.snippet}`,
267
+ },
268
+ })),
269
+ };
270
+ }
271
+ const defaultRenderer = {
272
+ render: renderDefault,
273
+ extractSymbols: extractDefaultSymbols,
274
+ };
202
275
  // ---------------------------------------------------------------------------
203
276
  // Registry
204
277
  // ---------------------------------------------------------------------------
@@ -206,16 +279,29 @@ const BUILT_IN_RENDERERS = {
206
279
  article: articleRenderer,
207
280
  typesReference: typesReferenceRenderer,
208
281
  };
282
+ function rendererFor(type) {
283
+ return BUILT_IN_RENDERERS[type] ?? defaultRenderer;
284
+ }
209
285
  /**
210
286
  * Render a document using the registered renderer for its `_type`, falling
211
287
  * back to the default walker. The returned `fidelity` flag tells callers
212
288
  * whether to emit the "info: dedicated renderer would help" log.
213
289
  */
214
290
  export async function renderDocument(doc, ctx = {}) {
215
- const renderer = BUILT_IN_RENDERERS[doc._type];
216
- if (renderer)
217
- return renderer(doc, ctx);
218
- return defaultRenderer(doc);
291
+ return rendererFor(doc._type).render(doc, ctx);
292
+ }
293
+ /**
294
+ * Extract a symbol-reference index for a document using its registered
295
+ * renderer (or the default walker for unknown types). Used by the
296
+ * grader-context pathway (W0197) to feed the LLM judge a compact
297
+ * deterministic recognition reference instead of the full rendered doc.
298
+ *
299
+ * Returns an empty `SymbolIndex` (`{ symbols: [] }`) when extraction
300
+ * yields nothing — callers interpret this as the signal to fall back to
301
+ * the rendered markdown.
302
+ */
303
+ export async function extractSymbolsForDoc(doc, ctx = {}) {
304
+ return rendererFor(doc._type).extractSymbols(doc, ctx);
219
305
  }
220
306
  /** Exported for tests and consumers that want the registered set. */
221
307
  export const REGISTERED_RENDERER_TYPES = Object.keys(BUILT_IN_RENDERERS).sort();