Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
420d78b4a2 | ||
|
|
98ed225471 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"monorepoCommit": "be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9",
|
||||
"monorepoCommit": "488f7e229afed8f03fc3bfa4abe707da0c613460",
|
||||
"sourcePath": "examples/local-first/packages/language-reader-runtime"
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ Documents preserve their exact original text and source attribution. Their
|
||||
Canonical sourceRevision is a content hash; variant revisions identify an immutable
|
||||
adaptation. All ranges use original UTF-16 offsets in that document.
|
||||
Creating a rewritten document retains the original document. Features contain
|
||||
presentation preferences and a processor key; schemas and implementations belong
|
||||
in code. Feature changes use expectedRevision to protect concurrent edits.
|
||||
a processor key, enabled state, name and declared options; computation policy
|
||||
and any number of render channels belong in code. Creation appearance choices
|
||||
are historical authoring instructions, never a stored single visual type. Feature changes use expectedRevision to protect concurrent edits.
|
||||
|
||||
Inference is admitted through a persisted narrow Agent. Each scope request owns
|
||||
a thread and request; managed graph edges retain those associations. Completion
|
||||
@@ -169,7 +170,7 @@ conversation at desktop and narrow widths.
|
||||
## Inline annotator configuration
|
||||
|
||||
Configuration belongs to annotator code and appears on its page in Reader settings.
|
||||
`Processor.config` is a list of `ConfigField` controls, or a function returning that
|
||||
`Processor.config` is a list of `ConfigNode` controls/groups, or a function returning that
|
||||
list. Supported controls: `color`, `number`, `select`, `toggle`, `text`, with a key,
|
||||
label, typed default, optional numeric bounds and select choices. The grammar
|
||||
processor generates one color control per part of speech; names does the same for
|
||||
@@ -178,7 +179,7 @@ entity types. Declare every option in code so the server can validate it.
|
||||
`ReadingFeature.options` persists the JSON object. `configureOptions` checks
|
||||
`expectedRevision`, validates options and increments `configRevision` atomically.
|
||||
The UI edits a draft and saves all processor options together; appearance/reveal
|
||||
controls save directly when the processor explicitly declares them. `addFeature` requires `options` (use `"{}"` for defaults) and optional `authoring` (use null for premades).
|
||||
controls use that same draft when the processor explicitly declares them. `addFeature` requires `options` (use `"{}"` for defaults) and optional `authoring` (use null for premades).
|
||||
Concurrent edits reject cleanly. Changing display settings preserves raw cached results; only semantic inputs invalidate computation.
|
||||
|
||||
`Processor.prompt(config)` can include validated config in developer instructions.
|
||||
@@ -186,8 +187,8 @@ Local processors receive config as their third argument.
|
||||
The presentation `annotationDisplays` registry can return per-item `color`, `fontFamily`, or
|
||||
`fontSize`, applied after strict model output validation. Model output stays the
|
||||
four-field range/text/tag contract; rendering choices are owned by checked code.
|
||||
Common options `fontFamily` and `fontSize` configure labels. Their style remains
|
||||
`overhead`; `options.placement` chooses `"above"` (default) or `"below"`. A
|
||||
An explicitly composed `labelControls` group configures its font and placement.
|
||||
There are no implicit common options. A
|
||||
`Processor.renders` hook can set `placement` independently. Both positions use
|
||||
fixed source lanes, and display changes reuse semantic caches. Wide labels gain
|
||||
faint alignment guides; hovering a label emphasizes its source without invoking
|
||||
@@ -200,8 +201,9 @@ a React component to `annotatorConfigViews[processor]` in
|
||||
`LanguageReaderPresentation/src/annotator-config.tsx`; it receives `fields`,
|
||||
`values`, `disabled`, and `change(key, value)`. This component edits the same draft
|
||||
and validated options. Built annotators have no universal output-type editor.
|
||||
`Processor.settings` explicitly opts into color, font, or timing controls;
|
||||
`description` explains the behavior and `preset` defines a premade output.
|
||||
`Processor.config` is the entire settings schema, including appearance and timing
|
||||
when the author chooses to expose them. `description` explains the behavior and
|
||||
`renders` defines each output channel.
|
||||
The New annotator panel can reuse any installed processor type with its code-owned
|
||||
controls, or describe a new type to the author. `LanguageReader.archiveType` takes
|
||||
`{processor, archived}` and updates `archivedTypes`, hiding a type from the creation
|
||||
@@ -269,7 +271,7 @@ Hover feedback changes source ink immediately and never draws a word outline.
|
||||
|
||||
## Tooltip presentation
|
||||
|
||||
Features can use `style: "tooltip"` with `reveal: "hover"` or `"click"`.
|
||||
Render channels can use `style: "tooltip"` with `reveal: "hover"` or `"click"`.
|
||||
Choose Tooltip in the annotation editor, then choose its reveal trigger. Existing
|
||||
processors and generated/local content work unchanged; this is a presentation
|
||||
choice. `always` is unsupported for tooltips. Hover inference retains dwell,
|
||||
@@ -396,3 +398,37 @@ cached facts through current appearance/display options, never initiates inferen
|
||||
Author new data models simply, and refactor to richer domain entities when needed
|
||||
while preserving user values via supported transformations. Independently scoped UI
|
||||
work can use bounded Codex subagents; join/check before activation.
|
||||
|
||||
## Composable, code-owned settings
|
||||
|
||||
Only `Processor.config` declares accepted settings. Neither validation nor the UI
|
||||
adds controls because an annotator renders an underline or a label. Custom React
|
||||
views in `annotatorConfigViews` own the entire config presentation. Generic
|
||||
controls use explicit `presentation` descriptors rather than guessing from keys.
|
||||
|
||||
Reuse independent nested structs from `config.ts`, for example:
|
||||
|
||||
```ts
|
||||
config: [
|
||||
underlineControls("primary", "Primary underline", {pattern: "dashed"}),
|
||||
underlineControls("secondary", "Secondary underline", {color: "#947459"}),
|
||||
labelControls("gloss", "Gloss text"),
|
||||
],
|
||||
renders: [
|
||||
{style: "underline", reveal: "always", options: "primary"},
|
||||
{style: "underline", reveal: "click", options: "secondary"},
|
||||
{style: "overhead", reveal: "click", options: "gloss"},
|
||||
]
|
||||
```
|
||||
|
||||
The generated React settings compose each group as its own labeled section.
|
||||
Groups can also be composed in bespoke views. Their defaults, validation and
|
||||
settings-to-render bindings are explicit; fixed channels need no settings.
|
||||
For a group's own React presentation, register
|
||||
`annotatorConfigViews["processor/groupKey"]`; it receives that group's fields and
|
||||
values and edits that group through the same validated draft. Compose these
|
||||
views as needed without introducing a universal appearance editor.
|
||||
Use `computation:false` on display-only leaf fields. Nested semantic input paths
|
||||
participate in cache identity and enter prompts as nested data. Appearance changes
|
||||
must preserve the semantic cache. Test changing each exposed control all the way
|
||||
through its render channel. Never hardcode a value over an exposed setting.
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ external atom ReadingWordObject id "atom:reading:word";
|
||||
external atom ReadingSubscription id "atom:reading:subscription";
|
||||
external atom ReadingNavigationObject id "atom:reading:navigation";
|
||||
type DocumentData = record {title:string;text:string;sourceRevision:string;source:string;url:string;attribution:string;};
|
||||
type FeatureData = record {name:string;processor:string;style:string;reveal:string;generation:string;scope:string;color:string;enabled:bool;configRevision:uint32;};
|
||||
type FeatureData = record {name:string;processor:string;enabled:bool;configRevision:uint32;};
|
||||
type JobData = record {key:string;kind:string;featureRevision:uint32;sourceRevision:string;start:uint32;end:uint32;status:string;output:string;error:string;createdAt:string;};
|
||||
type ReaderData = record {name:string;size:uint32;width:uint32;font:string;background:string;configRevision:uint32;};
|
||||
type ReaderSeed = record {palette:record {text:string;accent:string;};data:ReaderData;library:interface-ref<LanguageReader>;};
|
||||
@@ -121,9 +121,9 @@ package LanguageReaderRuntime id "package:reading:runtime" revision "package:rea
|
||||
operation addDocument id "reading:runtime:add-document" : record {title:string;text:string;source:string;url:string;attribution:string;} -> atom-ref<ReadingDocumentObject> mode call execution transaction targets [server] receiver atom ReadingLibrary requires {creation document id "document" : ReadingDocumentObject input DocumentSeed;interface home id "home" : LanguageReader using [defaultReader.resolve] from input;};
|
||||
operation attachAuthoring id "reading:runtime:attach-authoring" : atom-ref<ReadingJobObject> -> unit mode call execution transaction targets [server] receiver atom ReadingFeatureObject requires {edge authoring id "authoring" : optional-one atom ReadingJobObject [resolve,connect,disconnect];edge library id "library" : exactly-one interface LanguageReader [resolve];edge reader id "reader" : optional-one interface ReadingReader [resolve];interface job id "job" : ReadingJob using [library.resolve,reader.resolve] from input;};
|
||||
operation setEnabled id "reading:runtime:set-enabled" : bool -> unit mode call execution transaction targets [server,JavaScript] receiver atom ReadingFeatureObject requires {state data id "data" : FeatureData [read,write];};
|
||||
operation addFeature id "reading:runtime:add-feature" : record {authoring:optional<atom-ref<ReadingJobObject>>;reader:optional<atom-ref<ReadingReaderObject>>;name:string;processor:string;style:string;reveal:string;generation:string;scope:string;color:string;options:string;} -> atom-ref<ReadingFeatureObject> mode call execution transaction targets [server] receiver atom ReadingLibrary requires {interface authoring id "authoring" : ReadingJob using [library.resolve,reader.resolve] from input;creation feature id "feature" : ReadingFeatureObject input FeatureSeed;interface home id "home" : LanguageReader using [defaultReader.resolve] from input;interface reader id "reader" : ReadingReader using [library.resolve] from input;};
|
||||
operation addFeature id "reading:runtime:add-feature" : record {authoring:optional<atom-ref<ReadingJobObject>>;reader:optional<atom-ref<ReadingReaderObject>>;name:string;processor:string;options:string;} -> atom-ref<ReadingFeatureObject> mode call execution transaction targets [server] receiver atom ReadingLibrary requires {interface authoring id "authoring" : ReadingJob using [library.resolve,reader.resolve] from input;creation feature id "feature" : ReadingFeatureObject input FeatureSeed;interface home id "home" : LanguageReader using [defaultReader.resolve] from input;interface reader id "reader" : ReadingReader using [library.resolve] from input;};
|
||||
operation processorRevision id "reading:runtime:processor-revision" : unit -> string mode call execution snapshot targets [server] receiver atom ReadingFeatureObject requires {state data id "data" : FeatureData [read];};
|
||||
operation configure id "reading:runtime:configure" : record {expectedRevision:uint32;name:string;style:string;reveal:string;generation:string;scope:string;color:string;enabled:bool;} -> unit mode call execution transaction targets [server] receiver atom ReadingFeatureObject requires {state data id "data" : FeatureData [read,write];};
|
||||
operation configure id "reading:runtime:configure" : record {expectedRevision:uint32;name:string;enabled:bool;} -> unit mode call execution transaction targets [server] receiver atom ReadingFeatureObject requires {state data id "data" : FeatureData [read,write];};
|
||||
operation remember id "reading:runtime:remember" : string -> unit mode call execution transaction targets [server] receiver atom ReadingLibrary requires {
|
||||
interface home id "home" : LanguageReader using [ensureVocabulary.call] from input;
|
||||
interface vocabulary id "vocabulary" : ReadingVocabulary using [remember.call] from input;
|
||||
|
||||
+25
-25
@@ -1,27 +1,27 @@
|
||||
quixos-lock version 1 {
|
||||
quixos source { repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git"; commit "be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9"; }
|
||||
interface ReadingVocabulary source { repository "https://repos.quixos.org/quixos-templates/interface-reading-vocabulary.git"; commit "4c8dc969136d0add25c87c8f6cacb00f7dde97c0"; }
|
||||
interface AgentContextSubject source { repository "https://repos.quixos.org/quixos-templates/interface-agent-context-subject.git"; commit "bceb5b979b53e9ccbda96c008264e12eaa91f9fb"; }
|
||||
interface AgentDocumentation source { repository "https://repos.quixos.org/quixos-templates/interface-agent-documentation.git"; commit "c053da1b2c2cae1c140e75efec09095f5715176f"; }
|
||||
interface AgentWorkspace source { repository "https://repos.quixos.org/quixos-templates/interface-agent-workspace.git"; commit "fcf9a8d70e4488927d7b729d22fd972faf6b577a"; }
|
||||
interface AgentDirectory source { repository "https://repos.quixos.org/quixos-templates/interface-agent-directory.git"; commit "df28d7ee411168e4a44d890ee30c9be945861689"; }
|
||||
interface Agent source { repository "https://repos.quixos.org/quixos-templates/interface-agent.git"; commit "dcd3f271b42649b4ce15a890f9b41e19a7271563"; }
|
||||
interface AgentRequest source { repository "https://repos.quixos.org/quixos-templates/interface-agent-request.git"; commit "7f1e9d6278366329fd76f2698cd5a53c136f9914"; }
|
||||
interface EventStream source { repository "https://repos.quixos.org/quixos-templates/interface-event-stream.git"; commit "2d13e7489c29dcc64df767e144fa79f782790ecb"; }
|
||||
interface EventHandler source { repository "https://repos.quixos.org/quixos-templates/interface-event-handler.git"; commit "41825545baba6fb579d46987a28d225b43790d40"; }
|
||||
interface EventSubscription source { repository "https://repos.quixos.org/quixos-templates/interface-event-subscription.git"; commit "cbde0f3b7690e7226a702b1b9517e415ed10c0a2"; }
|
||||
interface WebStudioCanvas source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-canvas.git"; commit "98a61b746983960e152f5ae3a29f95bdb7483d9a"; }
|
||||
interface WebStudioCanvasPlacement source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-canvas-placement.git"; commit "6c40bcab64cecb7349f688800cea3db447a9dd85"; }
|
||||
interface WebStudioPlaceable source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-placeable.git"; commit "0a095eaaed67f47185a86816be86471285c031a9"; }
|
||||
interface LanguageReader source { repository "https://repos.quixos.org/quixos-templates/interface-language-reader.git"; commit "ad65d325d071443ba72edf5a99a5aeaa59f3e7e0"; }
|
||||
interface ReadingReader source { repository "https://repos.quixos.org/quixos-templates/interface-reading-reader.git"; commit "42b3565df44e6297ef88476fa44fa37e38a70d25"; }
|
||||
interface ReadingDocument source { repository "https://repos.quixos.org/quixos-templates/interface-reading-document.git"; commit "6ac56d55ee1dd79992cf28c3d760e1cf190225f1"; }
|
||||
interface ReadingFeature source { repository "https://repos.quixos.org/quixos-templates/interface-reading-feature.git"; commit "2e18f362ce34884e15771b4068197f1cd97c12d4"; }
|
||||
interface ReadingJob source { repository "https://repos.quixos.org/quixos-templates/interface-reading-job.git"; commit "3bc1b603b65c981e08c8521d111c9dfb983f3d83"; }
|
||||
interface ReadingRequest source { repository "https://repos.quixos.org/quixos-templates/interface-reading-request.git"; commit "5cfbb435b22074019741b9123359584a456efa8b"; }
|
||||
interface ReadingWord source { repository "https://repos.quixos.org/quixos-templates/interface-reading-word.git"; commit "9b302fb44e96671832de4aef4e4563dba5af6f2d"; }
|
||||
interface ReadingNavigation source { repository "https://repos.quixos.org/quixos-templates/interface-reading-navigation.git"; commit "710cab97959f633a0ffd9a757f2dd2cf9b06f77f"; }
|
||||
interface ReadingServices source { repository "https://repos.quixos.org/quixos-templates/interface-reading-services.git"; commit "ea1ffb89efbc7bdd9c20794cc63dc112055536c8"; }
|
||||
interface PresentationState source { repository "https://repos.quixos.org/quixos-templates/interface-presentation-state.git"; commit "b8f27e813b05231c9d3b4d3692e91b8fc2936dec"; }
|
||||
interface PresentationMount source { repository "https://repos.quixos.org/quixos-templates/interface-presentation-mount.git"; commit "4a322809adfbb50494c9d5dd9889b56c907e737d"; }
|
||||
quixos source { repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git"; commit "488f7e229afed8f03fc3bfa4abe707da0c613460"; }
|
||||
interface ReadingVocabulary source { repository "https://repos.quixos.org/quixos-templates/interface-reading-vocabulary.git"; commit "95f608afeeb255e2eb6c00d22ccc2d8efa13742d"; }
|
||||
interface AgentContextSubject source { repository "https://repos.quixos.org/quixos-templates/interface-agent-context-subject.git"; commit "f913c253733ff9632bec21758af76330665a3437"; }
|
||||
interface AgentDocumentation source { repository "https://repos.quixos.org/quixos-templates/interface-agent-documentation.git"; commit "fa4a362880335fb573f437299b8860b6a7292812"; }
|
||||
interface AgentWorkspace source { repository "https://repos.quixos.org/quixos-templates/interface-agent-workspace.git"; commit "c00f8149e01966ef130a834ddf1ee6155f3c01ef"; }
|
||||
interface AgentDirectory source { repository "https://repos.quixos.org/quixos-templates/interface-agent-directory.git"; commit "9dcf546fb4920c59b9532038926668f1b7a2fdd0"; }
|
||||
interface Agent source { repository "https://repos.quixos.org/quixos-templates/interface-agent.git"; commit "e5ebdc6b0a69704c687accb8420a1d808e427afd"; }
|
||||
interface AgentRequest source { repository "https://repos.quixos.org/quixos-templates/interface-agent-request.git"; commit "e5143427c37b6dc8bbf1b542f95bf08473905f9d"; }
|
||||
interface EventStream source { repository "https://repos.quixos.org/quixos-templates/interface-event-stream.git"; commit "165b9538e6987e79b009c14185db9dcc1b09072e"; }
|
||||
interface EventHandler source { repository "https://repos.quixos.org/quixos-templates/interface-event-handler.git"; commit "06d2b6cee68bdb2fcc562ffa2e2a323a079d7c07"; }
|
||||
interface EventSubscription source { repository "https://repos.quixos.org/quixos-templates/interface-event-subscription.git"; commit "d9c1155a7c78f9b4364c85989577e87983c72cc8"; }
|
||||
interface WebStudioCanvas source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-canvas.git"; commit "11814ea4fb3d7a29dfae180eccd9392ff81143ff"; }
|
||||
interface WebStudioCanvasPlacement source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-canvas-placement.git"; commit "15fe89143b0a6d568716119583166c17559154dd"; }
|
||||
interface WebStudioPlaceable source { repository "https://repos.quixos.org/quixos-templates/interface-web-studio-placeable.git"; commit "a1018f0eac5cbf32ae12a5f96a69f088a6cc6572"; }
|
||||
interface LanguageReader source { repository "https://repos.quixos.org/quixos-templates/interface-language-reader.git"; commit "8de22a0d48b96a31651eb15c4fb4c8a7e244f9e6"; }
|
||||
interface ReadingReader source { repository "https://repos.quixos.org/quixos-templates/interface-reading-reader.git"; commit "f00914748db8594df1a9916d933e7ed801e9beef"; }
|
||||
interface ReadingDocument source { repository "https://repos.quixos.org/quixos-templates/interface-reading-document.git"; commit "7f3c95fc9a4b7bb3ad3c695bb58a74a10db290d3"; }
|
||||
interface ReadingFeature source { repository "https://repos.quixos.org/quixos-templates/interface-reading-feature.git"; commit "3533a0f2cedd3b5993ae08986d7f6a4030c28445"; }
|
||||
interface ReadingJob source { repository "https://repos.quixos.org/quixos-templates/interface-reading-job.git"; commit "71deaa6f479bf226a55ecc2c8b15130f855c6972"; }
|
||||
interface ReadingRequest source { repository "https://repos.quixos.org/quixos-templates/interface-reading-request.git"; commit "ba1275a646de6590b78d66820a4c214dcef03964"; }
|
||||
interface ReadingWord source { repository "https://repos.quixos.org/quixos-templates/interface-reading-word.git"; commit "2623679d1259ec5646a66cae94c78b886416fd36"; }
|
||||
interface ReadingNavigation source { repository "https://repos.quixos.org/quixos-templates/interface-reading-navigation.git"; commit "8f90502dcf3626bca59044cf68709c4db0a3cfac"; }
|
||||
interface ReadingServices source { repository "https://repos.quixos.org/quixos-templates/interface-reading-services.git"; commit "02d0b41c6f1b25439318e2764f6499c2bd6d04ba"; }
|
||||
interface PresentationState source { repository "https://repos.quixos.org/quixos-templates/interface-presentation-state.git"; commit "ac0194a32b7773d6f738c94371c5b610d1946521"; }
|
||||
interface PresentationMount source { repository "https://repos.quixos.org/quixos-templates/interface-presentation-mount.git"; commit "52d9ef58de0aa8b1c1ba7d38c2606ea51511b6c2"; }
|
||||
}
|
||||
|
||||
+14
-1
@@ -11,7 +11,20 @@ export function cacheConfiguration(
|
||||
return JSON.stringify({
|
||||
processor: identity,
|
||||
scope,
|
||||
options: Object.fromEntries([...inputs].sort().map((key) => [key, options[key]])),
|
||||
options: Object.fromEntries(
|
||||
[...inputs]
|
||||
.sort()
|
||||
.map((key) => [
|
||||
key,
|
||||
key
|
||||
.split(".")
|
||||
.reduce<unknown>(
|
||||
(value, part) =>
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>)[part] : undefined,
|
||||
options,
|
||||
),
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+134
-48
@@ -1,18 +1,21 @@
|
||||
import type { Processor } from "./processors.js";
|
||||
|
||||
export type ConfigValue = string | number | boolean;
|
||||
export type AnnotatorConfig = Record<string, ConfigValue>;
|
||||
export type ConfigValue = string | number | boolean | AnnotatorConfig;
|
||||
export type AnnotatorConfig = { [key: string]: ConfigValue };
|
||||
export type ConfigField = {
|
||||
key: string;
|
||||
label: string;
|
||||
kind: "color" | "number" | "select" | "toggle" | "text";
|
||||
default: ConfigValue;
|
||||
default: string | number | boolean;
|
||||
presentation?: "font" | "underline" | "position";
|
||||
options?: { value: string; label: string }[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
/** False for display-only controls: changing them must reuse inference. */
|
||||
computation?: boolean;
|
||||
};
|
||||
export type ConfigGroup = { key: string; label: string; kind: "group"; fields: ConfigNode[] };
|
||||
export type ConfigNode = ConfigField | ConfigGroup;
|
||||
export const readingProfiles = [
|
||||
"light-fast",
|
||||
"light",
|
||||
@@ -23,61 +26,144 @@ export const readingProfiles = [
|
||||
"cerebras-light",
|
||||
"cerebras-balanced",
|
||||
];
|
||||
export function fieldsFor(processor: Processor): ConfigField[] {
|
||||
export function fieldsFor(processor: Processor): ConfigNode[] {
|
||||
return typeof processor.config === "function" ? processor.config() : (processor.config ?? []);
|
||||
}
|
||||
function leaves(fields: ConfigNode[], prefix = ""): { path: string; field: ConfigField }[] {
|
||||
return fields.flatMap((field) =>
|
||||
field.kind === "group" ? leaves(field.fields, prefix + field.key + ".") : [{ path: prefix + field.key, field }],
|
||||
);
|
||||
}
|
||||
export function computationFields(processor: Processor) {
|
||||
return (
|
||||
processor.cache?.inputs ??
|
||||
fieldsFor(processor)
|
||||
.filter((field) => field.computation !== false && field.kind !== "color")
|
||||
.map((field) => field.key)
|
||||
leaves(fieldsFor(processor))
|
||||
.filter(({ field }) => field.computation !== false)
|
||||
.map(({ path }) => path)
|
||||
);
|
||||
}
|
||||
export function normalizeConfig(processor: Processor, raw: string): AnnotatorConfig {
|
||||
if (raw.length > 16000) throw Error("Annotation settings are too large");
|
||||
const value = JSON.parse(raw);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Expected annotation settings");
|
||||
const fields: ConfigField[] = [
|
||||
{
|
||||
key: "placement",
|
||||
label: "Label position",
|
||||
kind: "select",
|
||||
default: "above",
|
||||
computation: false,
|
||||
options: [
|
||||
{ value: "above", label: "Above the text" },
|
||||
{ value: "below", label: "Below the text" },
|
||||
],
|
||||
},
|
||||
{ key: "fontFamily", label: "Font", kind: "text", default: "" },
|
||||
{ key: "fontSize", label: "Font size", kind: "number", default: 13, min: 6, max: 72 },
|
||||
{
|
||||
key: "underlinePattern",
|
||||
label: "Underline pattern",
|
||||
kind: "select",
|
||||
default: "solid",
|
||||
computation: false,
|
||||
options: ["solid", "dashed", "squiggly"].map((value) => ({ value, label: value })),
|
||||
},
|
||||
...fieldsFor(processor),
|
||||
];
|
||||
if (new Set(fields.map((field) => field.key)).size !== fields.length) throw Error("Duplicate annotation control");
|
||||
for (const key of Object.keys(value))
|
||||
if (!fields.some((field) => field.key === key)) throw Error("Unknown annotation setting: " + key);
|
||||
const config: AnnotatorConfig = {};
|
||||
for (const field of fields) {
|
||||
const entry = Object.hasOwn(value, field.key) ? value[field.key] : field.default;
|
||||
function normalize(fields: ConfigNode[], value: unknown, path = "", depth = 0): AnnotatorConfig {
|
||||
if (depth > 8 || !value || typeof value !== "object" || Array.isArray(value))
|
||||
throw Error("Expected annotation settings: " + path);
|
||||
const source = value as Record<string, unknown>;
|
||||
if (new Set(fields.map((field) => field.key)).size !== fields.length)
|
||||
throw Error("Duplicate annotation control: " + path);
|
||||
if (
|
||||
typeof entry !== typeof field.default ||
|
||||
(typeof entry === "number" &&
|
||||
(!Number.isFinite(entry) || entry < (field.min ?? -Infinity) || entry > (field.max ?? Infinity))) ||
|
||||
(typeof entry === "string" && (entry.length > 160 || /[\r\n\x00]/.test(entry))) ||
|
||||
(field.kind === "color" && (typeof entry !== "string" || !/^#[0-9a-f]{6}$/i.test(entry))) ||
|
||||
(field.kind === "select" && !field.options?.some((option) => option.value === entry))
|
||||
fields.some(
|
||||
(field) =>
|
||||
!/^[A-Za-z][A-Za-z0-9_]*$/.test(field.key) || ["constructor", "prototype", "__proto__"].includes(field.key),
|
||||
)
|
||||
)
|
||||
throw Error("Invalid annotation setting: " + field.label);
|
||||
config[field.key] = entry;
|
||||
throw Error("Invalid annotation control key");
|
||||
for (const key of Object.keys(source))
|
||||
if (!fields.some((field) => field.key === key)) throw Error("Unknown annotation setting: " + path + key);
|
||||
return Object.fromEntries(
|
||||
fields.map((field) => {
|
||||
const present = Object.hasOwn(source, field.key);
|
||||
if (field.kind === "group")
|
||||
return [
|
||||
field.key,
|
||||
normalize(field.fields, present ? source[field.key] : {}, path + field.key + ".", depth + 1),
|
||||
];
|
||||
const entry = present ? source[field.key] : field.default;
|
||||
if (
|
||||
typeof entry !== typeof field.default ||
|
||||
(typeof entry === "number" &&
|
||||
(!Number.isFinite(entry) || entry < (field.min ?? -Infinity) || entry > (field.max ?? Infinity))) ||
|
||||
(typeof entry === "string" && (entry.length > 160 || /[\r\n\x00]/.test(entry))) ||
|
||||
(field.kind === "color" && (typeof entry !== "string" || !/^#[0-9a-f]{6}$/i.test(entry))) ||
|
||||
(field.kind === "select" && !field.options?.some((option) => option.value === entry))
|
||||
)
|
||||
throw Error("Invalid annotation setting: " + path + field.label);
|
||||
return [field.key, entry as ConfigValue];
|
||||
}),
|
||||
);
|
||||
}
|
||||
return config;
|
||||
return normalize(fieldsFor(processor), JSON.parse(raw));
|
||||
}
|
||||
|
||||
/** Reusable, opt-in structs. Keys let one annotator compose several independent sets. */
|
||||
export function underlineControls(
|
||||
key: string,
|
||||
label: string,
|
||||
defaults: { color?: string; pattern?: "solid" | "dashed" | "squiggly" } = {},
|
||||
): ConfigGroup {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
kind: "group",
|
||||
fields: [
|
||||
{ key: "color", label: "Color", kind: "color", default: defaults.color ?? "#66866b", computation: false },
|
||||
{
|
||||
key: "underlinePattern",
|
||||
label: "Pattern",
|
||||
kind: "select",
|
||||
presentation: "underline",
|
||||
default: defaults.pattern ?? "solid",
|
||||
computation: false,
|
||||
options: ["solid", "dashed", "squiggly"].map((value) => ({
|
||||
value,
|
||||
label: value[0].toUpperCase() + value.slice(1),
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
export function labelControls(
|
||||
key: string,
|
||||
label: string,
|
||||
defaults: { color?: string; fontSize?: number; placement?: "above" | "below" } = {},
|
||||
): ConfigGroup {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
kind: "group",
|
||||
fields: [
|
||||
{ key: "color", label: "Color", kind: "color", default: defaults.color ?? "#66866b", computation: false },
|
||||
{ key: "fontFamily", label: "Font", kind: "text", presentation: "font", default: "", computation: false },
|
||||
{
|
||||
key: "fontSize",
|
||||
label: "Font size",
|
||||
kind: "number",
|
||||
default: defaults.fontSize ?? 13,
|
||||
min: 6,
|
||||
max: 72,
|
||||
computation: false,
|
||||
},
|
||||
{
|
||||
key: "placement",
|
||||
label: "Position",
|
||||
kind: "select",
|
||||
presentation: "position",
|
||||
default: defaults.placement ?? "above",
|
||||
computation: false,
|
||||
options: [
|
||||
{ value: "above", label: "Above" },
|
||||
{ value: "below", label: "Below" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
export function colorControl(key = "color", label = "Color", color = "#66866b"): ConfigField {
|
||||
return { key, label, kind: "color", default: color, computation: false };
|
||||
}
|
||||
/** Only declared semantic paths enter a model prompt, preserving nested structs. */
|
||||
export function semanticConfig(processor: Processor, config: AnnotatorConfig): AnnotatorConfig {
|
||||
const result: AnnotatorConfig = {};
|
||||
for (const path of computationFields(processor)) {
|
||||
const parts = path.split(".");
|
||||
let source: ConfigValue | undefined = config;
|
||||
for (const part of parts) source = source && typeof source === "object" ? source[part] : undefined;
|
||||
if (source === undefined) continue;
|
||||
let target = result;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
target[part] ??= {};
|
||||
target = target[part] as AnnotatorConfig;
|
||||
}
|
||||
target[parts.at(-1)!] = source;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+20
-12
@@ -47,9 +47,10 @@ appends sequentially. The UI prepares a few pages ahead, then annotation plannin
|
||||
works on the ready output. Never send an entire long document for rewriting.
|
||||
Ranges refer to UTF-16 offsets in the displayed document; replacement annotation
|
||||
ranges still refer to the original source span, and copying returns that source.
|
||||
ReadingFeature.data configures presentation and reveal; processor implementations
|
||||
live in LanguageReaderRuntime/src/processors.ts. Add actual checked code there,
|
||||
including its computation strategy, then add a feature through the public graph.
|
||||
ReadingFeature.data stores the instance name, enabled state, processor identity
|
||||
and config revision. Processor implementations live in
|
||||
LanguageReaderRuntime/src/processors.ts. Add actual checked code there, including
|
||||
its computation strategy and render channels, then register through the public graph.
|
||||
Read README.md before editing. Chinese and French source examples are workspace
|
||||
authoring examples, not an in-application language selector.
|
||||
Choose preparation with judgment: cheap local or batch computations can prepare
|
||||
@@ -81,29 +82,36 @@ source glyphs, outside canvas clipping. It reuses the existing processor and
|
||||
heavy-hover intent/cache path; never make extra calls from its UI.
|
||||
Renderer styles also include replace, bold and italic. Overhead-hover recolors the
|
||||
corresponding source span without activating word-hover tooltips or computation.
|
||||
Style overhead supports options.placement:"above" (default) or "below"; a
|
||||
Processor.renders hook can also declare placement. Each side keeps fixed source
|
||||
Style overhead supports placement:"above" (default) or "below" on its render
|
||||
channel or explicitly bound config group. Each side keeps fixed source
|
||||
lanes. Position is display-only and must reuse cached semantic values. Long
|
||||
labels receive faint alignment guides, and punctuation retains its normal ink.
|
||||
Short words and fitting overhead labels keep their
|
||||
source together across line/page breaks; oversized runs may still split.
|
||||
Rendering source is LanguageReaderPresentation/src/canvas. It uses measured
|
||||
source spans, fixed rows per annotation source and exact source selection.
|
||||
Each annotator owns its inline configuration: Processor.config can declare fields
|
||||
Instances have no persisted visual type, generation, scope, color or reveal.
|
||||
The processor owns these choices and can emit several render channels. Creation
|
||||
choices are authoring instructions only. Each annotator owns its inline
|
||||
configuration: Processor.config can declare fields and independent nested groups
|
||||
or generate them; config.ts validates options on write. ReadingFeature.options
|
||||
stores a JSON object and configureOptions requires the current configRevision.
|
||||
Raw results persist indefinitely by source revision, target range, processor, scope,
|
||||
actual context and semantic options. Model profile, scheduling, color, font and
|
||||
display thresholds never invalidate semantic facts. An explicit retry regenerates. Processor.cache.inputs
|
||||
explicitly names semantic option keys; otherwise non-color, non-display config fields
|
||||
explicitly names semantic option keys; otherwise declared non-display config fields
|
||||
participate. Mark display-only fields computation:false. prompt(config) receives only
|
||||
semantic inputs. Appearance, typography and visibility filters must run in the
|
||||
presentation's annotation-display.ts; install an annotationDisplays function for
|
||||
custom filtering or multi-color styles. Do not persist decoration in model results.
|
||||
Underlines use the common display-only underlinePattern option: solid (default),
|
||||
dashed or squiggly. Changing the pattern must reuse the existing computation cache.
|
||||
The presentation's annotationDisplays hook can override underlinePattern per item
|
||||
alongside color and font; only these three patterns are valid. All patterns occupy
|
||||
No controls are added automatically. Processor.config is the whole settings schema.
|
||||
Compose underlineControls(key,label,defaults) or labelControls(key,label,defaults)
|
||||
from config.ts as explicit named groups, even several underlines in one annotator.
|
||||
Bind each renders channel with options:"groupKey"; constants stay in unbound code.
|
||||
These structs have their own nested defaults, validation and React presentation.
|
||||
Pattern choices are solid, dashed and squiggly. Changing them reuses the cache.
|
||||
The presentation's annotationDisplays hook can supply underlinePattern per item
|
||||
alongside color and font; respect any exposed appearance controls. Only these three patterns are valid. All patterns occupy
|
||||
the same underline band, so switching appearance must not move source text.
|
||||
Processor.renders projects one cached item through several style/reveal hooks;
|
||||
content:"source" makes a tooltip show the original source without inference.
|
||||
@@ -125,7 +133,7 @@ queue; all nearby word tokens prepare, irrespective of the displayed HSK thresho
|
||||
The presentation's annotator-config.tsx renders declared controls and exposes
|
||||
annotatorConfigViews for purpose-built React components with the same values/change
|
||||
contract. Keep these controls inside the general annotator config, not separate
|
||||
workspace-global settings. Overhead font family/size are common options; mixed
|
||||
workspace-global settings. Opt-in label controls provide font family/size; mixed
|
||||
sizes share a source row baseline using maximum ascent/descent for each line.
|
||||
Library.inferenceProfile defaults to quick-fast (Luna, no reasoning, fast mode),
|
||||
is chosen in code by the author (no model picker), and routes future uncached requests.
|
||||
|
||||
@@ -9,46 +9,21 @@ export const chineseFeatures = [
|
||||
{
|
||||
name: "Pinyin for hard words",
|
||||
processor: "pinyin",
|
||||
style: "overhead",
|
||||
reveal: "always",
|
||||
generation: "section",
|
||||
scope: "word",
|
||||
color: "#66866b",
|
||||
},
|
||||
{
|
||||
name: "Word definition",
|
||||
processor: "definition",
|
||||
style: "sidebar",
|
||||
reveal: "click",
|
||||
generation: "focus",
|
||||
scope: "word",
|
||||
color: "#536b92",
|
||||
},
|
||||
{
|
||||
name: "Sentence definition",
|
||||
processor: "translation",
|
||||
style: "sidebar",
|
||||
reveal: "click",
|
||||
generation: "focus",
|
||||
scope: "sentence",
|
||||
color: "#927d5b",
|
||||
},
|
||||
{
|
||||
name: "Proper nouns",
|
||||
processor: "names",
|
||||
style: "highlight",
|
||||
reveal: "always",
|
||||
generation: "section",
|
||||
scope: "word",
|
||||
color: "#d4ac53",
|
||||
},
|
||||
{
|
||||
name: "Pinyin on hover",
|
||||
processor: "pinyinWord",
|
||||
style: "tooltip",
|
||||
reveal: "hover",
|
||||
generation: "section",
|
||||
scope: "word",
|
||||
color: "#66866b",
|
||||
},
|
||||
];
|
||||
|
||||
+12
-22
@@ -1,17 +1,17 @@
|
||||
import type { Processor } from "../processors.js";
|
||||
import { labelControls, colorControl } from "../config.js";
|
||||
/** Two independent fixed lanes following the current click. */
|
||||
export const frenchProcessors: Record<string, Processor> = {
|
||||
frenchAdvancedEnglish: {
|
||||
label: "Advanced words in English",
|
||||
scope: "word",
|
||||
description:
|
||||
"Replace advanced French vocabulary with short English equivalents where they fit. CEFR levels are estimates.",
|
||||
settings: { color: true },
|
||||
preset: { style: "replace", reveal: "always" },
|
||||
profile: "light-fast",
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
processing: { chunkChars: 600, contextChars: 1600 },
|
||||
config: [
|
||||
colorControl("color", "Replacement ink", "#526c86"),
|
||||
{
|
||||
key: "minimumLevel",
|
||||
label: "Replace vocabulary from",
|
||||
@@ -26,7 +26,7 @@ export const frenchProcessors: Record<string, Processor> = {
|
||||
],
|
||||
cache: { inputs: [] },
|
||||
renders: [
|
||||
{ style: "replace", reveal: "always", content: "result" },
|
||||
{ style: "replace", reveal: "always", content: "result", options: "" },
|
||||
{ style: "tooltip", reveal: "hover", content: "source" },
|
||||
],
|
||||
instructions:
|
||||
@@ -35,11 +35,11 @@ export const frenchProcessors: Record<string, Processor> = {
|
||||
frenchGloss: {
|
||||
profile: "cerebras-light",
|
||||
label: "French word glosses",
|
||||
description: "English meanings above each word in the selected sentence.",
|
||||
settings: { color: true, font: true },
|
||||
preset: { style: "overhead", reveal: "click" },
|
||||
strategy: "focus",
|
||||
scope: "sentence",
|
||||
config: [labelControls("label", "Word glosses", { color: "#526c86" })],
|
||||
renders: [{ style: "overhead", reveal: "click", options: "label" }],
|
||||
description: "English meanings above each word in the selected sentence.",
|
||||
strategy: "focus",
|
||||
processing: { focusChars: 500, contextChars: 1200, beforeSentences: 1, afterSentences: 1 },
|
||||
instructions:
|
||||
"Gloss each French word in the target range in concise English, including function words and repeated words. Use surrounding context to choose the meaning. Return one item per occurrence with the exact supplied JavaScript UTF-16 source offsets. Keep individual glosses short, not whole-sentence translations.",
|
||||
@@ -47,11 +47,11 @@ export const frenchProcessors: Record<string, Processor> = {
|
||||
frenchPassage: {
|
||||
profile: "cerebras-light",
|
||||
label: "French passage translation",
|
||||
description: "An English translation of a few sentences in the upper lane.",
|
||||
settings: { color: true, font: true },
|
||||
preset: { style: "overhead", reveal: "click" },
|
||||
strategy: "focus",
|
||||
scope: "section",
|
||||
config: [labelControls("label", "Passage translation", { color: "#947459" })],
|
||||
renders: [{ style: "overhead", reveal: "click", options: "label" }],
|
||||
description: "An English translation of a few sentences in the upper lane.",
|
||||
strategy: "focus",
|
||||
processing: { focusChars: 1000, contextChars: 1400, beforeSentences: 1, afterSentences: 1 },
|
||||
instructions:
|
||||
"Translate the target French passage into natural English. Return one item for each complete sentence in the target range with its exact source range. Do not include surrounding context in the returned source ranges. Be concise while preserving the meaning.",
|
||||
@@ -61,20 +61,10 @@ export const frenchFeatures = [
|
||||
{
|
||||
name: "Word glosses",
|
||||
processor: "frenchGloss",
|
||||
style: "overhead",
|
||||
reveal: "click",
|
||||
generation: "focus",
|
||||
scope: "sentence",
|
||||
color: "#526c86",
|
||||
},
|
||||
{
|
||||
name: "Passage translation",
|
||||
processor: "frenchPassage",
|
||||
style: "overhead",
|
||||
reveal: "click",
|
||||
generation: "focus",
|
||||
scope: "section",
|
||||
color: "#947459",
|
||||
},
|
||||
];
|
||||
export const frenchDocument = {
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export const plan: PortableImplementation["plan"] = async ({ input, ports }) =>
|
||||
const index = textIndex(document.text),
|
||||
range = { start: input.start, end: input.end };
|
||||
if (range.start >= range.end || range.end > document.text.length) return [];
|
||||
if (input.kind === "focus") return [index.focus(range, feature.scope, policy)];
|
||||
if (input.kind === "focus") return [index.focus(range, processor.scope, policy)];
|
||||
if (input.kind !== "nearby") throw new QxReject("INVALID_READING_PLAN");
|
||||
return index.nearby(range, { start: input.visibleStart, end: input.visibleEnd }, policy);
|
||||
};
|
||||
|
||||
+45
-16
@@ -4,12 +4,10 @@
|
||||
* Never start one background LLM request per token to implement a section pass.
|
||||
*/
|
||||
import type { ProcessingPolicy } from "./text-processing.js";
|
||||
import type { AnnotatorConfig, ConfigField } from "./config.js";
|
||||
import { labelControls, underlineControls, type AnnotatorConfig, type ConfigNode } from "./config.js";
|
||||
export type Processor = {
|
||||
label: string;
|
||||
description?: string;
|
||||
settings?: { color?: boolean; font?: boolean; when?: string[] };
|
||||
preset?: { style: string; reveal: string };
|
||||
strategy: "local" | "section" | "focus";
|
||||
scope: "word" | "sentence" | "section";
|
||||
instructions: string;
|
||||
@@ -18,7 +16,7 @@ export type Processor = {
|
||||
/** Require model ranges to echo their exact original source; opt out only for a documented need. */
|
||||
verifySource?: boolean;
|
||||
processing?: Partial<ProcessingPolicy> | ((config: AnnotatorConfig) => Partial<ProcessingPolicy>);
|
||||
config?: ConfigField[] | (() => ConfigField[]);
|
||||
config?: ConfigNode[] | (() => ConfigNode[]);
|
||||
prompt?: (config: AnnotatorConfig) => string;
|
||||
/** Explicit semantic inputs, excluding color/font/filter preferences. */
|
||||
cache?: {
|
||||
@@ -33,11 +31,17 @@ export type Processor = {
|
||||
};
|
||||
/** Present one semantic data source in several ways, without new model jobs.
|
||||
* Source content keeps the original UTF-16 range (e.g. a replacement tooltip). */
|
||||
renders?: readonly {
|
||||
renders: readonly {
|
||||
style: "overhead" | "highlight" | "underline" | "color" | "sidebar" | "tooltip" | "replace" | "bold" | "italic";
|
||||
reveal: "always" | "click" | "hover";
|
||||
content?: "result" | "source";
|
||||
placement?: "above" | "below";
|
||||
color?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
underlinePattern?: "solid" | "dashed" | "squiggly";
|
||||
/** Explicit path to a code-owned appearance config struct. */
|
||||
options?: string;
|
||||
}[];
|
||||
local?: (text: string, start: number, config?: AnnotatorConfig) => AnnotationResult[];
|
||||
};
|
||||
@@ -48,12 +52,12 @@ export const processors: Record<string, Processor> = {
|
||||
pinyin: {
|
||||
label: "Pinyin for hard words",
|
||||
description: "Show pronunciation above words beyond your HSK level.",
|
||||
settings: { color: true, font: true },
|
||||
preset: { style: "overhead", reveal: "always" },
|
||||
renders: [{ style: "overhead", reveal: "always", options: "label" }],
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
processing: { chunkChars: 160 },
|
||||
config: [
|
||||
labelControls("label", "Pronunciation"),
|
||||
{
|
||||
key: "minimumLevel",
|
||||
label: "Your HSK level",
|
||||
@@ -76,15 +80,15 @@ export const processors: Record<string, Processor> = {
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
processing: { chunkChars: 160 },
|
||||
preset: { style: "tooltip", reveal: "hover" },
|
||||
renders: [{ style: "tooltip", reveal: "hover" }],
|
||||
cache: { identity: "pinyin", inputs: [] },
|
||||
instructions: "",
|
||||
},
|
||||
gloss: {
|
||||
label: "English meaning",
|
||||
config: [labelControls("label", "Gloss appearance")],
|
||||
description: "Short English meanings above useful words.",
|
||||
settings: { color: true, font: true },
|
||||
preset: { style: "overhead", reveal: "always" },
|
||||
renders: [{ style: "overhead", reveal: "always", options: "label" }],
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
instructions:
|
||||
@@ -93,7 +97,7 @@ export const processors: Record<string, Processor> = {
|
||||
grammar: {
|
||||
label: "Parts of speech",
|
||||
description: "Color words by their grammatical role.",
|
||||
preset: { style: "color", reveal: "always" },
|
||||
renders: [{ style: "color", reveal: "always" }],
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
config: () =>
|
||||
@@ -108,6 +112,7 @@ export const processors: Record<string, Processor> = {
|
||||
key: tag + "Color",
|
||||
label: tag[0].toUpperCase() + tag.slice(1),
|
||||
kind: "color" as const,
|
||||
computation: false,
|
||||
default: color,
|
||||
})),
|
||||
instructions:
|
||||
@@ -116,7 +121,7 @@ export const processors: Record<string, Processor> = {
|
||||
names: {
|
||||
label: "Proper nouns",
|
||||
description: "Highlight people, places, and organizations.",
|
||||
preset: { style: "highlight", reveal: "always" },
|
||||
renders: [{ style: "highlight", reveal: "always" }],
|
||||
strategy: "section",
|
||||
scope: "word",
|
||||
config: () =>
|
||||
@@ -124,6 +129,7 @@ export const processors: Record<string, Processor> = {
|
||||
key: tag + "Color",
|
||||
label: tag[0].toUpperCase() + tag.slice(1),
|
||||
kind: "color" as const,
|
||||
computation: false,
|
||||
default: color,
|
||||
})),
|
||||
instructions:
|
||||
@@ -132,16 +138,38 @@ export const processors: Record<string, Processor> = {
|
||||
definition: {
|
||||
label: "Word definition",
|
||||
description: "Click a word for its meaning and usage in the sidebar.",
|
||||
preset: { style: "sidebar", reveal: "click" },
|
||||
renders: [{ style: "sidebar", reveal: "click" }],
|
||||
strategy: "focus",
|
||||
scope: "word",
|
||||
instructions:
|
||||
"Explain the focused Chinese word in its sentence context. Include pinyin, a concise English definition and one usage note. Return one item covering exactly the supplied focus range. Neighboring text is context, not a request to annotate every word.",
|
||||
},
|
||||
definitionTooltip: {
|
||||
label: "Word definition tooltip",
|
||||
description: "Explain a word in a floating note.",
|
||||
strategy: "focus",
|
||||
scope: "word",
|
||||
instructions: "",
|
||||
cache: { identity: "definition", inputs: [] },
|
||||
config: [
|
||||
{
|
||||
key: "reveal",
|
||||
label: "Show definition",
|
||||
kind: "select",
|
||||
default: "hover",
|
||||
computation: false,
|
||||
options: [
|
||||
{ value: "hover", label: "When hovering" },
|
||||
{ value: "click", label: "When clicked" },
|
||||
],
|
||||
},
|
||||
],
|
||||
renders: [{ style: "tooltip", reveal: "hover", options: "" }],
|
||||
},
|
||||
passageTranslation: {
|
||||
label: "Passage translation",
|
||||
description: "English translations alongside the passage you are reading.",
|
||||
preset: { style: "sidebar", reveal: "click" },
|
||||
renders: [{ style: "sidebar", reveal: "click" }],
|
||||
strategy: "section",
|
||||
scope: "sentence",
|
||||
instructions:
|
||||
@@ -150,7 +178,7 @@ export const processors: Record<string, Processor> = {
|
||||
translation: {
|
||||
label: "Sentence definition",
|
||||
description: "Click a word to explain the sentence in the sidebar.",
|
||||
preset: { style: "sidebar", reveal: "click" },
|
||||
renders: [{ style: "sidebar", reveal: "click" }],
|
||||
strategy: "focus",
|
||||
scope: "sentence",
|
||||
instructions:
|
||||
@@ -159,7 +187,8 @@ export const processors: Record<string, Processor> = {
|
||||
punctuation: {
|
||||
label: "Sentence punctuation",
|
||||
description: "Mark sentence endings with an underline.",
|
||||
preset: { style: "underline", reveal: "always" },
|
||||
config: [underlineControls("underline", "Punctuation underline")],
|
||||
renders: [{ style: "underline", reveal: "always", options: "underline" }],
|
||||
strategy: "local",
|
||||
scope: "section",
|
||||
instructions: "",
|
||||
|
||||
+42
-52
@@ -18,7 +18,14 @@ import { processorSource, annotationTaskSource } from "./source-version.js";
|
||||
import { processorIdentity } from "./processor-identity.js";
|
||||
import { createVariant, parseVariant, nextSection, applySection, variantText } from "./variants.js";
|
||||
import { textIndex, processingPolicy } from "./text-processing.js";
|
||||
import { fieldsFor, normalizeConfig, computationFields, readingProfiles, type AnnotatorConfig } from "./config.js";
|
||||
import {
|
||||
fieldsFor,
|
||||
normalizeConfig,
|
||||
computationFields,
|
||||
readingProfiles,
|
||||
type AnnotatorConfig,
|
||||
semanticConfig,
|
||||
} from "./config.js";
|
||||
|
||||
const as = <Id extends string>(ref: ExactReference, id: Id): ManagedReference<"interface", Id> => ({
|
||||
...ref,
|
||||
@@ -63,6 +70,10 @@ Inspect existing component patterns, design intentional layouts, and check them
|
||||
in a browser. Independent UI implementation/design can use a bounded subagent;
|
||||
join it before checking or activating the workspace. Prefer native retained
|
||||
navigation and CRDT drafts over browser-only page or input state.
|
||||
Annotator instances store implementation identity and declared options, not a single
|
||||
visual type. Their code owns every render channel and every settings control.
|
||||
Use explicit nested control groups or custom React config views; never infer
|
||||
controls from an output style or override a setting the user can edit.
|
||||
Keep hover effects from reflowing text. Changes to font metrics, source content
|
||||
or spacing need permanent or click activation; hover can change ink or show an
|
||||
anchored tooltip while the text stays in place.
|
||||
@@ -77,7 +88,9 @@ heavy per-word requests must stay focused, cached, and debounced on hover. Revea
|
||||
timing is independent. Add checked processor code (and UI or contracts if needed),
|
||||
run qx-workspace check for affected packages and root, then automatically activate
|
||||
the successful candidate. Finally register the feature through LanguageReader.addFeature
|
||||
with the appropriate processor, style, scope, generation, and options (JSON string).
|
||||
with its processor and options (JSON string). Instances do not store a visual type,
|
||||
color, reveal mode, scope, or generation strategy: the checked processor owns
|
||||
computation and every render channel. An annotator can render several ways at once.
|
||||
Cache raw semantic results through the shared cache wrapper. Explicitly declare
|
||||
Processor.cache.inputs for options that alter computation. Color, font, display
|
||||
thresholds and reveal preferences must not cause inference. Mark display-only
|
||||
@@ -90,6 +103,14 @@ the same reading. Prefer bounded batch preparation over a request per word.
|
||||
Use Processor.renders when one semantic source needs several appearances. A replace
|
||||
hook already supports colored ink; a hover tooltip with content:"source" shows
|
||||
the original source locally. Do not create extra inference for another appearance.
|
||||
There are no automatic settings based on output style or field names. Reuse
|
||||
underlineControls(key,label,defaults) or labelControls(key,label,defaults) from
|
||||
config.ts when useful; these are independent nested structs with their own UI.
|
||||
Compose several named groups when several channels need different settings.
|
||||
Bind each Processor.renders channel explicitly with options:"groupKey"; unbound
|
||||
channels retain code constants. Custom appearance functions must honor exposed
|
||||
controls, not overwrite them. Add a settings-to-render regression and verify
|
||||
changing each offered control actually changes the output without fresh inference.
|
||||
Inline custom configuration is a core annotator feature: declare or generate config
|
||||
fields in the Processor definition and use the presentation annotationDisplays registry for dynamic
|
||||
colors/fonts/filtering. For bespoke controls, install a React component in
|
||||
@@ -99,12 +120,12 @@ Use the request's reader context when registering the feature (addFeature.reader
|
||||
Before every turn, including follow-ups, read ReadingReader.data, its features,
|
||||
and each feature's current data and options through the graph. The requested
|
||||
configuration is a historical snapshot, not the live configuration. Preserve
|
||||
changes made since that snapshot. The creation choices are authoring instructions, not a permanent universal settings schema. Generate meaningful code-owned settings; built types are fixed unless code changes. Declare Processor.settings only for controls that make sense (color/font/when), and supply Processor.description and preset for premades. Associate registered features with this authoring job via addFeature.authoring, using the request data authoringJob reference. Honor type/reveal suggestions where possible. Generate a concise descriptive title when the requested title is empty.
|
||||
changes made since that snapshot. The creation choices are authoring instructions, not a permanent universal settings schema. Generate meaningful code-owned settings; built types are fixed unless code changes. Declare only meaningful Processor.config fields or nested groups, and supply Processor.description and renders for premades. Associate registered features with this authoring job via addFeature.authoring, using the request data authoringJob reference. Honor type/reveal suggestions where possible. Generate a concise descriptive title when the requested title is empty.
|
||||
Reuse an installed processor type when it already implements the requested behavior;
|
||||
create a new type only when code must change. Removing an annotator detaches that
|
||||
instance; archiveType only hides its processor from the creation library and never
|
||||
removes other instances or cached results. Labels share style:"overhead" with
|
||||
options.placement:"above" or "below"; honor the requested position. A rendering
|
||||
a declared labelControls group and a render channel options binding; honor the requested position. A rendering
|
||||
hook can independently declare placement when one source needs both positions.
|
||||
Do not use hover for behavior that reflows text: labels, replacements, bold,
|
||||
italic, font changes, sizes, or spacing. Use permanent or click activation for
|
||||
@@ -114,7 +135,7 @@ cerebras-light (Cerebras GPT OSS 120B) first for ordinary inference when its con
|
||||
Reusable local/section work should prepare near the reading position. Expensive
|
||||
contextual per-word work should cache, wait for intent, and never fan out one
|
||||
request per word across a document. Code-owned custom config is required where
|
||||
the behavior needs it; generic color/font preferences are optional suggestions. Keep the existing
|
||||
the behavior needs it; shared appearance controls are opt-in code components. Keep the existing
|
||||
app and source working on failure. Ask a focused question only if the feature cannot
|
||||
be implemented sensibly without the answer.`;
|
||||
const submission = (text: string, data: string | null, createdAt: string, overrides: Record<string, unknown> = {}) => ({
|
||||
@@ -133,28 +154,8 @@ const submission = (text: string, data: string | null, createdAt: string, overri
|
||||
causalDepth: 0,
|
||||
...overrides,
|
||||
});
|
||||
function validateFeature(data: {
|
||||
name: string;
|
||||
style: string;
|
||||
reveal: string;
|
||||
generation: string;
|
||||
scope: string;
|
||||
color: string;
|
||||
}) {
|
||||
if (
|
||||
!data.name.trim() ||
|
||||
data.name.length > 100 ||
|
||||
!["overhead", "highlight", "underline", "color", "sidebar", "tooltip", "replace", "bold", "italic"].includes(
|
||||
data.style,
|
||||
) ||
|
||||
!["always", "hover", "click"].includes(data.reveal) ||
|
||||
(data.style === "tooltip" && data.reveal === "always") ||
|
||||
(["overhead", "replace", "bold", "italic"].includes(data.style) && data.reveal === "hover") ||
|
||||
!["local", "section", "focus"].includes(data.generation) ||
|
||||
!["word", "sentence", "section"].includes(data.scope) ||
|
||||
!/^#[0-9a-f]{6}$/i.test(data.color)
|
||||
)
|
||||
throw new QxReject("INVALID_READING_FEATURE");
|
||||
function validateFeature(data: { name: string }) {
|
||||
if (!data.name.trim() || data.name.length > 100) throw new QxReject("INVALID_READING_FEATURE");
|
||||
}
|
||||
const emptyJob = (key: string, kind: string, createdAt: string) => ({
|
||||
key,
|
||||
@@ -195,7 +196,7 @@ function computation(processorName: string, scope: string, generation: string, o
|
||||
}
|
||||
async function annotationKey(
|
||||
document: { sourceRevision: string; text: string },
|
||||
feature: { processor: string; scope: string; generation: string },
|
||||
feature: { processor: string },
|
||||
options: AnnotatorConfig,
|
||||
start: number,
|
||||
end: number,
|
||||
@@ -209,7 +210,7 @@ async function annotationKey(
|
||||
const context = index.context(range, policy);
|
||||
return `${await processorRevision(feature.processor)}:${await hashIdentity("reading:annotation", [
|
||||
document.sourceRevision,
|
||||
computation(feature.processor, feature.scope, feature.generation, options),
|
||||
computation(feature.processor, processor.scope, processor.strategy, options),
|
||||
String(start),
|
||||
String(end),
|
||||
JSON.stringify(index.tokens(range)),
|
||||
@@ -333,7 +334,7 @@ export const registry = createRegistry({
|
||||
authoring: null,
|
||||
data: { ...feature, enabled: true, configRevision: 1 },
|
||||
library,
|
||||
options: "{}",
|
||||
options: JSON.stringify(normalizeConfig(processors[feature.processor], "{}")),
|
||||
},
|
||||
{ key: "feature:" + index },
|
||||
);
|
||||
@@ -473,9 +474,7 @@ export const registry = createRegistry({
|
||||
config: JSON.stringify({
|
||||
profile: value.profile,
|
||||
fields: fieldsFor(value),
|
||||
settings: value.settings ?? {},
|
||||
description: value.description ?? "",
|
||||
preset: value.preset,
|
||||
renders: value.renders,
|
||||
cache: {
|
||||
identity: value.cache?.identity ?? key,
|
||||
@@ -602,7 +601,7 @@ export const registry = createRegistry({
|
||||
authoring: null,
|
||||
data: { ...feature, configRevision: 1, enabled: true },
|
||||
library: iface(receiver, "library"),
|
||||
options: "{}",
|
||||
options: JSON.stringify(normalizeConfig(processors[feature.processor], "{}")),
|
||||
},
|
||||
{ key: `feature:${index}` },
|
||||
);
|
||||
@@ -633,12 +632,7 @@ export const registry = createRegistry({
|
||||
addFeature: async ({ receiver, input, ports }) => {
|
||||
validateFeature(input);
|
||||
const processor = processors[input.processor];
|
||||
if (
|
||||
!processor ||
|
||||
(processor.strategy === "focus" && input.generation !== "focus") ||
|
||||
(processor.strategy === "local" && input.generation !== "local")
|
||||
)
|
||||
throw new QxReject("INVALID_PROCESSOR_STRATEGY");
|
||||
if (!processor) throw new QxReject("INVALID_PROCESSOR_STRATEGY");
|
||||
const { options: raw, reader: requestedReader, authoring, ...data } = input;
|
||||
const reader = requestedReader
|
||||
? iface(requestedReader, "reader")
|
||||
@@ -670,12 +664,7 @@ export const registry = createRegistry({
|
||||
if (current.configRevision !== input.expectedRevision) throw new QxReject("READING_FEATURE_CHANGED");
|
||||
validateFeature(input);
|
||||
const processor = processors[current.processor];
|
||||
if (
|
||||
!processor ||
|
||||
(processor.strategy === "focus" && input.generation !== "focus") ||
|
||||
(processor.strategy === "local" && input.generation !== "local")
|
||||
)
|
||||
throw new QxReject("INVALID_PROCESSOR_STRATEGY");
|
||||
if (!processor) throw new QxReject("INVALID_PROCESSOR_STRATEGY");
|
||||
const { expectedRevision, ...changes } = input;
|
||||
await ports.data.set({ ...current, ...changes, configRevision: current.configRevision + 1 });
|
||||
return null;
|
||||
@@ -752,7 +741,7 @@ export const registry = createRegistry({
|
||||
);
|
||||
const index = textIndex(document.text),
|
||||
range = { start: input.start, end: input.end };
|
||||
const maximum = feature.generation === "focus" ? policy.focusChars : policy.chunkChars;
|
||||
const maximum = processor.strategy === "focus" ? policy.focusChars : policy.chunkChars;
|
||||
if (
|
||||
input.end <= input.start ||
|
||||
input.end > document.text.length ||
|
||||
@@ -781,7 +770,7 @@ export const registry = createRegistry({
|
||||
processors[processor.cache?.identity ?? feature.processor]?.cache?.previousRevisions?.length)
|
||||
) {
|
||||
const revision = await processorRevision(feature.processor);
|
||||
const config = computation(feature.processor, feature.scope, feature.generation, options);
|
||||
const config = computation(feature.processor, processor.scope, processor.strategy, options);
|
||||
const history = await ports.jobs["jobs.resolve"]({ receiver: iface(receiver, "document"), input: null });
|
||||
for (let offset = 0; offset < history.length; offset += 32) {
|
||||
const batch = history.slice(offset, offset + 32);
|
||||
@@ -808,12 +797,12 @@ export const registry = createRegistry({
|
||||
}
|
||||
}
|
||||
}
|
||||
const semanticOptions = Object.fromEntries(computationFields(processor).map((key) => [key, options[key]]));
|
||||
const semanticOptions = semanticConfig(processor, options);
|
||||
const local = processor.local?.(document.text.slice(input.start, input.end), input.start, semanticOptions);
|
||||
const job = await ports.job.create(
|
||||
{
|
||||
reader: null,
|
||||
configuration: computation(feature.processor, feature.scope, feature.generation, options),
|
||||
configuration: computation(feature.processor, processor.scope, processor.strategy, options),
|
||||
data: {
|
||||
...emptyJob(key, "annotation", input.createdAt),
|
||||
featureRevision: feature.configRevision,
|
||||
@@ -848,7 +837,7 @@ export const registry = createRegistry({
|
||||
const tokens = index.tokens(range),
|
||||
contextRange = index.context(range, policy);
|
||||
const task = JSON.stringify({
|
||||
scope: feature.scope,
|
||||
scope: processor.scope,
|
||||
start: input.start,
|
||||
end: input.end,
|
||||
text: excerpt,
|
||||
@@ -863,7 +852,7 @@ export const registry = createRegistry({
|
||||
{
|
||||
receiver: agentRef(agent),
|
||||
input: submission(
|
||||
`${processors[processor.cache?.identity ?? feature.processor].instructions}\n${processor.prompt?.(Object.fromEntries(computationFields(processor).map((key) => [key, options[key]]))) ?? ""}\nApply this task to task_data. Return the configured JSON shape. Use original UTF-16 offsets.${processor.verifySource !== false ? " Copy exact original source text into each item.source." : ""}`,
|
||||
`${processors[processor.cache?.identity ?? feature.processor].instructions}\n${processor.prompt?.(semanticConfig(processor, options)) ?? ""}\nApply this task to task_data. Return the configured JSON shape. Use original UTF-16 offsets.${processor.verifySource !== false ? " Copy exact original source text into each item.source." : ""}`,
|
||||
task,
|
||||
input.createdAt,
|
||||
{ publication: key, profile, response: response(annotationSchemaFor(processor)) },
|
||||
@@ -1098,6 +1087,7 @@ export const registry = createRegistry({
|
||||
if (data.kind === "annotation") {
|
||||
if (!document || !feature || !processors[feature.processor]) currentAnnotation = false;
|
||||
else {
|
||||
const processor = processors[feature.processor];
|
||||
const options = normalizeConfig(
|
||||
processors[feature.processor],
|
||||
await ports.feature_api["options.get"]({ receiver: featureRef!, input: null }),
|
||||
@@ -1109,7 +1099,7 @@ export const registry = createRegistry({
|
||||
acceptedProcessorRevision(data.key, feature.processor, revision) &&
|
||||
sameCacheConfiguration(
|
||||
await ports.configuration.get(),
|
||||
computation(feature.processor, feature.scope, feature.generation, options),
|
||||
computation(feature.processor, processor.scope, processor.strategy, options),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+60
-42
@@ -23,46 +23,64 @@ assert.deepEqual(processors.punctuation.local!("你好。", 4), [
|
||||
]);
|
||||
console.log("PASS structured annotation validation, scope limits and processor strategies");
|
||||
|
||||
import { normalizeConfig, fieldsFor } from "../src/config.js";
|
||||
import { normalizeConfig, fieldsFor, underlineControls, labelControls, semanticConfig } from "../src/config.js";
|
||||
const defaultOptions = normalizeConfig(processors.grammar, "{}");
|
||||
assert.equal(defaultOptions.nounColor, "#6a8aac");
|
||||
assert.equal(fieldsFor(processors.grammar).length, 6);
|
||||
const customized = normalizeConfig(
|
||||
processors.grammar,
|
||||
JSON.stringify({ nounColor: "#112233", fontFamily: "monospace", fontSize: 24 }),
|
||||
);
|
||||
assert.equal(customized.fontSize, 24);
|
||||
assert.equal(defaultOptions.underlinePattern, "solid");
|
||||
assert.equal(defaultOptions.placement, "above");
|
||||
assert.equal(normalizeConfig(processors.pinyin, '{"placement":"below"}').placement, "below");
|
||||
for (const placement of ["under", "", null, 1])
|
||||
assert.throws(() => normalizeConfig(processors.pinyin, JSON.stringify({ placement })), /Label position/);
|
||||
for (const underlinePattern of ["solid", "dashed", "squiggly"]) {
|
||||
assert.equal(
|
||||
normalizeConfig(processors.grammar, JSON.stringify({ underlinePattern })).underlinePattern,
|
||||
underlinePattern,
|
||||
);
|
||||
}
|
||||
for (const underlinePattern of ["wavy", "", null, 1]) {
|
||||
assert.throws(() => normalizeConfig(processors.grammar, JSON.stringify({ underlinePattern })), /Underline pattern/);
|
||||
}
|
||||
assert(!Object.hasOwn(defaultOptions, "underlinePattern"));
|
||||
assert(!Object.hasOwn(defaultOptions, "fontSize"));
|
||||
for (const options of [
|
||||
"[]",
|
||||
"null",
|
||||
'{"unknown":true}',
|
||||
'{"fontSize":0}',
|
||||
'{"fontSize":null}',
|
||||
'{"fontSize":80}',
|
||||
'{"fontSize":"24"}',
|
||||
'{"nounColor":"red"}',
|
||||
'{"minimumLevel":"not-a-level"}',
|
||||
'{"fontFamily":"bad\\nfont"}',
|
||||
]) {
|
||||
assert.throws(() =>
|
||||
normalizeConfig(options.includes("minimumLevel") ? processors.pinyin : processors.grammar, options),
|
||||
);
|
||||
}
|
||||
'{"underlinePattern":"dashed"}',
|
||||
'{"placement":"below"}',
|
||||
'{"fontSize":24}',
|
||||
'{"fontFamily":"monospace"}',
|
||||
])
|
||||
assert.throws(() => normalizeConfig(processors.grammar, options), /Unknown annotation setting/);
|
||||
const grouped = {
|
||||
...processors.punctuation,
|
||||
config: [
|
||||
underlineControls("primary", "Primary", { pattern: "dashed" }),
|
||||
underlineControls("secondary", "Secondary"),
|
||||
labelControls("label", "Labels"),
|
||||
{
|
||||
key: "meaning",
|
||||
label: "Meaning",
|
||||
kind: "group" as const,
|
||||
fields: [{ key: "language", label: "Language", kind: "text" as const, default: "English" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = normalizeConfig(
|
||||
grouped,
|
||||
JSON.stringify({
|
||||
primary: { underlinePattern: "squiggly", color: "#112233" },
|
||||
secondary: { underlinePattern: "solid" },
|
||||
label: { placement: "below", fontSize: 24 },
|
||||
meaning: { language: "French" },
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(options.primary, { color: "#112233", underlinePattern: "squiggly" });
|
||||
assert.deepEqual(options.secondary, { color: "#66866b", underlinePattern: "solid" });
|
||||
assert.deepEqual(semanticConfig(grouped, options), { meaning: { language: "French" } });
|
||||
assert.throws(() => normalizeConfig(grouped, '{"primary":{"underlinePattern":"wavy"}}'), /primary.Pattern/);
|
||||
assert.throws(() => normalizeConfig(grouped, '{"primary":{"color":"red"}}'), /primary.Color/);
|
||||
assert.throws(() => normalizeConfig(grouped, '{"primary":"dashed"}'), /Expected annotation settings/);
|
||||
assert.throws(() => normalizeConfig(grouped, '{"primary":{"unknown":true}}'), /primary.unknown/);
|
||||
assert.throws(() => normalizeConfig(grouped, '{"label":{"fontSize":0}}'), /label.Font size/);
|
||||
assert.throws(() => normalizeConfig(grouped, '{"label":{"placement":"under"}}'), /label.Position/);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeConfig({ ...grouped, config: [underlineControls("same", "One"), underlineControls("same", "Two")] }, "{}"),
|
||||
/Duplicate/,
|
||||
);
|
||||
const defaultLabel = normalizeConfig(processors.pinyin, '{"label":{"placement":"below"}}').label;
|
||||
assert.equal(typeof defaultLabel === "object" && defaultLabel.placement, "below");
|
||||
assert.equal(processors.pinyin.prompt, undefined);
|
||||
assert.deepEqual(fieldsFor(processors.definition), [], "Fixed sidebar annotators expose no invented controls");
|
||||
assert.deepEqual(normalizeConfig(processors.definitionTooltip, "{}"), { reveal: "hover" });
|
||||
assert.deepEqual(normalizeConfig(processors.definitionTooltip, '{"reveal":"click"}'), { reveal: "click" });
|
||||
assert.deepEqual(semanticConfig(processors.definitionTooltip, { reveal: "click" }), {});
|
||||
assert.throws(() => normalizeConfig(processors.definitionTooltip, '{"reveal":"always"}'), /Show definition/);
|
||||
console.log("PASS generated annotator config, validation, prompt options and dynamic typography/color");
|
||||
|
||||
import { TextIndex, processingPolicy, textIndex } from "../src/text-processing.js";
|
||||
@@ -183,14 +201,14 @@ assert.equal(
|
||||
);
|
||||
assert.deepEqual(computationFields(processors.names), [], "Per-category colors never reach inference");
|
||||
assert.equal(
|
||||
cached(processors.grammar, normalizeConfig(processors.grammar, '{"underlinePattern":"solid"}')),
|
||||
cached(processors.grammar, normalizeConfig(processors.grammar, '{"underlinePattern":"squiggly"}')),
|
||||
"Changing underline geometry reuses the same computed annotation results",
|
||||
cached(grouped, options),
|
||||
cached(grouped, normalizeConfig(grouped, '{"meaning":{"language":"French"}}')),
|
||||
"Independent nested appearance groups preserve semantic caches",
|
||||
);
|
||||
assert.equal(
|
||||
cached(processors.pinyin, normalizeConfig(processors.pinyin, '{"underlinePattern":"dashed"}')),
|
||||
cached(processors.pinyin, normalizeConfig(processors.pinyin, "{}")),
|
||||
"Common underline appearance never reaches semantic prompt configuration",
|
||||
assert.notEqual(
|
||||
cached(grouped, options),
|
||||
cached(grouped, normalizeConfig(grouped, "{}")),
|
||||
"Nested semantic fields isolate caches",
|
||||
);
|
||||
const custom = { ...processors.pinyin, cache: { inputs: ["targetLanguage"] } };
|
||||
assert.notEqual(
|
||||
|
||||
Reference in New Issue
Block a user