Skip to content

Commit bb4fa71

Browse files
committed
feat(document-api): add story targeting and runtime support for non-body content
1 parent ed4839b commit bb4fa71

56 files changed

Lines changed: 3653 additions & 180 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/document-api/src/contract/contract.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,15 @@ describe('document-api contract catalog', () => {
9292
const [legacyVariant, structuralVariant] = insertInputSchema.oneOf!;
9393

9494
expect(legacyVariant.type).toBe('object');
95-
expect(Object.keys(legacyVariant.properties!).sort()).toEqual(['target', 'type', 'value']);
95+
expect(Object.keys(legacyVariant.properties!).sort()).toEqual(['in', 'target', 'type', 'value']);
9696
expect(legacyVariant.required).toEqual(['value']);
9797
expect(legacyVariant.additionalProperties).toBe(false);
9898
expect((legacyVariant.properties!.target as { $ref?: string }).$ref).toBe('#/$defs/TextAddress');
9999

100100
expect(structuralVariant.type).toBe('object');
101101
expect(Object.keys(structuralVariant.properties!).sort()).toEqual([
102102
'content',
103+
'in',
103104
'nestingPolicy',
104105
'placement',
105106
'target',

packages/document-api/src/contract/metadata-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export const PRE_APPLY_THROW_CODES = [
4040
// SD-2070 content controls throw codes
4141
'LOCK_VIOLATION',
4242
'TYPE_MISMATCH',
43+
// Story-scoped throw codes
44+
'STORY_NOT_FOUND',
45+
'STORY_MISMATCH',
46+
'STORY_NOT_SUPPORTED',
47+
'CROSS_STORY_PLAN',
48+
'MATERIALIZATION_FAILED',
4349
] as const;
4450

4551
export type PreApplyThrowCode = (typeof PRE_APPLY_THROW_CODES)[number];

packages/document-api/src/contract/operation-definitions.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ const T_HEADER_FOOTER_MUTATION = [
236236
'INTERNAL_ERROR',
237237
] as const;
238238

239+
// Story-scoped throw-code arrays
240+
const T_STORY = [
241+
'STORY_NOT_FOUND',
242+
'STORY_MISMATCH',
243+
'STORY_NOT_SUPPORTED',
244+
'CROSS_STORY_PLAN',
245+
'MATERIALIZATION_FAILED',
246+
] as const;
247+
239248
// Reference-namespace throw-code shorthand arrays
240249
const T_REF_READ_LIST = ['CAPABILITY_UNAVAILABLE', 'INVALID_INPUT'] as const;
241250
const T_REF_MUTATION = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'INVALID_INPUT', 'CAPABILITY_UNAVAILABLE'] as const;
@@ -276,7 +285,7 @@ const FORMAT_INLINE_ALIAS_OPERATION_DEFINITIONS: Record<FormatInlineAliasOperati
276285
supportsDryRun: true,
277286
supportsTrackedMode: entry.tracked,
278287
possibleFailureCodes: ['INVALID_TARGET'],
279-
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'],
288+
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT', ...T_STORY],
280289
}),
281290
referenceDocPath: `format/${camelToKebab(entry.key)}.mdx`,
282291
referenceGroup: 'format',
@@ -311,7 +320,7 @@ export const OPERATION_DEFINITIONS = {
311320
requiresDocumentContext: true,
312321
metadata: readOperation({
313322
idempotency: 'idempotent',
314-
throws: ['CAPABILITY_UNAVAILABLE', 'INVALID_INPUT', 'ADDRESS_STALE'],
323+
throws: ['CAPABILITY_UNAVAILABLE', 'INVALID_INPUT', 'ADDRESS_STALE', ...T_STORY],
315324
deterministicTargetResolution: false,
316325
}),
317326
referenceDocPath: 'find.mdx',
@@ -347,7 +356,9 @@ export const OPERATION_DEFINITIONS = {
347356
description: 'Extract the plain-text content of the document.',
348357
expectedResult: 'Returns the full plain-text content of the document as a string.',
349358
requiresDocumentContext: true,
350-
metadata: readOperation(),
359+
metadata: readOperation({
360+
throws: [...T_STORY],
361+
}),
351362
referenceDocPath: 'get-text.mdx',
352363
referenceGroup: 'core',
353364

@@ -359,7 +370,9 @@ export const OPERATION_DEFINITIONS = {
359370
description: 'Extract the document content as a Markdown string.',
360371
expectedResult: 'Returns the full document content as a Markdown-formatted string.',
361372
requiresDocumentContext: true,
362-
metadata: readOperation(),
373+
metadata: readOperation({
374+
throws: [...T_STORY],
375+
}),
363376
referenceDocPath: 'get-markdown.mdx',
364377
referenceGroup: 'core',
365378
intentGroup: 'get_content',
@@ -370,7 +383,9 @@ export const OPERATION_DEFINITIONS = {
370383
description: 'Extract the document content as an HTML string.',
371384
expectedResult: 'Returns the full document content as an HTML-formatted string.',
372385
requiresDocumentContext: true,
373-
metadata: readOperation(),
386+
metadata: readOperation({
387+
throws: [...T_STORY],
388+
}),
374389
referenceDocPath: 'get-html.mdx',
375390
referenceGroup: 'core',
376391
intentGroup: 'get_content',
@@ -456,6 +471,7 @@ export const OPERATION_DEFINITIONS = {
456471
'RAW_MODE_REQUIRED',
457472
'PRESERVE_ONLY_VIOLATION',
458473
'CAPABILITY_UNSUPPORTED',
474+
...T_STORY,
459475
],
460476
}),
461477
referenceDocPath: 'insert.mdx',
@@ -499,6 +515,7 @@ export const OPERATION_DEFINITIONS = {
499515
'RAW_MODE_REQUIRED',
500516
'PRESERVE_ONLY_VIOLATION',
501517
'CAPABILITY_UNSUPPORTED',
518+
...T_STORY,
502519
],
503520
}),
504521
referenceDocPath: 'replace.mdx',
@@ -518,7 +535,7 @@ export const OPERATION_DEFINITIONS = {
518535
supportsDryRun: true,
519536
supportsTrackedMode: true,
520537
possibleFailureCodes: ['NO_OP'],
521-
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'],
538+
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT', ...T_STORY],
522539
}),
523540
referenceDocPath: 'delete.mdx',
524541
referenceGroup: 'core',
@@ -599,7 +616,7 @@ export const OPERATION_DEFINITIONS = {
599616
supportsDryRun: true,
600617
supportsTrackedMode: true,
601618
possibleFailureCodes: ['INVALID_TARGET'],
602-
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'],
619+
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT', ...T_STORY],
603620
}),
604621
referenceDocPath: 'format/apply.mdx',
605622
referenceGroup: 'format',
@@ -636,7 +653,7 @@ export const OPERATION_DEFINITIONS = {
636653
supportsDryRun: true,
637654
supportsTrackedMode: true,
638655
possibleFailureCodes: ['INVALID_TARGET'],
639-
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'],
656+
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET', ...T_STORY],
640657
}),
641658
referenceDocPath: 'create/paragraph.mdx',
642659
referenceGroup: 'create',
@@ -653,7 +670,7 @@ export const OPERATION_DEFINITIONS = {
653670
supportsDryRun: true,
654671
supportsTrackedMode: true,
655672
possibleFailureCodes: ['INVALID_TARGET'],
656-
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'],
673+
throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET', ...T_STORY],
657674
}),
658675
referenceDocPath: 'create/heading.mdx',
659676
referenceGroup: 'create',
@@ -2008,7 +2025,7 @@ export const OPERATION_DEFINITIONS = {
20082025
requiresDocumentContext: true,
20092026
metadata: readOperation({
20102027
idempotency: 'idempotent',
2011-
throws: T_QUERY_MATCH,
2028+
throws: [...T_QUERY_MATCH, ...T_STORY],
20122029
deterministicTargetResolution: true,
20132030
}),
20142031
referenceDocPath: 'query/match.mdx',
@@ -2041,7 +2058,7 @@ export const OPERATION_DEFINITIONS = {
20412058
requiresDocumentContext: true,
20422059
metadata: readOperation({
20432060
idempotency: 'idempotent',
2044-
throws: T_PLAN_ENGINE,
2061+
throws: [...T_PLAN_ENGINE, ...T_STORY],
20452062
deterministicTargetResolution: true,
20462063
}),
20472064
referenceDocPath: 'mutations/preview.mdx',
@@ -2066,6 +2083,7 @@ export const OPERATION_DEFINITIONS = {
20662083
'RAW_MODE_REQUIRED',
20672084
'PRESERVE_ONLY_VIOLATION',
20682085
'CAPABILITY_UNSUPPORTED',
2086+
...T_STORY,
20692087
],
20702088
deterministicTargetResolution: true,
20712089
}),
@@ -3058,7 +3076,7 @@ export const OPERATION_DEFINITIONS = {
30583076
supportsDryRun: true,
30593077
supportsTrackedMode: false,
30603078
possibleFailureCodes: ['INVALID_TARGET', 'INVALID_INPUT'],
3061-
throws: [...T_NOT_FOUND_COMMAND, 'INVALID_INPUT'],
3079+
throws: [...T_NOT_FOUND_COMMAND, 'INVALID_INPUT', ...T_STORY],
30623080
}),
30633081
referenceDocPath: 'create/image.mdx',
30643082
referenceGroup: 'create',

packages/document-api/src/contract/schemas.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,49 @@ const SHARED_DEFS: Record<string, JsonSchema> = {
497497
BlockAddressOrRange: {
498498
oneOf: [ref('BlockAddress'), ref('BlockRange')],
499499
},
500+
501+
// -- Story locator (discriminated union on storyType) --
502+
StoryLocator: {
503+
oneOf: [
504+
objectSchema({ kind: { const: 'story' }, storyType: { const: 'body' } }, ['kind', 'storyType']),
505+
objectSchema(
506+
{
507+
kind: { const: 'story' },
508+
storyType: { const: 'headerFooterSlot' },
509+
section: ref('SectionAddress'),
510+
headerFooterKind: { enum: ['header', 'footer'] },
511+
variant: { enum: ['default', 'first', 'even'] },
512+
resolution: { enum: ['effective', 'explicit'] },
513+
onWrite: { enum: ['materializeIfInherited', 'editResolvedPart', 'error'] },
514+
},
515+
['kind', 'storyType', 'section', 'headerFooterKind', 'variant'],
516+
),
517+
objectSchema(
518+
{
519+
kind: { const: 'story' },
520+
storyType: { const: 'headerFooterPart' },
521+
refId: { type: 'string' },
522+
},
523+
['kind', 'storyType', 'refId'],
524+
),
525+
objectSchema(
526+
{
527+
kind: { const: 'story' },
528+
storyType: { const: 'footnote' },
529+
noteId: { type: 'string' },
530+
},
531+
['kind', 'storyType', 'noteId'],
532+
),
533+
objectSchema(
534+
{
535+
kind: { const: 'story' },
536+
storyType: { const: 'endnote' },
537+
noteId: { type: 'string' },
538+
},
539+
['kind', 'storyType', 'noteId'],
540+
),
541+
],
542+
} satisfies JsonSchema,
500543
};
501544

502545
// ---------------------------------------------------------------------------
@@ -538,6 +581,7 @@ const textMutationResolutionSchema = ref('TextMutationResolution');
538581
const textMutationSuccessSchema = ref('TextMutationSuccess');
539582
const matchRunSchema = ref('MatchRun');
540583
const matchBlockSchema = ref('MatchBlock');
584+
const storyLocatorSchema = ref('StoryLocator');
541585

542586
// Keep these aliases for internal readability
543587
void positionSchema;
@@ -896,6 +940,7 @@ const sdReadOptionsSchema = objectSchema({
896940

897941
const sdFindInputSchema = objectSchema(
898942
{
943+
in: storyLocatorSchema,
899944
select: sdSelectorSchema,
900945
within: blockNodeAddressSchema,
901946
limit: { type: 'integer' },
@@ -1511,6 +1556,7 @@ const insertInputSchema: JsonSchema = {
15111556
oneOf: [
15121557
objectSchema(
15131558
{
1559+
in: storyLocatorSchema,
15141560
target: {
15151561
...textAddressSchema,
15161562
description: "Insertion point: {kind:'text', blockId:'...', range:{start, end}}.",
@@ -1526,6 +1572,7 @@ const insertInputSchema: JsonSchema = {
15261572
),
15271573
objectSchema(
15281574
{
1575+
in: storyLocatorSchema,
15291576
target: {
15301577
...blockNodeAddressSchema,
15311578
description: "Block address for structural insertion: {kind:'block', nodeType:'...', nodeId:'...'}.",
@@ -2750,15 +2797,20 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
27502797
output: sdNodeResultSchema,
27512798
},
27522799
getText: {
2753-
input: strictEmptyObjectSchema,
2800+
input: objectSchema({
2801+
in: storyLocatorSchema,
2802+
}),
27542803
output: { type: 'string' },
27552804
},
27562805
getMarkdown: {
2757-
input: strictEmptyObjectSchema,
2806+
input: objectSchema({
2807+
in: storyLocatorSchema,
2808+
}),
27582809
output: { type: 'string' },
27592810
},
27602811
getHtml: {
27612812
input: objectSchema({
2813+
in: storyLocatorSchema,
27622814
unflattenLists: {
27632815
type: 'boolean',
27642816
description: 'When true, flattens nested list structures in output. Default: false.',
@@ -2808,13 +2860,20 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
28082860
oneOf: [
28092861
// Text replacement: TargetLocator + text
28102862
{
2811-
...targetLocatorWithPayload({ text: { type: 'string', description: 'Replacement text content.' } }, ['text']),
2863+
...targetLocatorWithPayload(
2864+
{
2865+
in: storyLocatorSchema,
2866+
text: { type: 'string', description: 'Replacement text content.' },
2867+
},
2868+
['text'],
2869+
),
28122870
},
28132871
// Structural replacement: exactly one of (target | ref) + content
28142872
{
28152873
oneOf: [
28162874
objectSchema(
28172875
{
2876+
in: storyLocatorSchema,
28182877
target: {
28192878
oneOf: [blockNodeAddressSchema, selectionTargetSchema],
28202879
description: 'Target block or selection to replace.',
@@ -2829,6 +2888,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
28292888
),
28302889
objectSchema(
28312890
{
2891+
in: storyLocatorSchema,
28322892
ref: { type: 'string', description: 'Reference handle from a previous search result.' },
28332893
content: {
28342894
...sdFragmentSchema,
@@ -2849,6 +2909,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
28492909
delete: {
28502910
input: {
28512911
...targetLocatorWithPayload({
2912+
in: storyLocatorSchema,
28522913
behavior: { ...deleteBehaviorSchema, description: "Delete behavior: 'selection' (default) or 'exact'." },
28532914
}),
28542915
},
@@ -2860,6 +2921,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
28602921
input: {
28612922
...targetLocatorWithPayload(
28622923
{
2924+
in: storyLocatorSchema,
28632925
inline: {
28642926
...buildInlineRunPatchSchema(),
28652927
description:
@@ -3303,6 +3365,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
33033365
})(),
33043366
'create.paragraph': {
33053367
input: objectSchema({
3368+
in: storyLocatorSchema,
33063369
at: {
33073370
description:
33083371
"Position: {kind:'documentEnd'} to append, {kind:'documentStart'} to prepend, or {kind:'before'|'after', target:{kind:'block', nodeType:'...', nodeId:'...'}} for relative placement.",
@@ -3334,6 +3397,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
33343397
'create.heading': {
33353398
input: objectSchema(
33363399
{
3400+
in: storyLocatorSchema,
33373401
level: { ...headingLevelSchema, description: 'Heading level (1-6).' },
33383402
at: {
33393403
description:
@@ -4469,6 +4533,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
44694533
'query.match': {
44704534
input: objectSchema(
44714535
{
4536+
in: storyLocatorSchema,
44724537
select: {
44734538
description:
44744539
"Search selector. Use {type:'text', pattern:'...'} for text search or {type:'node', nodeType:'paragraph'|'heading'|...} for node search.",
@@ -4725,6 +4790,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
47254790

47264791
const mutationsInputSchema = objectSchema(
47274792
{
4793+
in: storyLocatorSchema,
47284794
expectedRevision: {
47294795
type: 'string',
47304796
description:
@@ -5871,6 +5937,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
58715937
'create.image': {
58725938
input: objectSchema(
58735939
{
5940+
in: storyLocatorSchema,
58745941
src: { type: 'string' },
58755942
alt: { type: 'string' },
58765943
title: { type: 'string' },

0 commit comments

Comments
 (0)