Add checked React field bindings and Web Studio factories

Replace opaque props with checked generic presentation contracts and lazy typed
interface references. Generate readonly/writable field APIs and component checks.

Preserve CRDT editing through explicit resolved getter/setter contracts, binding-
fenced delta RPCs, native watches and replica-aware field adapters. Custom setters
retain semantic writes; storage snapshots never grant write authority. Cover
concurrent edits, lost acknowledgements, readonly contracts and authorization.

Add receiver-free static factory dispatch, state-field binding shorthand, and
conformance-based creation. Migrate TODO, editable scaffolds and authoring guides.
Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production
browser bundling and CRDT persistence with temporary PostgreSQL.
This commit is contained in:
Timothy J. Aveni
2026-09-16 16:49:09 -07:00
parent 52803dda05
commit 59735c5e38
22 changed files with 3034 additions and 2300 deletions
File diff suppressed because one or more lines are too long
@@ -21,100 +21,101 @@ INPUT=20
CONFORM=21
AS=22
BIND=23
TO=24
PRIVATE=25
SHARED=26
STATE=27
EDGE=28
PROJECTION=29
WITH=30
USING=31
VIA=32
MATERIALIZE=33
IF=34
ABSENT=35
ON=36
POLICY=37
DEFAULT=38
SOURCE=39
REPOSITORY=40
COMMIT=41
REVISION=42
SEMANTIC_MAJOR=43
ON_DELETE=44
RETAIN_OTHER=45
KEYED=46
PUBLIC_TRAVERSAL=47
ID=48
DOC=49
MODE=50
EMITS=51
RECEIVER=52
REQUIRES=53
ANY=54
GET=55
SET=56
WATCH=57
START=58
STOP=59
READ=60
WRITE=61
RESOLVE=62
CONNECT=63
DISCONNECT=64
CALL=65
WATCH_START=66
WATCH_STOP=67
SUBSCRIBE=68
UNSUBSCRIBE=69
OPTIMISTIC_REGISTER=70
CRDT=71
OPTIONAL_ONE=72
EXACTLY_ONE=73
MANY_UNIQUE=74
MANY=75
ORDERED=76
UNIT=77
WATCH_HANDLE=78
MESSAGE=79
ATOM_REF=80
INTERFACE_REF=81
OPTIONAL=82
LIST=83
RECORD=84
BOOL=85
BYTES=86
DOUBLE=87
INT32=88
INT64=89
STRING=90
UINT32=91
UINT64=92
TRUE=93
FALSE=94
NULL=95
ARROW=96
COLON=97
SEMI=98
COMMA=99
DOT=100
LBRACE=101
RBRACE=102
LBRACK=103
RBRACK=104
LPAREN=105
RPAREN=106
LT=107
GT=108
AMP=109
EQUAL=110
INTEGER=111
JSON_NUMBER=112
IDENTIFIER=113
STRING_LITERAL=114
LINE_COMMENT=115
BLOCK_COMMENT=116
WS=117
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
'workspace'=1
'type'=2
'object'=3
@@ -138,90 +139,91 @@ WS=117
'conform'=21
'as'=22
'bind'=23
'to'=24
'private'=25
'shared'=26
'state'=27
'edge'=28
'projection'=29
'with'=30
'using'=31
'via'=32
'materialize'=33
'if'=34
'absent'=35
'on'=36
'policy'=37
'default'=38
'source'=39
'repository'=40
'commit'=41
'revision'=42
'semantic-major'=43
'on-delete'=44
'retain-other'=45
'keyed'=46
'public-traversal'=47
'id'=48
'doc'=49
'mode'=50
'emits'=51
'receiver'=52
'requires'=53
'any'=54
'get'=55
'set'=56
'watch'=57
'start'=58
'stop'=59
'read'=60
'write'=61
'resolve'=62
'connect'=63
'disconnect'=64
'call'=65
'watch-start'=66
'watch-stop'=67
'subscribe'=68
'unsubscribe'=69
'optimistic-register'=70
'crdt'=71
'optional-one'=72
'exactly-one'=73
'many-unique'=74
'many'=75
'ordered'=76
'unit'=77
'watch-handle'=78
'message'=79
'atom-ref'=80
'interface-ref'=81
'optional'=82
'list'=83
'record'=84
'bool'=85
'bytes'=86
'double'=87
'int32'=88
'int64'=89
'string'=90
'uint32'=91
'uint64'=92
'true'=93
'false'=94
'null'=95
'->'=96
':'=97
';'=98
','=99
'.'=100
'{'=101
'}'=102
'['=103
']'=104
'('=105
')'=106
'<'=107
'>'=108
'&'=109
'='=110
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
File diff suppressed because one or more lines are too long
@@ -21,100 +21,101 @@ INPUT=20
CONFORM=21
AS=22
BIND=23
TO=24
PRIVATE=25
SHARED=26
STATE=27
EDGE=28
PROJECTION=29
WITH=30
USING=31
VIA=32
MATERIALIZE=33
IF=34
ABSENT=35
ON=36
POLICY=37
DEFAULT=38
SOURCE=39
REPOSITORY=40
COMMIT=41
REVISION=42
SEMANTIC_MAJOR=43
ON_DELETE=44
RETAIN_OTHER=45
KEYED=46
PUBLIC_TRAVERSAL=47
ID=48
DOC=49
MODE=50
EMITS=51
RECEIVER=52
REQUIRES=53
ANY=54
GET=55
SET=56
WATCH=57
START=58
STOP=59
READ=60
WRITE=61
RESOLVE=62
CONNECT=63
DISCONNECT=64
CALL=65
WATCH_START=66
WATCH_STOP=67
SUBSCRIBE=68
UNSUBSCRIBE=69
OPTIMISTIC_REGISTER=70
CRDT=71
OPTIONAL_ONE=72
EXACTLY_ONE=73
MANY_UNIQUE=74
MANY=75
ORDERED=76
UNIT=77
WATCH_HANDLE=78
MESSAGE=79
ATOM_REF=80
INTERFACE_REF=81
OPTIONAL=82
LIST=83
RECORD=84
BOOL=85
BYTES=86
DOUBLE=87
INT32=88
INT64=89
STRING=90
UINT32=91
UINT64=92
TRUE=93
FALSE=94
NULL=95
ARROW=96
COLON=97
SEMI=98
COMMA=99
DOT=100
LBRACE=101
RBRACE=102
LBRACK=103
RBRACK=104
LPAREN=105
RPAREN=106
LT=107
GT=108
AMP=109
EQUAL=110
INTEGER=111
JSON_NUMBER=112
IDENTIFIER=113
STRING_LITERAL=114
LINE_COMMENT=115
BLOCK_COMMENT=116
WS=117
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
'workspace'=1
'type'=2
'object'=3
@@ -138,90 +139,91 @@ WS=117
'conform'=21
'as'=22
'bind'=23
'to'=24
'private'=25
'shared'=26
'state'=27
'edge'=28
'projection'=29
'with'=30
'using'=31
'via'=32
'materialize'=33
'if'=34
'absent'=35
'on'=36
'policy'=37
'default'=38
'source'=39
'repository'=40
'commit'=41
'revision'=42
'semantic-major'=43
'on-delete'=44
'retain-other'=45
'keyed'=46
'public-traversal'=47
'id'=48
'doc'=49
'mode'=50
'emits'=51
'receiver'=52
'requires'=53
'any'=54
'get'=55
'set'=56
'watch'=57
'start'=58
'stop'=59
'read'=60
'write'=61
'resolve'=62
'connect'=63
'disconnect'=64
'call'=65
'watch-start'=66
'watch-stop'=67
'subscribe'=68
'unsubscribe'=69
'optimistic-register'=70
'crdt'=71
'optional-one'=72
'exactly-one'=73
'many-unique'=74
'many'=75
'ordered'=76
'unit'=77
'watch-handle'=78
'message'=79
'atom-ref'=80
'interface-ref'=81
'optional'=82
'list'=83
'record'=84
'bool'=85
'bytes'=86
'double'=87
'int32'=88
'int64'=89
'string'=90
'uint32'=91
'uint64'=92
'true'=93
'false'=94
'null'=95
'->'=96
':'=97
';'=98
','=99
'.'=100
'{'=101
'}'=102
'['=103
']'=104
'('=105
')'=106
'<'=107
'>'=108
'&'=109
'='=110
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -47,6 +47,7 @@ import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
import { StateFieldBindingDeclContext } from "./QuixosCapabilityParser.js";
import { RelationshipMaterializationDeclContext } from "./QuixosCapabilityParser.js";
import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
@@ -347,6 +348,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.stateFieldBindingDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitStateFieldBindingDecl?: (ctx: StateFieldBindingDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipMaterializationDecl`.
* @param ctx the parse tree
+46 -1
View File
@@ -552,6 +552,7 @@ const lowerOperationMember = (state: LoweringState, context: OperationMemberCont
inputType,
outputType,
mode: "call",
...(context.STATIC() ? { scope: "class" as const } : {}),
},
],
};
@@ -702,7 +703,15 @@ const lowerInterfaceTemplate = (
inputType,
outputType,
operations: [
signature("call", capabilityId.operation(stringValue(operation.stringLiteral(1))), inputType, outputType),
{
...signature(
"call",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
inputType,
outputType,
),
...(operation.STATIC() ? { scope: "class" as const } : {}),
},
],
};
}
@@ -1430,6 +1439,42 @@ const lowerConformance = (
const operationBindings = context
.conformanceItem()
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
const field = item.stateFieldBindingDecl();
if (field) {
const memberName = identifier(field.identifier(0));
const member = interfaceSymbol.definition?.members.find((entry) => entry.displayName === memberName);
const attachment = requireSymbol(
state,
state.attachments,
identifier(field.identifier(1)),
field,
"attachment",
);
if (!member || member.kind !== "value" || attachment?.attachment.kind !== "state") {
loweringIssue(
state,
field,
"invalid-state-field-binding",
"Field shorthand requires a value member and a state slot",
);
return [];
}
const primitives = {
get: "read",
set: "write",
"watch-start": "watch-start",
"watch-stop": "watch-stop",
} as const;
const slotId = attachment.attachment.id;
return member.operations.map((operation) => ({
operationId: operation.id,
binding: {
kind: "state" as const,
slotId,
primitive: primitives[operation.displayName as keyof typeof primitives],
},
}));
}
const bindingContext = item.operationBindingDecl();
if (!bindingContext) {
return [];
+14 -10
View File
@@ -142,7 +142,7 @@ export const scaffoldRecipe = async (
if (react) {
create(
"src/component.tsx",
`// Props are opaque at the platform boundary until capability generics exist.\nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1><p>Edit this component, then run qx-workspace check.</p></section>;\n}\n`,
`// Add a checked props export, select it in quixos.check.json options.react,\n// then use its generated ReactResults type. Scaffold bundle does this for you.\nexport default function Component(_props: {camino: Record<string, never>; render: unknown; dispatch: (action: unknown) => void}) {\n return <section><h1>${name}</h1></section>;\n}\n`,
);
create(
"src/impl/sourceGet.ts",
@@ -180,14 +180,7 @@ export const scaffoldRecipe = async (
bindingOutput: "src/gen/qx.ts",
...(react
? {
options: {
messages: {
"org.quixos.web-studio.ReactProps": {
module: "@quixos/camino-package-runtime",
export: "opaqueReactPropsBinding",
},
},
},
options: { react: { propsExports: [] } },
}
: {}),
}),
@@ -394,9 +387,20 @@ export const scaffoldRecipe = async (
for (const [file, content] of Object.entries(spec.initialFiles)) {
if (
typeof content !== "string" ||
["quixos.lock", "flake.nix", "quixos.toolchain.json", "quixos.check.json", "package.json"].includes(file)
["quixos.lock", "flake.nix", "quixos.toolchain.json", "package.json"].includes(file)
)
throw new Error(`Not an initial authored file: ${file}`);
if (file === "quixos.check.json") {
const check = JSON.parse(content);
if (
check.backend !== "typescript" ||
check.bindingOutput !== "src/gen/qx.ts" ||
Object.keys(check).some((key) => !["backend", "bindingOutput", "options"].includes(key))
)
throw new Error(
"Initial check configuration may customize binding options, not the scaffold verification backend or output",
);
}
const existing = files.findIndex((entry) => entry.file === prefix + file);
if (existing >= 0) files.splice(existing, 1);
create(file, content);
+3
View File
@@ -310,6 +310,9 @@ const main = async () => {
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
const request: StructuralRequest = {
kind: "interface",
// Like package scaffolding, declaration creation is provisional. Imports
// can be attached next; the normal check verifies the complete graph.
validation: "syntax",
source: spec.source,
files: [
{