Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/_generated-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,5 +496,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "d63e4c31b2ecb4768b8d7c22f4fca6ec66da0d674ff05b7663fa95db8791754b"
"sourceHash": "ee513c6250d785a2081789da920000cc75e2077ffd129acafd7ef7983c6518b1"
}
78 changes: 76 additions & 2 deletions apps/docs/document-api/reference/mutations/apply.mdx

Large diffs are not rendered by default.

78 changes: 76 additions & 2 deletions apps/docs/document-api/reference/mutations/preview.mdx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/docs/document-engine/sdks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.get_node_by_id` | `get-node-by-id` | Retrieve a single node by its unique ID. |
| `doc.get_text` | `get-text` | Extract the plain-text content of the document. |
| `doc.get_markdown` | `get-markdown` | Extract the document content as a Markdown string. |
| `doc.get_html` | `get-html` | Extract the document content as an HTML string. |
| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. |
| `doc.query.match` | `query match` | Deterministic selector-based search with cardinality contracts for mutation targeting. |
| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. |
Expand Down
54 changes: 53 additions & 1 deletion packages/document-api/scripts/lib/reference-docs-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
OPERATION_DESCRIPTION_MAP,
OPERATION_EXPECTED_RESULT_MAP,
OPERATION_REFERENCE_DOC_PATH_MAP,
PUBLIC_STEP_OP_CATALOG,
REFERENCE_OPERATION_ALIASES,
REFERENCE_OPERATION_GROUPS,
type ReferenceAliasDefinition,
Expand Down Expand Up @@ -63,7 +64,7 @@
* break unquoted YAML scalars (colons, hash signs, brackets, etc.).
*/
function yamlQuote(value: string): string {
if (/[:#\[\]{}&*!|>'"%@`]/u.test(value)) {

Check warning on line 67 in packages/document-api/scripts/lib/reference-docs-artifacts.ts

View workflow job for this annotation

GitHub Actions / validate

Unnecessary escape character: \[
return `"${value.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"`;
}
return value;
Expand All @@ -82,6 +83,55 @@
return `<span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="${href}"><code>${label}</code></a></span>`;
}

const STEP_DOMAIN_ORDER = ['assert', 'text', 'format', 'create', 'tables'] as const;
const STEP_DOMAIN_LABELS: Record<(typeof STEP_DOMAIN_ORDER)[number], string> = {
assert: 'Assert',
text: 'Text',
format: 'Format',
create: 'Create',
tables: 'Tables',
};

function renderStepReferenceCell(referenceOperationId?: ContractOperationSnapshot['operationId']): string {
if (!referenceOperationId) return '—';
const operationPath = toOperationDocPath(referenceOperationId);
return renderNoWrapLinkCode(referenceOperationId, toPublicDocHref(operationPath));
}

function renderStepOpsSection(operation: ContractOperationSnapshot): string {
if (operation.operationId !== 'mutations.apply' && operation.operationId !== 'mutations.preview') {
return '';
}

const domainSections = STEP_DOMAIN_ORDER.map((domain) => {
const entries = PUBLIC_STEP_OP_CATALOG.filter((entry) => entry.domain === domain);
if (entries.length === 0) return '';

const rows = entries
.map(
(entry) =>
`| ${renderNoWrapCode(entry.opId)} | ${escapeCell(entry.description)} | ${renderStepReferenceCell(entry.referenceOperationId)} |`,
)
.join('\n');

return `### ${STEP_DOMAIN_LABELS[domain]}

| Step op (\`steps[].op\`) | Description | Related API operation |
| --- | --- | --- |
${rows}`;
})
.filter(Boolean)
.join('\n\n');

return `## Supported step operations

Use these values in \`steps[].op\` when authoring mutation plans.

${domainSections}

The runtime capability snapshot also exposes this allowlist at \`planEngine.supportedStepOps\`.`;
}

// ---------------------------------------------------------------------------
// $ref resolution
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -563,6 +613,8 @@

const inputExample = generateExample(operation.schemas.input, $defs);
const outputExample = generateExample(operation.schemas.output, $defs);
const stepOpsSection = renderStepOpsSection(operation);
const expectedResultSection = `${expectedResult}${stepOpsSection ? `\n\n${stepOpsSection}` : ''}`;

// -- Build raw-schema accordion blocks --
const rawSchemaBlocks: string[] = [];
Expand Down Expand Up @@ -599,7 +651,7 @@

## Expected result

${expectedResult}
${expectedResultSection}

## Input fields

Expand Down
17 changes: 17 additions & 0 deletions packages/document-api/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { OPERATION_DEFINITIONS, type ReferenceGroupKey } from './operation-defin
import { DOCUMENT_API_MEMBER_PATHS, OPERATION_MEMBER_PATH_MAP, memberPathForOperation } from './operation-map.js';
import { OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_GROUPS } from './reference-doc-map.js';
import { buildInternalContractSchemas } from './schemas.js';
import { PUBLIC_MUTATION_STEP_OP_IDS, STEP_OP_CATALOG } from './step-op-catalog.js';
import { OPERATION_IDS, PRE_APPLY_THROW_CODES, isValidOperationIdFormat } from './types.js';

describe('document-api contract catalog', () => {
Expand Down Expand Up @@ -194,6 +195,22 @@ describe('document-api contract catalog', () => {
}
});

it('keeps public mutation step ops explicit and reference-valid', () => {
expect(PUBLIC_MUTATION_STEP_OP_IDS.length).toBeGreaterThan(0);
expect(new Set(PUBLIC_MUTATION_STEP_OP_IDS).size).toBe(PUBLIC_MUTATION_STEP_OP_IDS.length);
expect(PUBLIC_MUTATION_STEP_OP_IDS).not.toContain('domain.command');
expect(PUBLIC_MUTATION_STEP_OP_IDS).toContain('assert');

const validOperationIds = new Set<string>(OPERATION_IDS);
for (const stepOp of STEP_OP_CATALOG) {
if (!stepOp.referenceOperationId) continue;
expect(
validOperationIds.has(stepOp.referenceOperationId),
`${stepOp.opId} references unknown operation ${stepOp.referenceOperationId}`,
).toBe(true);
}
});

it('marks exactly the out-of-band mutation operations as historyUnsafe', () => {
const historyUnsafeOps = OPERATION_IDS.filter((id) => COMMAND_CATALOG[id].historyUnsafe === true).sort();

Expand Down
1 change: 1 addition & 0 deletions packages/document-api/src/contract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './operation-map.js';
export * from './reference-doc-map.js';
export * from './reference-aliases.js';
export * from './operation-registry.js';
export * from './step-op-catalog.js';
4 changes: 2 additions & 2 deletions packages/document-api/src/contract/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@
};
}

function listsExitResultSchemaFor(operationId: OperationId): JsonSchema {

Check warning on line 627 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'listsExitResultSchemaFor' is defined but never used. Allowed unused vars must match /^_/u
return {
oneOf: [listsExitSuccessSchema, listsFailureSchemaFor(operationId)],
};
Expand Down Expand Up @@ -2746,7 +2746,7 @@
changeMode: { enum: ['direct', 'tracked'] },
steps: arraySchema({ type: 'object' }),
},
['expectedRevision', 'atomic', 'changeMode', 'steps'],
['atomic', 'changeMode', 'steps'],
),
output: objectSchema(
{
Expand All @@ -2766,7 +2766,7 @@
changeMode: { enum: ['direct', 'tracked'] },
steps: arraySchema({ type: 'object' }),
},
['expectedRevision', 'atomic', 'changeMode', 'steps'],
['atomic', 'changeMode', 'steps'],
),
output: objectSchema(
{
Expand Down
214 changes: 214 additions & 0 deletions packages/document-api/src/contract/step-op-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import type { OperationId } from './types.js';

export type StepOpDomain = 'assert' | 'text' | 'format' | 'create' | 'tables' | 'internal';
export type StepOpSurface = 'public' | 'internal';

export interface StepOpCatalogEntry<
OpId extends string = string,
Domain extends StepOpDomain = StepOpDomain,
Surface extends StepOpSurface = StepOpSurface,
> {
opId: OpId;
domain: Domain;
surface: Surface;
description: string;
referenceOperationId?: OperationId;
}

function step<
const OpId extends string,
const Domain extends StepOpDomain,
const Surface extends StepOpSurface = 'public',
>(
opId: OpId,
domain: Domain,
description: string,
options: { surface?: Surface; referenceOperationId?: OperationId } = {},
): StepOpCatalogEntry<OpId, Domain, Surface> {
return {
opId,
domain,
surface: (options.surface ?? 'public') as Surface,
description,
...(options.referenceOperationId ? { referenceOperationId: options.referenceOperationId } : {}),
};
}

const STEP_OP_CATALOG_UNFROZEN = [
step('assert', 'assert', 'Assert selector cardinality after mutation steps complete.'),

step('text.rewrite', 'text', 'Rewrite matched text ranges with replacement content.', {
referenceOperationId: 'replace',
}),
step('text.insert', 'text', 'Insert text before or after a matched range.', {
referenceOperationId: 'insert',
}),
step('text.delete', 'text', 'Delete matched text ranges.', {
referenceOperationId: 'delete',
}),

step('format.apply', 'format', 'Apply inline formatting patch changes to matched text ranges.', {
referenceOperationId: 'format.apply',
}),

step('create.paragraph', 'create', 'Create a paragraph adjacent to the matched block.', {
referenceOperationId: 'create.paragraph',
}),
step('create.heading', 'create', 'Create a heading adjacent to the matched block.', {
referenceOperationId: 'create.heading',
}),
step('create.table', 'create', 'Create a table at the requested location.', {
referenceOperationId: 'create.table',
}),

step('tables.delete', 'tables', 'Delete the target table from the document.', {
referenceOperationId: 'tables.delete',
}),
step('tables.clearContents', 'tables', 'Clear contents from a target table or cell range.', {
referenceOperationId: 'tables.clearContents',
}),
step('tables.move', 'tables', 'Move a table to a new position.', {
referenceOperationId: 'tables.move',
}),
step('tables.split', 'tables', 'Split a table into two tables at a target row.', {
referenceOperationId: 'tables.split',
}),
step('tables.convertFromText', 'tables', 'Convert a text range into a table.', {
referenceOperationId: 'tables.convertFromText',
}),
step('tables.convertToText', 'tables', 'Convert a table to plain text.', {
referenceOperationId: 'tables.convertToText',
}),
step('tables.setLayout', 'tables', 'Set table layout mode.', {
referenceOperationId: 'tables.setLayout',
}),
step('tables.insertRow', 'tables', 'Insert a row into the target table.', {
referenceOperationId: 'tables.insertRow',
}),
step('tables.deleteRow', 'tables', 'Delete a row from the target table.', {
referenceOperationId: 'tables.deleteRow',
}),
step('tables.setRowHeight', 'tables', 'Set row height in the target table.', {
referenceOperationId: 'tables.setRowHeight',
}),
step('tables.distributeRows', 'tables', 'Distribute row heights evenly.', {
referenceOperationId: 'tables.distributeRows',
}),
step('tables.setRowOptions', 'tables', 'Set row-level options (header repeat, page break, etc.).', {
referenceOperationId: 'tables.setRowOptions',
}),
step('tables.insertColumn', 'tables', 'Insert a column into the target table.', {
referenceOperationId: 'tables.insertColumn',
}),
step('tables.deleteColumn', 'tables', 'Delete a column from the target table.', {
referenceOperationId: 'tables.deleteColumn',
}),
step('tables.setColumnWidth', 'tables', 'Set column width in the target table.', {
referenceOperationId: 'tables.setColumnWidth',
}),
step('tables.distributeColumns', 'tables', 'Distribute column widths evenly.', {
referenceOperationId: 'tables.distributeColumns',
}),
step('tables.insertCell', 'tables', 'Insert a cell into a table row.', {
referenceOperationId: 'tables.insertCell',
}),
step('tables.deleteCell', 'tables', 'Delete a cell from a table row.', {
referenceOperationId: 'tables.deleteCell',
}),
step('tables.mergeCells', 'tables', 'Merge a range of table cells.', {
referenceOperationId: 'tables.mergeCells',
}),
step('tables.unmergeCells', 'tables', 'Unmerge a merged table cell.', {
referenceOperationId: 'tables.unmergeCells',
}),
step('tables.splitCell', 'tables', 'Split a table cell into multiple cells.', {
referenceOperationId: 'tables.splitCell',
}),
step('tables.setCellProperties', 'tables', 'Set properties on target table cells.', {
referenceOperationId: 'tables.setCellProperties',
}),
step('tables.sort', 'tables', 'Sort table rows by a column value.', {
referenceOperationId: 'tables.sort',
}),
step('tables.setAltText', 'tables', 'Set table alt text properties.', {
referenceOperationId: 'tables.setAltText',
}),
step('tables.setStyle', 'tables', 'Set table style identifier.', {
referenceOperationId: 'tables.setStyle',
}),
step('tables.clearStyle', 'tables', 'Clear direct table style assignment.', {
referenceOperationId: 'tables.clearStyle',
}),
step('tables.setStyleOption', 'tables', 'Set table style option flags.', {
referenceOperationId: 'tables.setStyleOption',
}),
step('tables.setBorder', 'tables', 'Set table border properties.', {
referenceOperationId: 'tables.setBorder',
}),
step('tables.clearBorder', 'tables', 'Clear table border properties.', {
referenceOperationId: 'tables.clearBorder',
}),
step('tables.applyBorderPreset', 'tables', 'Apply a border preset to a table.', {
referenceOperationId: 'tables.applyBorderPreset',
}),
step('tables.setShading', 'tables', 'Set table shading properties.', {
referenceOperationId: 'tables.setShading',
}),
step('tables.clearShading', 'tables', 'Clear table shading properties.', {
referenceOperationId: 'tables.clearShading',
}),
step('tables.setTablePadding', 'tables', 'Set table-level cell padding.', {
referenceOperationId: 'tables.setTablePadding',
}),
step('tables.setCellPadding', 'tables', 'Set cell padding for target cells.', {
referenceOperationId: 'tables.setCellPadding',
}),
step('tables.setCellSpacing', 'tables', 'Set table cell spacing.', {
referenceOperationId: 'tables.setCellSpacing',
}),
step('tables.clearCellSpacing', 'tables', 'Clear table cell spacing.', {
referenceOperationId: 'tables.clearCellSpacing',
}),

// Internal bridge op used by wrappers that execute pre-compiled plans with _handler closures.
step('domain.command', 'internal', 'Internal wrapper bridge op. Not a user-authored step.', {
surface: 'internal',
}),
] as const satisfies readonly StepOpCatalogEntry[];

export const STEP_OP_CATALOG: readonly StepOpCatalogEntry[] = Object.freeze(
STEP_OP_CATALOG_UNFROZEN.map((entry) => Object.freeze(entry)),
);

export type MutationStepOpId = (typeof STEP_OP_CATALOG_UNFROZEN)[number]['opId'];
type InternalStepOpCatalogEntry = Extract<(typeof STEP_OP_CATALOG_UNFROZEN)[number], { surface: 'internal' }>;
type InternalMutationStepOpId = InternalStepOpCatalogEntry['opId'];
export type PublicMutationStepOpId = Exclude<MutationStepOpId, InternalMutationStepOpId>;

const PUBLIC_STEP_OP_CATALOG_UNFROZEN = STEP_OP_CATALOG_UNFROZEN.filter(
(entry) => entry.surface === 'public',
) as readonly StepOpCatalogEntry<PublicMutationStepOpId, StepOpDomain, 'public'>[];

export type PublicStepOpCatalogEntry = (typeof PUBLIC_STEP_OP_CATALOG_UNFROZEN)[number];

export const PUBLIC_STEP_OP_CATALOG: readonly StepOpCatalogEntry[] = Object.freeze(
PUBLIC_STEP_OP_CATALOG_UNFROZEN.map((entry) => Object.freeze(entry)),
);

export const KNOWN_MUTATION_STEP_OP_IDS: readonly MutationStepOpId[] = Object.freeze(
STEP_OP_CATALOG_UNFROZEN.map((entry) => entry.opId),
);
export const PUBLIC_MUTATION_STEP_OP_IDS: readonly PublicMutationStepOpId[] = Object.freeze(
PUBLIC_STEP_OP_CATALOG_UNFROZEN.map((entry) => entry.opId),
);

const KNOWN_MUTATION_STEP_OP_SET: ReadonlySet<string> = new Set(KNOWN_MUTATION_STEP_OP_IDS);
const PUBLIC_MUTATION_STEP_OP_SET: ReadonlySet<string> = new Set(PUBLIC_MUTATION_STEP_OP_IDS);

export function isKnownMutationStepOp(opId: string): opId is MutationStepOpId {
return KNOWN_MUTATION_STEP_OP_SET.has(opId);
}

export function isPublicMutationStepOp(opId: string): opId is PublicMutationStepOpId {
return PUBLIC_MUTATION_STEP_OP_SET.has(opId);
}
4 changes: 2 additions & 2 deletions packages/document-api/src/types/mutation-plan.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,14 @@ export type MutationStep = TextRewriteStep | TextInsertStep | TextDeleteStep | S
export type ChangeMode = 'direct' | 'tracked';

export type MutationsApplyInput = {
expectedRevision: string;
expectedRevision?: string;
atomic: true;
changeMode: ChangeMode;
steps: MutationStep[];
};

export type MutationsPreviewInput = {
expectedRevision: string;
expectedRevision?: string;
atomic: true;
changeMode: ChangeMode;
steps: MutationStep[];
Expand Down
8 changes: 4 additions & 4 deletions packages/document-api/src/types/step-manifest.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* Step manifest types — public, engine-agnostic metadata for step ops.
*
* `StepManifest` is the single source of truth for schema generation,
* docs, tool catalogs, capabilities, and wrapper generation.
* It lives in document-api (engine-agnostic) and is consumed by
* super-editor executor registration at runtime.
* These types describe a rich manifest model for mutation step operations.
* The current catalog used by docs and capabilities lives in
* `contract/step-op-catalog.ts`; this type remains available for future
* expansion to schema-level per-step metadata.
*/

export interface IdentityStrategy {
Expand Down
Loading
Loading