graphddb 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildBridgeBundle,
4
- buildManifest
5
- } from "./chunk-FI63YKK5.js";
4
+ buildManifest,
5
+ buildOperations
6
+ } from "./chunk-Y7XV5QL2.js";
6
7
  import {
7
8
  MetadataRegistry,
8
9
  normalizeSelectSpec
9
- } from "./chunk-B3GWIWT6.js";
10
+ } from "./chunk-CCIVET5K.js";
11
+ import {
12
+ createDefaultLinter
13
+ } from "./chunk-PDUVTYC5.js";
10
14
 
11
15
  // src/cli.ts
12
16
  import { createRequire } from "module";
@@ -440,6 +444,105 @@ var commandDefinitions = {
440
444
  }
441
445
  }
442
446
  ]
447
+ },
448
+ "generate.docs": {
449
+ "effects": {
450
+ "risk_level": "low",
451
+ "reads": [
452
+ "ts-definitions"
453
+ ],
454
+ "writes": [
455
+ "generated-code"
456
+ ]
457
+ },
458
+ "options": [
459
+ {
460
+ "name": "entry",
461
+ "schema": {
462
+ "type": "string"
463
+ },
464
+ "file": {
465
+ "mode": "read",
466
+ "exists": true,
467
+ "media_type": "application/typescript",
468
+ "encoding": "utf-8"
469
+ }
470
+ },
471
+ {
472
+ "name": "queries",
473
+ "schema": {
474
+ "type": "string"
475
+ },
476
+ "file": {
477
+ "mode": "read",
478
+ "exists": true,
479
+ "media_type": "application/typescript",
480
+ "encoding": "utf-8"
481
+ }
482
+ },
483
+ {
484
+ "name": "out",
485
+ "schema": {
486
+ "type": "string"
487
+ }
488
+ },
489
+ {
490
+ "name": "format",
491
+ "schema": {
492
+ "type": "string",
493
+ "enum": [
494
+ "markdown"
495
+ ],
496
+ "default": "markdown"
497
+ }
498
+ },
499
+ {
500
+ "name": "split",
501
+ "schema": {
502
+ "type": "string",
503
+ "enum": [
504
+ "none",
505
+ "entity"
506
+ ],
507
+ "default": "none"
508
+ }
509
+ },
510
+ {
511
+ "name": "template-dir",
512
+ "schema": {
513
+ "type": "string"
514
+ }
515
+ },
516
+ {
517
+ "name": "helpers",
518
+ "schema": {
519
+ "type": "string"
520
+ }
521
+ },
522
+ {
523
+ "name": "billing-mode",
524
+ "schema": {
525
+ "type": "string",
526
+ "enum": [
527
+ "PAY_PER_REQUEST",
528
+ "PROVISIONED"
529
+ ],
530
+ "default": "PAY_PER_REQUEST"
531
+ }
532
+ },
533
+ {
534
+ "name": "stream",
535
+ "schema": {
536
+ "type": "string",
537
+ "enum": [
538
+ "auto",
539
+ "on",
540
+ "off"
541
+ ],
542
+ "default": "auto"
543
+ }
544
+ }
545
+ ]
443
546
  }
444
547
  };
445
548
  function deriveCommandPolicy(command_id, optionValues) {
@@ -466,8 +569,8 @@ function deriveCommandPolicy(command_id, optionValues) {
466
569
  }
467
570
 
468
571
  // src/generated/contract.ts
469
- var CONTRACT_YAML = "cli_contracts: 0.1.0\n\ninfo:\n title: GraphDDB CLI\n version: 0.1.0\n description: >-\n Contract for the graphddb code-generation CLI. The CLI reads TypeScript\n GraphDDB model / query / command definitions (the single source of truth)\n and emits Python bindings (manifest.json, operations.json, types.py,\n repositories.py, __init__.py) for the Python bridge runtime.\n license:\n name: MIT\n contact:\n name: foo-log-inc\n url: https://github.com/foo-log-inc/graphddb\n\nartifact_slots:\n ts-definitions:\n description: >-\n TypeScript GraphDDB definition modules (entity models, defineQueries,\n defineCommands) used as the single source of truth.\n direction: read\n generated-code:\n description: >-\n Generated Python bindings and JSON specs written to the output directory\n (manifest.json, operations.json, types.py, repositories.py, __init__.py).\n direction: write\n\ncommand_sets:\n graphddb:\n summary: Graph data modeling on DynamoDB \u2014 code generation CLI.\n x-stdin: >-\n No command reads from stdin. All inputs are provided via file options.\n\n commands:\n # \u2500\u2500 generate python \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.python:\n summary: Generate Python bindings from TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models, query definitions, and command\n definitions, builds the serializable manifest / operations bundle, and\n writes the Python bridge bindings (manifest.json, operations.json,\n types.py, repositories.py, __init__.py) to the output directory.\n Output is deterministic for a given set of inputs.\n usage:\n - graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/\n - graphddb generate python --entry definitions.ts --out generated/\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import). May also export queries / commands.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: queries\n aliases: [q]\n description: >-\n Module exporting `queries` (a defineQueries map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: commands\n aliases: [c]\n description: >-\n Module exporting `commands` (a defineCommands map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: transactions\n aliases: [t]\n description: >-\n Module exporting `transactions` (a defineTransactions map).\n Defaults to the commands module (or the entry module) when omitted.\n Optional \u2014 a project with no declarative transactions omits it.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contracts\n description: >-\n Module exporting `contracts` (a map of publicQueryModel /\n publicCommandModel results \u2014 the CQRS Contract layer). Defaults to\n the entry module when omitted. Optional \u2014 a project with no\n contracts omits it, and the output gains no `contracts` content.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contexts\n description: >-\n Module exporting `contexts` (context-ownership: context name \u2192 the\n Model / Contract names that belong to it). Defaults to the\n contracts module (or the entry module) when omitted. Optional \u2014\n consumed by the boundary lint to tell own from foreign.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: dataclass\n description: >-\n Emit Python result / element types as @dataclass classes instead\n of TypedDict (decimal / Optional / Connection mapped accordingly).\n schema:\n type: boolean\n\n - name: out\n aliases: [o]\n required: true\n description: Output directory for the generated Python bindings.\n value_name: dir\n schema:\n type: string\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n files:\n - path: '{options.out}/manifest.json'\n required: true\n media_type: application/json\n description: Entity / table / key / relation metadata.\n - path: '{options.out}/operations.json'\n required: true\n media_type: application/json\n description: Per-query / per-command execution specs.\n - path: '{options.out}/types.py'\n required: true\n media_type: text/x-python\n description: TypedDict result types derived from query selects.\n - path: '{options.out}/repositories.py'\n required: true\n media_type: text/x-python\n description: Per-entity repository classes (sync methods).\n - path: '{options.out}/__init__.py'\n required: true\n media_type: text/x-python\n description: Package re-exports for the generated bindings.\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n # \u2500\u2500 generate cloudformation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.cloudformation:\n summary: >-\n Generate an AWS CloudFormation template (DynamoDB table + GSIs) from\n TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models and builds the serializable\n manifest, then emits an AWS CloudFormation template describing the\n DynamoDB table(s) the single-table design maps onto: one\n `AWS::DynamoDB::Table` per physical table, with the base key schema\n (`PK` / `SK`), the unioned Global Secondary Indexes\n (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a\n `TableName` parameter (default = physical name), and Outputs\n (table name + ARN). Output is deterministic and, by default, YAML.\n usage:\n - graphddb generate cloudformation --entry models.ts --out table.yaml\n - graphddb generate cloudformation --entry models.ts --format json\n - graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml\n - graphddb generate cloudformation --entry models.ts --stream on --out table.yaml\n - graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import).\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: out\n aliases: [o]\n description: >-\n Output file for the generated CloudFormation template. Written to\n stdout when omitted.\n value_name: file\n schema:\n type: string\n\n - name: format\n aliases: [f]\n description: Output serialization format (yaml or json).\n value_name: format\n schema:\n type: string\n enum: [yaml, json]\n default: yaml\n\n - name: gsi-projection\n description: >-\n GSI ProjectionType. ALL (default) projects every attribute;\n KEYS_ONLY projects only the key attributes.\n value_name: projection\n schema:\n type: string\n enum: [ALL, KEYS_ONLY]\n default: ALL\n\n - name: stream\n description: >-\n DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission.\n auto (default) emits a stream only when the model set uses\n stream-based maintenance (updateMode:'stream'); on always emits;\n off never emits. When enabled, a StreamArn Output is added too.\n value_name: mode\n schema:\n type: string\n enum: [auto, on, off]\n default: auto\n\n - name: billing-mode\n description: >-\n DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand\n (no provisioned throughput). PROVISIONED emits BillingMode:\n PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the\n table and every GSI, and unlocks Application Auto Scaling\n (--autoscale).\n value_name: mode\n schema:\n type: string\n enum: [PAY_PER_REQUEST, PROVISIONED]\n default: PAY_PER_REQUEST\n\n - name: rcu\n description: >-\n Provisioned ReadCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: wcu\n description: >-\n Provisioned WriteCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale\n description: >-\n Enable Application Auto Scaling. Emits an\n AWS::ApplicationAutoScaling::ScalableTarget +\n AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read\n and write capacity AND for each GSI read and write capacity. Only\n valid with --billing-mode PROVISIONED (autoscaling is invalid on\n on-demand and is rejected). Off by default \u2014 no autoscaling\n resources are emitted unless this flag is passed.\n schema:\n type: boolean\n\n - name: autoscale-min\n description: >-\n Auto Scaling minimum capacity (MinCapacity) for every scalable\n target. Must be <= --autoscale-max. Only meaningful with\n --autoscale. Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-max\n description: >-\n Auto Scaling maximum capacity (MaxCapacity) for every scalable\n target. Must be >= --autoscale-min. Only meaningful with\n --autoscale. Defaults to 100 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-target\n description: >-\n Auto Scaling target utilization percentage (TargetValue of the\n TargetTrackingScaling policy). Only meaningful with --autoscale.\n Defaults to 70 when omitted.\n value_name: percent\n schema:\n type: integer\n minimum: 1\n maximum: 100\n\n - name: pitr\n description: >-\n Enable point-in-time recovery (PointInTimeRecoverySpecification\n PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n - name: table-class\n description: >-\n DynamoDB table class. Omitted by default (AWS defaults to\n STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit\n TableClass.\n value_name: class\n schema:\n type: string\n enum: [STANDARD, STANDARD_INFREQUENT_ACCESS]\n\n - name: sse\n description: >-\n Enable server-side encryption with the AWS-owned/managed key\n (SSESpecification SSEEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\ncomponents:\n schemas:\n GenerateResult:\n type: object\n required: [status, outDir, files]\n properties:\n status:\n type: string\n enum: [ok]\n outDir:\n type: string\n description: Output directory the files were written to.\n files:\n type: array\n description: Paths of the generated files (relative to outDir).\n items:\n type: string\n Error:\n type: object\n required: [code, message]\n properties:\n code:\n type: string\n message:\n type: string\n";
470
- var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "GraphDDB CLI",\n "version": "0.1.0",\n "description": "Contract for the graphddb code-generation CLI. The CLI reads TypeScript GraphDDB model / query / command definitions (the single source of truth) and emits Python bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) for the Python bridge runtime.",\n "license": {\n "name": "MIT"\n },\n "contact": {\n "name": "foo-log-inc",\n "url": "https://github.com/foo-log-inc/graphddb"\n }\n },\n "artifact_slots": {\n "ts-definitions": {\n "description": "TypeScript GraphDDB definition modules (entity models, defineQueries, defineCommands) used as the single source of truth.",\n "direction": "read"\n },\n "generated-code": {\n "description": "Generated Python bindings and JSON specs written to the output directory (manifest.json, operations.json, types.py, repositories.py, __init__.py).",\n "direction": "write"\n }\n },\n "command_sets": {\n "graphddb": {\n "summary": "Graph data modeling on DynamoDB \u2014 code generation CLI.",\n "x-stdin": "No command reads from stdin. All inputs are provided via file options.",\n "commands": {\n "generate.python": {\n "summary": "Generate Python bindings from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models, query definitions, and command definitions, builds the serializable manifest / operations bundle, and writes the Python bridge bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) to the output directory. Output is deterministic for a given set of inputs.",\n "usage": [\n "graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/",\n "graphddb generate python --entry definitions.ts --out generated/"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import). May also export queries / commands.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "queries",\n "aliases": [\n "q"\n ],\n "description": "Module exporting `queries` (a defineQueries map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "commands",\n "aliases": [\n "c"\n ],\n "description": "Module exporting `commands` (a defineCommands map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "transactions",\n "aliases": [\n "t"\n ],\n "description": "Module exporting `transactions` (a defineTransactions map). Defaults to the commands module (or the entry module) when omitted. Optional \u2014 a project with no declarative transactions omits it.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contracts",\n "description": "Module exporting `contracts` (a map of publicQueryModel / publicCommandModel results \u2014 the CQRS Contract layer). Defaults to the entry module when omitted. Optional \u2014 a project with no contracts omits it, and the output gains no `contracts` content.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contexts",\n "description": "Module exporting `contexts` (context-ownership: context name \u2192 the Model / Contract names that belong to it). Defaults to the contracts module (or the entry module) when omitted. Optional \u2014 consumed by the boundary lint to tell own from foreign.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "dataclass",\n "description": "Emit Python result / element types as @dataclass classes instead of TypedDict (decimal / Optional / Connection mapped accordingly).",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "required": true,\n "description": "Output directory for the generated Python bindings.",\n "value_name": "dir",\n "schema": {\n "type": "string"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n },\n "files": [\n {\n "path": "{options.out}/manifest.json",\n "required": true,\n "media_type": "application/json",\n "description": "Entity / table / key / relation metadata."\n },\n {\n "path": "{options.out}/operations.json",\n "required": true,\n "media_type": "application/json",\n "description": "Per-query / per-command execution specs."\n },\n {\n "path": "{options.out}/types.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "TypedDict result types derived from query selects."\n },\n {\n "path": "{options.out}/repositories.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Per-entity repository classes (sync methods)."\n },\n {\n "path": "{options.out}/__init__.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Package re-exports for the generated bindings."\n }\n ]\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n },\n "generate.cloudformation": {\n "summary": "Generate an AWS CloudFormation template (DynamoDB table + GSIs) from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models and builds the serializable manifest, then emits an AWS CloudFormation template describing the DynamoDB table(s) the single-table design maps onto: one `AWS::DynamoDB::Table` per physical table, with the base key schema (`PK` / `SK`), the unioned Global Secondary Indexes (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a `TableName` parameter (default = physical name), and Outputs (table name + ARN). Output is deterministic and, by default, YAML.",\n "usage": [\n "graphddb generate cloudformation --entry models.ts --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --format json",\n "graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --stream on --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import).",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "description": "Output file for the generated CloudFormation template. Written to stdout when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "format",\n "aliases": [\n "f"\n ],\n "description": "Output serialization format (yaml or json).",\n "value_name": "format",\n "schema": {\n "type": "string",\n "enum": [\n "yaml",\n "json"\n ],\n "default": "yaml"\n }\n },\n {\n "name": "gsi-projection",\n "description": "GSI ProjectionType. ALL (default) projects every attribute; KEYS_ONLY projects only the key attributes.",\n "value_name": "projection",\n "schema": {\n "type": "string",\n "enum": [\n "ALL",\n "KEYS_ONLY"\n ],\n "default": "ALL"\n }\n },\n {\n "name": "stream",\n "description": "DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission. auto (default) emits a stream only when the model set uses stream-based maintenance (updateMode:\'stream\'); on always emits; off never emits. When enabled, a StreamArn Output is added too.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "auto",\n "on",\n "off"\n ],\n "default": "auto"\n }\n },\n {\n "name": "billing-mode",\n "description": "DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand (no provisioned throughput). PROVISIONED emits BillingMode: PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the table and every GSI, and unlocks Application Auto Scaling (--autoscale).",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "PAY_PER_REQUEST",\n "PROVISIONED"\n ],\n "default": "PAY_PER_REQUEST"\n }\n },\n {\n "name": "rcu",\n "description": "Provisioned ReadCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "wcu",\n "description": "Provisioned WriteCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale",\n "description": "Enable Application Auto Scaling. Emits an AWS::ApplicationAutoScaling::ScalableTarget + AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read and write capacity AND for each GSI read and write capacity. Only valid with --billing-mode PROVISIONED (autoscaling is invalid on on-demand and is rejected). Off by default \u2014 no autoscaling resources are emitted unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "autoscale-min",\n "description": "Auto Scaling minimum capacity (MinCapacity) for every scalable target. Must be <= --autoscale-max. Only meaningful with --autoscale. Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-max",\n "description": "Auto Scaling maximum capacity (MaxCapacity) for every scalable target. Must be >= --autoscale-min. Only meaningful with --autoscale. Defaults to 100 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-target",\n "description": "Auto Scaling target utilization percentage (TargetValue of the TargetTrackingScaling policy). Only meaningful with --autoscale. Defaults to 70 when omitted.",\n "value_name": "percent",\n "schema": {\n "type": "integer",\n "minimum": 1,\n "maximum": 100\n }\n },\n {\n "name": "pitr",\n "description": "Enable point-in-time recovery (PointInTimeRecoverySpecification PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "table-class",\n "description": "DynamoDB table class. Omitted by default (AWS defaults to STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit TableClass.",\n "value_name": "class",\n "schema": {\n "type": "string",\n "enum": [\n "STANDARD",\n "STANDARD_INFREQUENT_ACCESS"\n ]\n }\n },\n {\n "name": "sse",\n "description": "Enable server-side encryption with the AWS-owned/managed key (SSESpecification SSEEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n }\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n }\n }\n }\n },\n "components": {\n "schemas": {\n "GenerateResult": {\n "type": "object",\n "required": [\n "status",\n "outDir",\n "files"\n ],\n "properties": {\n "status": {\n "type": "string",\n "enum": [\n "ok"\n ]\n },\n "outDir": {\n "type": "string",\n "description": "Output directory the files were written to."\n },\n "files": {\n "type": "array",\n "description": "Paths of the generated files (relative to outDir).",\n "items": {\n "type": "string"\n }\n }\n }\n },\n "Error": {\n "type": "object",\n "required": [\n "code",\n "message"\n ],\n "properties": {\n "code": {\n "type": "string"\n },\n "message": {\n "type": "string"\n }\n }\n }\n }\n }\n}';
572
+ var CONTRACT_YAML = "cli_contracts: 0.1.0\n\ninfo:\n title: GraphDDB CLI\n version: 0.1.0\n description: >-\n Contract for the graphddb code-generation CLI. The CLI reads TypeScript\n GraphDDB model / query / command definitions (the single source of truth)\n and emits Python bindings (manifest.json, operations.json, types.py,\n repositories.py, __init__.py) for the Python bridge runtime.\n license:\n name: MIT\n contact:\n name: foo-log-inc\n url: https://github.com/foo-log-inc/graphddb\n\nartifact_slots:\n ts-definitions:\n description: >-\n TypeScript GraphDDB definition modules (entity models, defineQueries,\n defineCommands) used as the single source of truth.\n direction: read\n generated-code:\n description: >-\n Generated Python bindings and JSON specs written to the output directory\n (manifest.json, operations.json, types.py, repositories.py, __init__.py).\n direction: write\n\ncommand_sets:\n graphddb:\n summary: Graph data modeling on DynamoDB \u2014 code generation CLI.\n x-stdin: >-\n No command reads from stdin. All inputs are provided via file options.\n\n commands:\n # \u2500\u2500 generate python \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.python:\n summary: Generate Python bindings from TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models, query definitions, and command\n definitions, builds the serializable manifest / operations bundle, and\n writes the Python bridge bindings (manifest.json, operations.json,\n types.py, repositories.py, __init__.py) to the output directory.\n Output is deterministic for a given set of inputs.\n usage:\n - graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/\n - graphddb generate python --entry definitions.ts --out generated/\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import). May also export queries / commands.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: queries\n aliases: [q]\n description: >-\n Module exporting `queries` (a defineQueries map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: commands\n aliases: [c]\n description: >-\n Module exporting `commands` (a defineCommands map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: transactions\n aliases: [t]\n description: >-\n Module exporting `transactions` (a defineTransactions map).\n Defaults to the commands module (or the entry module) when omitted.\n Optional \u2014 a project with no declarative transactions omits it.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contracts\n description: >-\n Module exporting `contracts` (a map of publicQueryModel /\n publicCommandModel results \u2014 the CQRS Contract layer). Defaults to\n the entry module when omitted. Optional \u2014 a project with no\n contracts omits it, and the output gains no `contracts` content.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contexts\n description: >-\n Module exporting `contexts` (context-ownership: context name \u2192 the\n Model / Contract names that belong to it). Defaults to the\n contracts module (or the entry module) when omitted. Optional \u2014\n consumed by the boundary lint to tell own from foreign.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: dataclass\n description: >-\n Emit Python result / element types as @dataclass classes instead\n of TypedDict (decimal / Optional / Connection mapped accordingly).\n schema:\n type: boolean\n\n - name: out\n aliases: [o]\n required: true\n description: Output directory for the generated Python bindings.\n value_name: dir\n schema:\n type: string\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n files:\n - path: '{options.out}/manifest.json'\n required: true\n media_type: application/json\n description: Entity / table / key / relation metadata.\n - path: '{options.out}/operations.json'\n required: true\n media_type: application/json\n description: Per-query / per-command execution specs.\n - path: '{options.out}/types.py'\n required: true\n media_type: text/x-python\n description: TypedDict result types derived from query selects.\n - path: '{options.out}/repositories.py'\n required: true\n media_type: text/x-python\n description: Per-entity repository classes (sync methods).\n - path: '{options.out}/__init__.py'\n required: true\n media_type: text/x-python\n description: Package re-exports for the generated bindings.\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n # \u2500\u2500 generate cloudformation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.cloudformation:\n summary: >-\n Generate an AWS CloudFormation template (DynamoDB table + GSIs) from\n TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models and builds the serializable\n manifest, then emits an AWS CloudFormation template describing the\n DynamoDB table(s) the single-table design maps onto: one\n `AWS::DynamoDB::Table` per physical table, with the base key schema\n (`PK` / `SK`), the unioned Global Secondary Indexes\n (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a\n `TableName` parameter (default = physical name), and Outputs\n (table name + ARN). Output is deterministic and, by default, YAML.\n usage:\n - graphddb generate cloudformation --entry models.ts --out table.yaml\n - graphddb generate cloudformation --entry models.ts --format json\n - graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml\n - graphddb generate cloudformation --entry models.ts --stream on --out table.yaml\n - graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import).\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: out\n aliases: [o]\n description: >-\n Output file for the generated CloudFormation template. Written to\n stdout when omitted.\n value_name: file\n schema:\n type: string\n\n - name: format\n aliases: [f]\n description: Output serialization format (yaml or json).\n value_name: format\n schema:\n type: string\n enum: [yaml, json]\n default: yaml\n\n - name: gsi-projection\n description: >-\n GSI ProjectionType. ALL (default) projects every attribute;\n KEYS_ONLY projects only the key attributes.\n value_name: projection\n schema:\n type: string\n enum: [ALL, KEYS_ONLY]\n default: ALL\n\n - name: stream\n description: >-\n DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission.\n auto (default) emits a stream only when the model set uses\n stream-based maintenance (updateMode:'stream'); on always emits;\n off never emits. When enabled, a StreamArn Output is added too.\n value_name: mode\n schema:\n type: string\n enum: [auto, on, off]\n default: auto\n\n - name: billing-mode\n description: >-\n DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand\n (no provisioned throughput). PROVISIONED emits BillingMode:\n PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the\n table and every GSI, and unlocks Application Auto Scaling\n (--autoscale).\n value_name: mode\n schema:\n type: string\n enum: [PAY_PER_REQUEST, PROVISIONED]\n default: PAY_PER_REQUEST\n\n - name: rcu\n description: >-\n Provisioned ReadCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: wcu\n description: >-\n Provisioned WriteCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale\n description: >-\n Enable Application Auto Scaling. Emits an\n AWS::ApplicationAutoScaling::ScalableTarget +\n AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read\n and write capacity AND for each GSI read and write capacity. Only\n valid with --billing-mode PROVISIONED (autoscaling is invalid on\n on-demand and is rejected). Off by default \u2014 no autoscaling\n resources are emitted unless this flag is passed.\n schema:\n type: boolean\n\n - name: autoscale-min\n description: >-\n Auto Scaling minimum capacity (MinCapacity) for every scalable\n target. Must be <= --autoscale-max. Only meaningful with\n --autoscale. Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-max\n description: >-\n Auto Scaling maximum capacity (MaxCapacity) for every scalable\n target. Must be >= --autoscale-min. Only meaningful with\n --autoscale. Defaults to 100 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-target\n description: >-\n Auto Scaling target utilization percentage (TargetValue of the\n TargetTrackingScaling policy). Only meaningful with --autoscale.\n Defaults to 70 when omitted.\n value_name: percent\n schema:\n type: integer\n minimum: 1\n maximum: 100\n\n - name: pitr\n description: >-\n Enable point-in-time recovery (PointInTimeRecoverySpecification\n PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n - name: table-class\n description: >-\n DynamoDB table class. Omitted by default (AWS defaults to\n STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit\n TableClass.\n value_name: class\n schema:\n type: string\n enum: [STANDARD, STANDARD_INFREQUENT_ACCESS]\n\n - name: sse\n description: >-\n Enable server-side encryption with the AWS-owned/managed key\n (SSESpecification SSEEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n # \u2500\u2500 generate docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.docs:\n summary: >-\n Generate model-specification documentation (Markdown + Mermaid) from\n TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models (and, optionally, query / command\n definitions), builds the serializable manifest / operations bundle, then\n renders human-facing model documentation \u2014 Table Overview, Entity\n Relationship Diagram, Physical Key Schema, Property Matrix, Entity\n Details, Access Patterns, Mutation Contracts, Maintained Access Paths,\n CDC, and CloudFormation summary \u2014 as Markdown with Mermaid diagrams. The\n layout is defined by bundled Handlebars templates users can override with\n --template-dir (per-partial) and extend with --helpers. Output is\n deterministic for a given set of inputs.\n usage:\n - graphddb generate docs --entry models.ts --out doc.md\n - graphddb generate docs --entry models.ts --queries definitions.ts --out doc.md\n - graphddb generate docs --entry models.ts --out docs/\n - graphddb generate docs --entry models.ts --out docs/ --template-dir doc-templates/ --helpers doc-helpers.js\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import). May also export queries / commands.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: queries\n aliases: [q]\n description: >-\n Module exporting `queries` / `commands` (defineQueries /\n defineCommands maps) used for the Access Patterns / Mutation\n Contracts sections. Defaults to the entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: out\n aliases: [o]\n required: true\n description: >-\n Output path for the generated documentation. A file path ending in\n .md writes the single Markdown file at exactly that path (creating\n parent directories); any other value is treated as a directory and\n the file is written as <dir>/index.md.\n value_name: path\n schema:\n type: string\n\n - name: format\n aliases: [f]\n description: Output format. Only markdown is supported today.\n value_name: format\n schema:\n type: string\n enum: [markdown]\n default: markdown\n\n - name: split\n description: >-\n Output granularity. none (default) emits a single index.md; entity\n is reserved for a future per-entity split.\n value_name: mode\n schema:\n type: string\n enum: [none, entity]\n default: none\n\n - name: template-dir\n description: >-\n Directory of override <partial>.hbs templates. Each same-named\n partial (index / table-overview / er-diagram / key-schema /\n property-matrix / entity / access-pattern / maintained / cdc /\n cloudformation) overrides the bundled one; absent partials fall back\n to the bundled default.\n value_name: dir\n schema:\n type: string\n\n - name: helpers\n description: >-\n JS module whose default (or named `helpers`) export is a map of extra\n Handlebars helpers to register alongside the built-in set (eq / join\n / pascal / mermaidId / codeOrEmpty).\n value_name: file\n schema:\n type: string\n\n - name: billing-mode\n description: >-\n DynamoDB billing mode used in the Table Overview (shared with\n generate cloudformation). PAY_PER_REQUEST (default) or PROVISIONED.\n value_name: mode\n schema:\n type: string\n enum: [PAY_PER_REQUEST, PROVISIONED]\n default: PAY_PER_REQUEST\n\n - name: stream\n description: >-\n DynamoDB Streams state used in the Table Overview (shared with\n generate cloudformation). auto (default) detects stream-based\n maintenance (updateMode:'stream'); on always enabled; off never.\n value_name: mode\n schema:\n type: string\n enum: [auto, on, off]\n default: auto\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\ncomponents:\n schemas:\n GenerateResult:\n type: object\n required: [status, outDir, files]\n properties:\n status:\n type: string\n enum: [ok]\n outDir:\n type: string\n description: Output directory the files were written to.\n files:\n type: array\n description: Paths of the generated files (relative to outDir).\n items:\n type: string\n Error:\n type: object\n required: [code, message]\n properties:\n code:\n type: string\n message:\n type: string\n";
573
+ var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "GraphDDB CLI",\n "version": "0.1.0",\n "description": "Contract for the graphddb code-generation CLI. The CLI reads TypeScript GraphDDB model / query / command definitions (the single source of truth) and emits Python bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) for the Python bridge runtime.",\n "license": {\n "name": "MIT"\n },\n "contact": {\n "name": "foo-log-inc",\n "url": "https://github.com/foo-log-inc/graphddb"\n }\n },\n "artifact_slots": {\n "ts-definitions": {\n "description": "TypeScript GraphDDB definition modules (entity models, defineQueries, defineCommands) used as the single source of truth.",\n "direction": "read"\n },\n "generated-code": {\n "description": "Generated Python bindings and JSON specs written to the output directory (manifest.json, operations.json, types.py, repositories.py, __init__.py).",\n "direction": "write"\n }\n },\n "command_sets": {\n "graphddb": {\n "summary": "Graph data modeling on DynamoDB \u2014 code generation CLI.",\n "x-stdin": "No command reads from stdin. All inputs are provided via file options.",\n "commands": {\n "generate.python": {\n "summary": "Generate Python bindings from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models, query definitions, and command definitions, builds the serializable manifest / operations bundle, and writes the Python bridge bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) to the output directory. Output is deterministic for a given set of inputs.",\n "usage": [\n "graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/",\n "graphddb generate python --entry definitions.ts --out generated/"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import). May also export queries / commands.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "queries",\n "aliases": [\n "q"\n ],\n "description": "Module exporting `queries` (a defineQueries map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "commands",\n "aliases": [\n "c"\n ],\n "description": "Module exporting `commands` (a defineCommands map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "transactions",\n "aliases": [\n "t"\n ],\n "description": "Module exporting `transactions` (a defineTransactions map). Defaults to the commands module (or the entry module) when omitted. Optional \u2014 a project with no declarative transactions omits it.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contracts",\n "description": "Module exporting `contracts` (a map of publicQueryModel / publicCommandModel results \u2014 the CQRS Contract layer). Defaults to the entry module when omitted. Optional \u2014 a project with no contracts omits it, and the output gains no `contracts` content.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contexts",\n "description": "Module exporting `contexts` (context-ownership: context name \u2192 the Model / Contract names that belong to it). Defaults to the contracts module (or the entry module) when omitted. Optional \u2014 consumed by the boundary lint to tell own from foreign.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "dataclass",\n "description": "Emit Python result / element types as @dataclass classes instead of TypedDict (decimal / Optional / Connection mapped accordingly).",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "required": true,\n "description": "Output directory for the generated Python bindings.",\n "value_name": "dir",\n "schema": {\n "type": "string"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n },\n "files": [\n {\n "path": "{options.out}/manifest.json",\n "required": true,\n "media_type": "application/json",\n "description": "Entity / table / key / relation metadata."\n },\n {\n "path": "{options.out}/operations.json",\n "required": true,\n "media_type": "application/json",\n "description": "Per-query / per-command execution specs."\n },\n {\n "path": "{options.out}/types.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "TypedDict result types derived from query selects."\n },\n {\n "path": "{options.out}/repositories.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Per-entity repository classes (sync methods)."\n },\n {\n "path": "{options.out}/__init__.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Package re-exports for the generated bindings."\n }\n ]\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n },\n "generate.cloudformation": {\n "summary": "Generate an AWS CloudFormation template (DynamoDB table + GSIs) from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models and builds the serializable manifest, then emits an AWS CloudFormation template describing the DynamoDB table(s) the single-table design maps onto: one `AWS::DynamoDB::Table` per physical table, with the base key schema (`PK` / `SK`), the unioned Global Secondary Indexes (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a `TableName` parameter (default = physical name), and Outputs (table name + ARN). Output is deterministic and, by default, YAML.",\n "usage": [\n "graphddb generate cloudformation --entry models.ts --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --format json",\n "graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --stream on --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import).",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "description": "Output file for the generated CloudFormation template. Written to stdout when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "format",\n "aliases": [\n "f"\n ],\n "description": "Output serialization format (yaml or json).",\n "value_name": "format",\n "schema": {\n "type": "string",\n "enum": [\n "yaml",\n "json"\n ],\n "default": "yaml"\n }\n },\n {\n "name": "gsi-projection",\n "description": "GSI ProjectionType. ALL (default) projects every attribute; KEYS_ONLY projects only the key attributes.",\n "value_name": "projection",\n "schema": {\n "type": "string",\n "enum": [\n "ALL",\n "KEYS_ONLY"\n ],\n "default": "ALL"\n }\n },\n {\n "name": "stream",\n "description": "DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission. auto (default) emits a stream only when the model set uses stream-based maintenance (updateMode:\'stream\'); on always emits; off never emits. When enabled, a StreamArn Output is added too.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "auto",\n "on",\n "off"\n ],\n "default": "auto"\n }\n },\n {\n "name": "billing-mode",\n "description": "DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand (no provisioned throughput). PROVISIONED emits BillingMode: PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the table and every GSI, and unlocks Application Auto Scaling (--autoscale).",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "PAY_PER_REQUEST",\n "PROVISIONED"\n ],\n "default": "PAY_PER_REQUEST"\n }\n },\n {\n "name": "rcu",\n "description": "Provisioned ReadCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "wcu",\n "description": "Provisioned WriteCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale",\n "description": "Enable Application Auto Scaling. Emits an AWS::ApplicationAutoScaling::ScalableTarget + AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read and write capacity AND for each GSI read and write capacity. Only valid with --billing-mode PROVISIONED (autoscaling is invalid on on-demand and is rejected). Off by default \u2014 no autoscaling resources are emitted unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "autoscale-min",\n "description": "Auto Scaling minimum capacity (MinCapacity) for every scalable target. Must be <= --autoscale-max. Only meaningful with --autoscale. Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-max",\n "description": "Auto Scaling maximum capacity (MaxCapacity) for every scalable target. Must be >= --autoscale-min. Only meaningful with --autoscale. Defaults to 100 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-target",\n "description": "Auto Scaling target utilization percentage (TargetValue of the TargetTrackingScaling policy). Only meaningful with --autoscale. Defaults to 70 when omitted.",\n "value_name": "percent",\n "schema": {\n "type": "integer",\n "minimum": 1,\n "maximum": 100\n }\n },\n {\n "name": "pitr",\n "description": "Enable point-in-time recovery (PointInTimeRecoverySpecification PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "table-class",\n "description": "DynamoDB table class. Omitted by default (AWS defaults to STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit TableClass.",\n "value_name": "class",\n "schema": {\n "type": "string",\n "enum": [\n "STANDARD",\n "STANDARD_INFREQUENT_ACCESS"\n ]\n }\n },\n {\n "name": "sse",\n "description": "Enable server-side encryption with the AWS-owned/managed key (SSESpecification SSEEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n }\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n },\n "generate.docs": {\n "summary": "Generate model-specification documentation (Markdown + Mermaid) from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models (and, optionally, query / command definitions), builds the serializable manifest / operations bundle, then renders human-facing model documentation \u2014 Table Overview, Entity Relationship Diagram, Physical Key Schema, Property Matrix, Entity Details, Access Patterns, Mutation Contracts, Maintained Access Paths, CDC, and CloudFormation summary \u2014 as Markdown with Mermaid diagrams. The layout is defined by bundled Handlebars templates users can override with --template-dir (per-partial) and extend with --helpers. Output is deterministic for a given set of inputs.",\n "usage": [\n "graphddb generate docs --entry models.ts --out doc.md",\n "graphddb generate docs --entry models.ts --queries definitions.ts --out doc.md",\n "graphddb generate docs --entry models.ts --out docs/",\n "graphddb generate docs --entry models.ts --out docs/ --template-dir doc-templates/ --helpers doc-helpers.js"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import). May also export queries / commands.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "queries",\n "aliases": [\n "q"\n ],\n "description": "Module exporting `queries` / `commands` (defineQueries / defineCommands maps) used for the Access Patterns / Mutation Contracts sections. Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "required": true,\n "description": "Output path for the generated documentation. A file path ending in .md writes the single Markdown file at exactly that path (creating parent directories); any other value is treated as a directory and the file is written as <dir>/index.md.",\n "value_name": "path",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "format",\n "aliases": [\n "f"\n ],\n "description": "Output format. Only markdown is supported today.",\n "value_name": "format",\n "schema": {\n "type": "string",\n "enum": [\n "markdown"\n ],\n "default": "markdown"\n }\n },\n {\n "name": "split",\n "description": "Output granularity. none (default) emits a single index.md; entity is reserved for a future per-entity split.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "none",\n "entity"\n ],\n "default": "none"\n }\n },\n {\n "name": "template-dir",\n "description": "Directory of override <partial>.hbs templates. Each same-named partial (index / table-overview / er-diagram / key-schema / property-matrix / entity / access-pattern / maintained / cdc / cloudformation) overrides the bundled one; absent partials fall back to the bundled default.",\n "value_name": "dir",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "helpers",\n "description": "JS module whose default (or named `helpers`) export is a map of extra Handlebars helpers to register alongside the built-in set (eq / join / pascal / mermaidId / codeOrEmpty).",\n "value_name": "file",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "billing-mode",\n "description": "DynamoDB billing mode used in the Table Overview (shared with generate cloudformation). PAY_PER_REQUEST (default) or PROVISIONED.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "PAY_PER_REQUEST",\n "PROVISIONED"\n ],\n "default": "PAY_PER_REQUEST"\n }\n },\n {\n "name": "stream",\n "description": "DynamoDB Streams state used in the Table Overview (shared with generate cloudformation). auto (default) detects stream-based maintenance (updateMode:\'stream\'); on always enabled; off never.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "auto",\n "on",\n "off"\n ],\n "default": "auto"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n }\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n }\n }\n }\n },\n "components": {\n "schemas": {\n "GenerateResult": {\n "type": "object",\n "required": [\n "status",\n "outDir",\n "files"\n ],\n "properties": {\n "status": {\n "type": "string",\n "enum": [\n "ok"\n ]\n },\n "outDir": {\n "type": "string",\n "description": "Output directory the files were written to."\n },\n "files": {\n "type": "array",\n "description": "Paths of the generated files (relative to outDir).",\n "items": {\n "type": "string"\n }\n }\n }\n },\n "Error": {\n "type": "object",\n "required": [\n "code",\n "message"\n ],\n "properties": {\n "code": {\n "type": "string"\n },\n "message": {\n "type": "string"\n }\n }\n }\n }\n }\n}';
471
574
 
472
575
  // src/generated/program.ts
473
576
  function createProgram(handlers2, version) {
@@ -493,6 +596,15 @@ function createProgram(handlers2, version) {
493
596
  }
494
597
  await handlers2.generateCloudformation(opts, globalOpts);
495
598
  });
599
+ __cmd_generate.command("docs").description("Generate model-specification documentation (Markdown + Mermaid) from TypeScript GraphDDB definitions.").option("-e, --entry <file>", "Entry module that registers the entity models (side-effecting import). May also export queries / commands.").option("-q, --queries <file>", "Module exporting `queries` / `commands` (defineQueries / defineCommands maps) used for the Access Patterns / Mutation Contracts sections. Defaults to the entry module when omitted.").option("-o, --out <path>", "Output path for the generated documentation. A file path ending in .md writes the single Markdown file at exactly that path (creating parent directories); any other value is treated as a directory and the file is written as <dir>/index.md.").option("-f, --format <format>", "Output format. Only markdown is supported today.", "markdown").option("--split <mode>", "Output granularity. none (default) emits a single index.md; entity is reserved for a future per-entity split.", "none").option("--template-dir <dir>", "Directory of override <partial>.hbs templates. Each same-named partial (index / table-overview / er-diagram / key-schema / property-matrix / entity / access-pattern / maintained / cdc / cloudformation) overrides the bundled one; absent partials fall back to the bundled default.").option("--helpers <file>", "JS module whose default (or named `helpers`) export is a map of extra Handlebars helpers to register alongside the built-in set (eq / join / pascal / mermaidId / codeOrEmpty).").option("--billing-mode <mode>", "DynamoDB billing mode used in the Table Overview (shared with generate cloudformation). PAY_PER_REQUEST (default) or PROVISIONED.", "PAY_PER_REQUEST").option("--stream <mode>", "DynamoDB Streams state used in the Table Overview (shared with generate cloudformation). auto (default) detects stream-based maintenance (updateMode:'stream'); on always enabled; off never.", "auto").action(async (opts, cmd) => {
600
+ const globalOpts = cmd.optsWithGlobals();
601
+ if (globalOpts.introspect) {
602
+ const policy = deriveCommandPolicy("generate.docs", opts);
603
+ console.log(JSON.stringify(policy, null, 2));
604
+ return;
605
+ }
606
+ await handlers2.generateDocs(opts, globalOpts);
607
+ });
496
608
  program.command("extract").description("Extract contract specification for this CLI tool.").argument("[commands...]", "Command IDs to extract. Use dot notation.").option("-a, --all", "Extract all commands.", false).option("--include-meta", "Include extraction metadata.", true).option("-F, --format <format>", "Output format (yaml or json).", "yaml").action(async (commands, opts, cmd) => {
497
609
  if (commands.length === 0 && !opts.all) {
498
610
  process.stderr.write(JSON.stringify({ code: "INVALID_ARGS", message: "Specify command IDs or use --all" }) + "\n");
@@ -510,7 +622,7 @@ function createProgram(handlers2, version) {
510
622
  type: "cli-contracts/extract",
511
623
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
512
624
  specVersion: doc.cli_contracts ?? "0.1.0",
513
- commands: ["graphddb.generate.python", "graphddb.generate.cloudformation"]
625
+ commands: ["graphddb.generate.python", "graphddb.generate.cloudformation", "graphddb.generate.docs"]
514
626
  };
515
627
  }
516
628
  Object.assign(out, doc);
@@ -527,7 +639,7 @@ function createProgram(handlers2, version) {
527
639
  yamlLines.push("extractedAt: " + (/* @__PURE__ */ new Date()).toISOString());
528
640
  yamlLines.push("spec_version: " + (doc.cli_contracts ?? "0.1.0"));
529
641
  yamlLines.push("commands:");
530
- for (const id of ["graphddb.generate.python", "graphddb.generate.cloudformation"]) {
642
+ for (const id of ["graphddb.generate.python", "graphddb.generate.cloudformation", "graphddb.generate.docs"]) {
531
643
  yamlLines.push(" - " + id);
532
644
  }
533
645
  }
@@ -570,7 +682,7 @@ function createProgram(handlers2, version) {
570
682
  import { mkdirSync, writeFileSync } from "fs";
571
683
  import { dirname, relative } from "path";
572
684
  import { pathToFileURL } from "url";
573
- import { isAbsolute, resolve } from "path";
685
+ import { isAbsolute, resolve as resolve2 } from "path";
574
686
 
575
687
  // src/codegen/python.ts
576
688
  var PY_HEADER = "# DO NOT EDIT. Generated by GraphDDB.";
@@ -659,6 +771,9 @@ function collectResultTypes(manifest, entity, select, typeName, out, seen) {
659
771
  } else {
660
772
  fields.push([field, `${itemTypeName} | None`]);
661
773
  }
774
+ if (relation.description !== void 0) {
775
+ fieldDescriptions[field] = relation.description;
776
+ }
662
777
  continue;
663
778
  }
664
779
  if (value === true) {
@@ -790,7 +905,15 @@ function methodParams(def) {
790
905
  }
791
906
  return out;
792
907
  }
793
- function collectRepositories(queries, commands) {
908
+ function gsiDescriptionForQuery(name, entity, manifest, querySpecs) {
909
+ if (!manifest || !querySpecs) return void 0;
910
+ const spec = querySpecs[name];
911
+ const indexName = spec?.operations[0]?.indexName;
912
+ if (indexName === void 0) return void 0;
913
+ const gsi = manifest.entities[entity]?.gsis.find((g) => g.indexName === indexName);
914
+ return gsi?.description;
915
+ }
916
+ function collectRepositories(queries, commands, manifest, querySpecs) {
794
917
  const byEntity = /* @__PURE__ */ new Map();
795
918
  const ensure = (entity) => {
796
919
  const cls = `${entityBaseName(entity)}Repository`;
@@ -803,13 +926,14 @@ function collectRepositories(queries, commands) {
803
926
  };
804
927
  for (const name of Object.keys(queries).sort()) {
805
928
  const def = queries[name];
929
+ const description = def.description ?? gsiDescriptionForQuery(name, def.entity.name, manifest, querySpecs);
806
930
  ensure(def.entity.name).push({
807
931
  methodName: toSnakeCase(name),
808
932
  params: methodParams(def),
809
933
  returnType: `${queryResultTypeName(name)} | None`,
810
934
  kind: "query",
811
935
  operationId: name,
812
- ...def.description !== void 0 ? { description: def.description } : {}
936
+ ...description !== void 0 ? { description } : {}
813
937
  });
814
938
  }
815
939
  for (const name of Object.keys(commands).sort()) {
@@ -914,8 +1038,8 @@ function renderTransactionsClass(methods) {
914
1038
  body.join("\n\n")
915
1039
  ].join("\n");
916
1040
  }
917
- function generateRepositories(queries, commands, transactions = {}) {
918
- const classes = collectRepositories(queries, commands);
1041
+ function generateRepositories(queries, commands, transactions = {}, manifest, querySpecs) {
1042
+ const classes = collectRepositories(queries, commands, manifest, querySpecs);
919
1043
  const txMethods = collectTransactionMethods(transactions);
920
1044
  const resultTypes = /* @__PURE__ */ new Set();
921
1045
  for (const name of Object.keys(queries)) {
@@ -1450,6 +1574,662 @@ function generateCdcRegistry(options = {}) {
1450
1574
  return lines.join("\n");
1451
1575
  }
1452
1576
 
1577
+ // src/codegen/docs-model.ts
1578
+ var AS_CONFIGURED = "as configured at deployment";
1579
+ function fieldTypeLabel(entity, field) {
1580
+ const f = entity.fields[field];
1581
+ if (!f) return "";
1582
+ if (f.format === "datetime") return "datetime";
1583
+ if (f.format === "date") return "date";
1584
+ return f.type;
1585
+ }
1586
+ function collectGsiColumns(manifest) {
1587
+ const names = /* @__PURE__ */ new Set();
1588
+ for (const entity of Object.values(manifest.entities)) {
1589
+ for (const gsi of entity.gsis) names.add(gsi.indexName);
1590
+ }
1591
+ return [...names].sort();
1592
+ }
1593
+ function gsiByName(entity, indexName) {
1594
+ return entity.gsis.find((g) => g.indexName === indexName);
1595
+ }
1596
+ function propertyRole(entity, property) {
1597
+ const key = entity.key;
1598
+ if (key) {
1599
+ if (referencesField(key.pkTemplate, property)) return "PK";
1600
+ if (key.skTemplate && referencesField(key.skTemplate, property)) return "SK";
1601
+ }
1602
+ for (const gsi of entity.gsis) {
1603
+ if (referencesField(gsi.pkTemplate, property)) return `${gsi.indexName} PK`;
1604
+ if (gsi.skTemplate && referencesField(gsi.skTemplate, property)) {
1605
+ return `${gsi.indexName} SK`;
1606
+ }
1607
+ }
1608
+ if (entity.fields[property]) return "\u2713";
1609
+ return "";
1610
+ }
1611
+ function referencesField(template, field) {
1612
+ return template.includes(`{${field}}`);
1613
+ }
1614
+ function keyLabel(prefix, template) {
1615
+ return template ? `${prefix} = ${template}` : "";
1616
+ }
1617
+ function rootIndex(spec) {
1618
+ const root = spec.operations[0];
1619
+ if (!root) return "";
1620
+ return root.indexName ?? "PK";
1621
+ }
1622
+ function isRelation(spec) {
1623
+ return spec.operations.length > 1;
1624
+ }
1625
+ function usesBatchGet(spec) {
1626
+ return spec.operations.some((op) => op.type === "BatchGetItem");
1627
+ }
1628
+ function usesCursor(spec) {
1629
+ return (spec.cardinality ?? "many") === "many";
1630
+ }
1631
+ function inputShape(spec) {
1632
+ const parts = [];
1633
+ for (const name of Object.keys(spec.params).sort()) {
1634
+ const p = spec.params[name];
1635
+ const opt = p.required ? "" : "?";
1636
+ parts.push(`${name}${opt}: ${p.type}`);
1637
+ }
1638
+ if ("cardinality" in spec && (spec.cardinality ?? "many") === "many") {
1639
+ parts.push("limit?: number", "cursor?: string");
1640
+ }
1641
+ return `{ ${parts.join(", ")} }`;
1642
+ }
1643
+ function opPkTemplate(op) {
1644
+ if (op.indexName) {
1645
+ return op.keyCondition[`${op.indexName}PK`] ?? op.keyCondition["PK"] ?? op.keyCondition["pk"] ?? "";
1646
+ }
1647
+ return op.keyCondition["PK"] ?? op.keyCondition["pk"] ?? "";
1648
+ }
1649
+ function rootEntityName(spec, manifest) {
1650
+ const root = spec.operations[0];
1651
+ if (!root) return "";
1652
+ const pk = opPkTemplate(root);
1653
+ for (const [name, entity] of Object.entries(manifest.entities)) {
1654
+ const cand = root.indexName ? gsiByName(entity, root.indexName)?.pkTemplate : entity.key?.pkTemplate;
1655
+ if (cand && pk && cand === pk) return name;
1656
+ }
1657
+ return "";
1658
+ }
1659
+ function outputShape(spec, entityName) {
1660
+ const many = (spec.cardinality ?? "many") === "many";
1661
+ const base = entityName || "Result";
1662
+ return many ? `Page<${base}>` : base;
1663
+ }
1664
+ function buildResolution(spec, manifest) {
1665
+ const root = spec.operations[0];
1666
+ if (!root) return { resolve: "", diagram: [] };
1667
+ if (spec.operations.length === 1) {
1668
+ const keyLabelStr = rootKeyLabel(root);
1669
+ const proj = root.projection.length > 0 ? `Projection(${root.projection.slice(0, 4).join(", ")})` : "Projection";
1670
+ const resolve3 = `${keyLabelStr} \u2192 ${root.type} \u2192 ${proj}`;
1671
+ const inParam = Object.keys(spec.params).sort()[0] ?? "key";
1672
+ const outEntity = rootEntityName(spec, manifest) || "Result";
1673
+ const diagram2 = [
1674
+ { label: inParam, edge: "" },
1675
+ { label: keyLabelStr, edge: "" },
1676
+ { label: root.type, edge: "" },
1677
+ { label: outEntity, edge: "" }
1678
+ ];
1679
+ return { resolve: resolve3, diagram: diagram2 };
1680
+ }
1681
+ const entityHops = [];
1682
+ const diagram = [];
1683
+ for (let i = 0; i < spec.operations.length; i++) {
1684
+ const op = spec.operations[i];
1685
+ const entity = opEntityName(op, manifest) || op.tableName;
1686
+ entityHops.push(`${op.type}(${entity}${op.indexName ? ` by ${op.indexName}` : ""})`);
1687
+ const edge = i < spec.operations.length - 1 ? spec.operations[i + 1].type === "BatchGetItem" ? "BatchGet" : op.indexName ?? "PK" : "";
1688
+ diagram.push({ label: entity, edge });
1689
+ }
1690
+ return { resolve: entityHops.join(" \u2192 "), diagram };
1691
+ }
1692
+ function rootKeyLabel(op) {
1693
+ const pk = opPkTemplate(op);
1694
+ const prefix = op.indexName ? `${op.indexName} PK` : "PK";
1695
+ return `${prefix} = ${pk}`;
1696
+ }
1697
+ function opEntityName(op, manifest) {
1698
+ const pk = opPkTemplate(op);
1699
+ for (const [name, entity] of Object.entries(manifest.entities)) {
1700
+ const cand = op.indexName ? gsiByName(entity, op.indexName)?.pkTemplate : entity.key?.pkTemplate;
1701
+ if (cand && pk) {
1702
+ if (literalPrefix(cand) === literalPrefix(pk)) return name;
1703
+ }
1704
+ }
1705
+ return "";
1706
+ }
1707
+ function literalPrefix(template) {
1708
+ const idx = template.indexOf("{");
1709
+ return idx === -1 ? template : template.slice(0, idx);
1710
+ }
1711
+ function commandSignature(_name, spec) {
1712
+ const verb = spec.type === "PutItem" ? "create" : spec.type === "UpdateItem" ? "update" : "delete";
1713
+ return `${spec.entity}.${verb}()`;
1714
+ }
1715
+ function buildMaintainedPaths(registry) {
1716
+ const rows = [];
1717
+ const entities = registry.getAll();
1718
+ const byName = /* @__PURE__ */ new Map();
1719
+ for (const [cls, meta] of entities) byName.set(cls.name, meta);
1720
+ for (const name of [...byName.keys()].sort()) {
1721
+ const meta = byName.get(name);
1722
+ for (const rel of meta.relations) {
1723
+ const row = maintainedRowForRelation(name, rel);
1724
+ if (row) rows.push(row);
1725
+ }
1726
+ for (const agg of meta.aggregates) {
1727
+ const row = maintainedRowForAggregate(name, agg);
1728
+ if (row) rows.push(row);
1729
+ }
1730
+ }
1731
+ return rows;
1732
+ }
1733
+ function maintainedRowForRelation(ownerName, rel) {
1734
+ const pattern = rel.options?.pattern;
1735
+ if (!pattern) return null;
1736
+ const triggers = rel.options?.write?.maintainedOn ?? [];
1737
+ const source = sourceFromTriggers(triggers) || rel.targetFactory().name;
1738
+ const target = `${ownerName}.${rel.propertyName}`;
1739
+ const update = pattern === "counter" ? "ADD +1 / \u22121 on create / remove" : `project ${source} fields into ${ownerName}`;
1740
+ return { source, target, pattern, update };
1741
+ }
1742
+ function maintainedRowForAggregate(ownerName, agg) {
1743
+ const pattern = agg.options.pattern;
1744
+ if (!pattern) return null;
1745
+ const triggers = agg.options.write?.maintainedOn ?? [];
1746
+ const source = sourceFromTriggers(triggers) || agg.targetFactory().name;
1747
+ const target = `${ownerName}.${agg.propertyName}`;
1748
+ const update = pattern === "counter" ? "ADD +1 / \u22121 on create / remove" : `project ${source} into ${ownerName}`;
1749
+ return { source, target, pattern, update };
1750
+ }
1751
+ function sourceFromTriggers(triggers) {
1752
+ for (const t of triggers) {
1753
+ const dot = t.indexOf(".");
1754
+ if (dot > 0) return t.slice(0, dot);
1755
+ }
1756
+ return "";
1757
+ }
1758
+ function buildCdcEntries(registry) {
1759
+ const names = [];
1760
+ for (const [cls, meta] of registry.getAll()) {
1761
+ if (meta.cdcProjected) names.push(cls.name);
1762
+ }
1763
+ return names.sort().map((model) => ({ model }));
1764
+ }
1765
+ function buildCfnResources(manifest, streamsEnabled) {
1766
+ const rows = [
1767
+ { resource: "AWS::DynamoDB::Table", description: "Physical table" }
1768
+ ];
1769
+ const gsiCols = collectGsiColumns(manifest);
1770
+ if (gsiCols.length > 0) {
1771
+ rows.push({
1772
+ resource: "GlobalSecondaryIndexes",
1773
+ description: `GSI definitions (${gsiCols.join(", ")})`
1774
+ });
1775
+ }
1776
+ rows.push({
1777
+ resource: "StreamSpecification",
1778
+ description: streamsEnabled === void 0 ? AS_CONFIGURED : streamsEnabled ? "NEW_AND_OLD_IMAGES" : "disabled"
1779
+ });
1780
+ const hasTtl = Object.values(manifest.entities).some((e) => e.ttlAttribute);
1781
+ if (hasTtl) {
1782
+ rows.push({
1783
+ resource: "TimeToLiveSpecification",
1784
+ description: "TTL (when a TTL attribute is declared)"
1785
+ });
1786
+ }
1787
+ rows.push(
1788
+ { resource: "PointInTimeRecovery", description: "PITR" },
1789
+ { resource: "Auto Scaling", description: "Optional (PROVISIONED mode)" }
1790
+ );
1791
+ return rows;
1792
+ }
1793
+ function buildDocModel(manifest, operations, registry = MetadataRegistry, options = {}) {
1794
+ const entityNames = Object.keys(manifest.entities).sort();
1795
+ const table = entityNames.length > 0 ? manifest.entities[entityNames[0]].table : "";
1796
+ const title = options.title ?? `${table} Table Specification`;
1797
+ const cfnFile = options.cfnFile ?? "cloudformation.yaml";
1798
+ const overview = [
1799
+ { item: "Table", value: `\`${table}\`` },
1800
+ {
1801
+ item: "Billing Mode",
1802
+ value: options.billingMode ?? "PAY_PER_REQUEST"
1803
+ },
1804
+ {
1805
+ item: "Streams",
1806
+ value: options.streamsEnabled === void 0 ? AS_CONFIGURED : options.streamsEnabled ? "NEW_AND_OLD_IMAGES" : "Disabled"
1807
+ },
1808
+ {
1809
+ item: "TTL",
1810
+ value: Object.values(manifest.entities).some((e) => e.ttlAttribute) ? "Enabled" : "Disabled"
1811
+ },
1812
+ { item: "PITR", value: AS_CONFIGURED },
1813
+ { item: "Table Class", value: AS_CONFIGURED },
1814
+ { item: "Encryption", value: AS_CONFIGURED },
1815
+ { item: "CloudFormation", value: `\`${cfnFile}\`` }
1816
+ ];
1817
+ const relationEdges = [];
1818
+ for (const name of entityNames) {
1819
+ const entity = manifest.entities[name];
1820
+ for (const propName of Object.keys(entity.relations).sort()) {
1821
+ const rel = entity.relations[propName];
1822
+ relationEdges.push({
1823
+ source: name,
1824
+ target: rel.target,
1825
+ label: rel.type
1826
+ });
1827
+ }
1828
+ }
1829
+ const gsiColumns = collectGsiColumns(manifest);
1830
+ const keySchema = entityNames.map((name) => {
1831
+ const entity = manifest.entities[name];
1832
+ return {
1833
+ model: name,
1834
+ pk: entity.key?.pkTemplate ?? "",
1835
+ sk: entity.key?.skTemplate ?? "",
1836
+ gsis: gsiColumns.map((col) => {
1837
+ const g = gsiByName(entity, col);
1838
+ return { pk: g?.pkTemplate ?? "", sk: g?.skTemplate ?? "" };
1839
+ })
1840
+ };
1841
+ });
1842
+ const propertyOrder = [];
1843
+ const seen = /* @__PURE__ */ new Set();
1844
+ for (const name of entityNames) {
1845
+ const entity = manifest.entities[name];
1846
+ for (const field of Object.keys(entity.fields).sort()) {
1847
+ if (!seen.has(field)) {
1848
+ seen.add(field);
1849
+ propertyOrder.push(field);
1850
+ }
1851
+ }
1852
+ }
1853
+ const propertyMatrix = propertyOrder.map((property) => {
1854
+ let type = "";
1855
+ let description = "";
1856
+ for (const name of entityNames) {
1857
+ const entity = manifest.entities[name];
1858
+ const field = entity.fields[property];
1859
+ if (!field) continue;
1860
+ if (!type) type = fieldTypeLabel(entity, property);
1861
+ if (!description && field.description) description = field.description;
1862
+ }
1863
+ const cells = entityNames.map(
1864
+ (name) => propertyRole(manifest.entities[name], property)
1865
+ );
1866
+ return { property, type, description, cells };
1867
+ });
1868
+ const propertyMatrixHasDescriptions = propertyMatrix.some(
1869
+ (row) => row.description !== ""
1870
+ );
1871
+ const maintainedPaths = buildMaintainedPaths(registry);
1872
+ const entities = entityNames.map((name) => {
1873
+ const entity = manifest.entities[name];
1874
+ const relations = Object.keys(entity.relations).sort().map((propName) => {
1875
+ const rel = entity.relations[propName];
1876
+ return { property: propName, target: rel.target, type: rel.type };
1877
+ });
1878
+ const maintained = maintainedPaths.filter((row) => row.target.startsWith(`${name}.`)).map((row) => ({
1879
+ field: row.target.slice(name.length + 1),
1880
+ pattern: row.pattern
1881
+ }));
1882
+ return {
1883
+ name,
1884
+ description: entity.description ?? "",
1885
+ pk: keyLabel("PK", entity.key?.pkTemplate ?? null),
1886
+ sk: keyLabel("SK", entity.key?.skTemplate ?? null),
1887
+ gsis: entity.gsis.map((g) => ({
1888
+ indexName: g.indexName,
1889
+ pk: g.pkTemplate,
1890
+ sk: g.skTemplate ?? "",
1891
+ unique: g.unique,
1892
+ description: g.description ?? ""
1893
+ })),
1894
+ relations,
1895
+ maintained,
1896
+ fields: Object.keys(entity.fields).sort().map((field) => ({
1897
+ field,
1898
+ type: fieldTypeLabel(entity, field),
1899
+ description: entity.fields[field].description ?? ""
1900
+ })),
1901
+ hasFieldDescriptions: Object.values(entity.fields).some(
1902
+ (f) => (f.description ?? "") !== ""
1903
+ )
1904
+ };
1905
+ });
1906
+ const accessPatterns = [];
1907
+ const accessMatrix = [];
1908
+ for (const name of Object.keys(operations.queries).sort()) {
1909
+ const spec = operations.queries[name];
1910
+ const entityName = rootEntityName(spec, manifest);
1911
+ const { resolve: resolve3, diagram } = buildResolution(spec, manifest);
1912
+ const relation = isRelation(spec);
1913
+ const batchGet = usesBatchGet(spec);
1914
+ const cursor = usesCursor(spec);
1915
+ const index = rootIndex(spec);
1916
+ const signature = `${entityName || "Query"}.${name}(${Object.keys(spec.params).sort().join(", ")})`;
1917
+ accessPatterns.push({
1918
+ signature,
1919
+ description: spec.description ?? "",
1920
+ index,
1921
+ input: inputShape(spec),
1922
+ output: outputShape(spec, entityName),
1923
+ resolve: resolve3,
1924
+ relation,
1925
+ batchGet,
1926
+ cursor,
1927
+ diagram
1928
+ });
1929
+ accessMatrix.push({ query: signature, index, relation, batchGet, cursor });
1930
+ }
1931
+ const mutationContracts = [];
1932
+ for (const name of Object.keys(operations.commands).sort()) {
1933
+ const spec = operations.commands[name];
1934
+ const verb = spec.type === "PutItem" ? "TransactWrite \u2192 " + spec.entity + " item" : spec.type === "UpdateItem" ? "TransactWrite \u2192 " + spec.entity + " item" : "TransactWrite \u2192 delete " + spec.entity + " item";
1935
+ mutationContracts.push({
1936
+ signature: commandSignature(name, spec),
1937
+ description: spec.description ?? "",
1938
+ input: inputShape(spec),
1939
+ output: spec.entity,
1940
+ resolve: verb
1941
+ });
1942
+ }
1943
+ const cdcEntries = buildCdcEntries(registry);
1944
+ const cfnResources = buildCfnResources(manifest, options.streamsEnabled);
1945
+ return {
1946
+ table,
1947
+ title,
1948
+ overview,
1949
+ entityNames,
1950
+ relationEdges,
1951
+ gsiColumns,
1952
+ keySchema,
1953
+ propertyMatrix,
1954
+ propertyMatrixHasDescriptions,
1955
+ entities,
1956
+ accessPatterns,
1957
+ accessMatrix,
1958
+ mutationContracts,
1959
+ maintainedPaths,
1960
+ cdcEntries,
1961
+ cfnResources,
1962
+ cfnFile
1963
+ };
1964
+ }
1965
+
1966
+ // src/codegen/docs.ts
1967
+ import { existsSync, readFileSync } from "fs";
1968
+ import { resolve } from "path";
1969
+ import Handlebars from "handlebars";
1970
+
1971
+ // src/codegen/docs-templates.ts
1972
+ var PARTIAL_NAMES = [
1973
+ "index",
1974
+ "table-overview",
1975
+ "er-diagram",
1976
+ "key-schema",
1977
+ "property-matrix",
1978
+ "entity",
1979
+ "access-pattern",
1980
+ "maintained",
1981
+ "cdc",
1982
+ "cloudformation"
1983
+ ];
1984
+ var INDEX = `# {{title}}
1985
+
1986
+ > Generated by \`graphddb generate docs\`
1987
+
1988
+ ---
1989
+
1990
+ {{> table-overview }}
1991
+
1992
+ ---
1993
+
1994
+ {{> er-diagram }}
1995
+ {{> key-schema }}
1996
+
1997
+ ---
1998
+
1999
+ {{> property-matrix }}
2000
+
2001
+ ---
2002
+
2003
+ ## Entity Details
2004
+
2005
+ {{#each entities}}
2006
+ {{> entity this }}
2007
+ {{/each}}
2008
+ {{#if accessMatrix.length}}---
2009
+
2010
+ ## Access Patterns
2011
+
2012
+ Each query resolves to a physical operation, an index (PK or GSI), and a projection.
2013
+ The matrix summarizes; the contracts below give the input/output and execution for each.
2014
+
2015
+ | Query | Index | Relation | BatchGet | Cursor |
2016
+ |------|------|------|------|------|
2017
+ {{#each accessMatrix}}| \`{{query}}\` | {{index}} | {{#if relation}}\u2713{{/if}} | {{#if batchGet}}\u2713{{/if}} | {{#if cursor}}\u2713{{/if}} |
2018
+ {{/each}}
2019
+
2020
+ {{#each accessPatterns}}
2021
+ {{> access-pattern this }}
2022
+ {{/each}}
2023
+ {{/if}}
2024
+ {{#if mutationContracts.length}}---
2025
+
2026
+ ## Mutation Contracts
2027
+
2028
+ {{#each mutationContracts}}
2029
+ ### \`{{signature}}\`
2030
+ {{#if description}}
2031
+
2032
+ {{description}}
2033
+ {{/if}}
2034
+
2035
+ \`\`\`yaml
2036
+ Input: {{input}}
2037
+ Output: {{output}}
2038
+ Resolve: {{resolve}}
2039
+ \`\`\`
2040
+
2041
+ {{/each}}
2042
+ {{/if}}
2043
+ {{> maintained }}
2044
+ {{> cdc }}
2045
+ ---
2046
+
2047
+ {{> cloudformation }}
2048
+ `;
2049
+ var TABLE_OVERVIEW = `## Table Overview
2050
+
2051
+ | Item | Value |
2052
+ |------|-------|
2053
+ {{#each overview}}| {{item}} | {{value}} |
2054
+ {{/each}}`;
2055
+ var ER_DIAGRAM = `{{#if relationEdges.length}}## Entity Relationship Diagram
2056
+
2057
+ \`\`\`mermaid
2058
+ graph TD
2059
+ {{#each relationEdges}} {{mermaidId source}} -->|{{label}}| {{mermaidId target}}
2060
+ {{/each}}\`\`\`
2061
+
2062
+ ---
2063
+
2064
+ {{/if}}`;
2065
+ var KEY_SCHEMA = `## Physical Key Schema
2066
+
2067
+ | Model | PK | SK |{{#each gsiColumns}} {{this}} PK | {{this}} SK |{{/each}}
2068
+ |------|------|------|{{#each gsiColumns}}------|------|{{/each}}
2069
+ {{#each keySchema}}| {{model}} | {{codeOrEmpty pk}} | {{codeOrEmpty sk}} |{{#each gsis}} {{codeOrEmpty pk}} | {{codeOrEmpty sk}} |{{/each}}
2070
+ {{/each}}`;
2071
+ var PROPERTY_MATRIX = `## Property Matrix
2072
+
2073
+ | Property | Type |{{#if propertyMatrixHasDescriptions}} Description |{{/if}}{{#each entityNames}} {{this}} |{{/each}}
2074
+ |----------|------|{{#if propertyMatrixHasDescriptions}}-------------|{{/if}}{{#each entityNames}}------|{{/each}}
2075
+ {{#each propertyMatrix}}| {{property}} | {{type}} |{{#if ../propertyMatrixHasDescriptions}} {{description}} |{{/if}}{{#each cells}} {{this}} |{{/each}}
2076
+ {{/each}}
2077
+
2078
+ Legend: **PK** primary partition key \xB7 **SK** primary sort key \xB7 GSI keys \xB7 **\u2713** normal attribute`;
2079
+ var ENTITY = `### {{name}}
2080
+ {{#if description}}
2081
+
2082
+ {{description}}
2083
+ {{/if}}
2084
+
2085
+ - **Key**: \`{{pk}}\`{{#if sk}} / \`{{sk}}\`{{/if}}
2086
+ {{#each gsis}}- **{{indexName}}**: \`PK = {{pk}}\`{{#if sk}} / \`SK = {{sk}}\`{{/if}}{{#if unique}} (unique){{/if}}
2087
+ {{/each}}
2088
+ {{#if relations.length}}- **Relations**: {{#each relations}}\`{{property}}\` \u2192 {{target}} ({{type}}){{#unless @last}}, {{/unless}}{{/each}}
2089
+ {{/if}}
2090
+ {{#if maintained.length}}- **Maintained**: {{#each maintained}}\`{{field}}\` ({{pattern}}){{#unless @last}}, {{/unless}}{{/each}}
2091
+ {{/if}}
2092
+
2093
+ | Field | Type |{{#if hasFieldDescriptions}} Description |{{/if}}
2094
+ |------|------|{{#if hasFieldDescriptions}}------|{{/if}}
2095
+ {{#each fields}}| {{field}} | {{type}} |{{#if ../hasFieldDescriptions}} {{description}} |{{/if}}
2096
+ {{/each}}
2097
+ `;
2098
+ var ACCESS_PATTERN = `### \`{{signature}}\`
2099
+ {{#if description}}
2100
+
2101
+ {{description}}
2102
+ {{/if}}
2103
+
2104
+ \`\`\`yaml
2105
+ Input: {{input}}
2106
+ Output: {{output}}
2107
+ Resolve: {{resolve}}
2108
+ \`\`\`
2109
+
2110
+ \`\`\`mermaid
2111
+ flowchart LR
2112
+ {{#each diagram}}{{mermaidId label}}["{{label}}"]{{#unless @last}} {{#if edge}}-- "{{edge}}" {{/if}}--> {{/unless}}{{/each}}
2113
+ \`\`\`
2114
+
2115
+ `;
2116
+ var MAINTAINED = `{{#if maintainedPaths.length}}---
2117
+
2118
+ ## Maintained Access Paths
2119
+
2120
+ Write-side projections kept in sync automatically, within the source write's transaction
2121
+ (or via the CDC stream for \`updateMode: 'stream'\`).
2122
+
2123
+ | Source | Target | Pattern | Update |
2124
+ |---------|--------|--------|--------|
2125
+ {{#each maintainedPaths}}| {{source}} | {{target}} | {{pattern}} | {{update}} |
2126
+ {{/each}}
2127
+
2128
+ \`\`\`mermaid
2129
+ flowchart LR
2130
+ {{#each maintainedPaths}} {{mermaidId source}} -- "{{pattern}}" --> {{mermaidId target}}
2131
+ {{/each}}\`\`\`
2132
+
2133
+ {{/if}}`;
2134
+ var CDC = `{{#if cdcEntries.length}}---
2135
+
2136
+ ## CDC
2137
+
2138
+ \`@cdcProjected\` models expose \`fromChange\` / \`subscribe\` to parse the change stream into
2139
+ typed records. Delivery and idempotency are the consumer's responsibility.
2140
+
2141
+ {{#each cdcEntries}}
2142
+ \`\`\`yaml
2143
+ Contract: {{model}}.subscribe
2144
+ Input: ChangeEvent<{{model}}>
2145
+ Output: [{{model}} | null, {{model}} | null] # [oldRecord, newRecord]
2146
+ \`\`\`
2147
+
2148
+ \`\`\`mermaid
2149
+ flowchart LR
2150
+ Mutate --> Stream["DynamoDB Stream"] --> CE["ChangeEvent"]
2151
+ CE --> FC["{{model}}.fromChange()"] --> Sub["subscribe()"] --> Consumer
2152
+ \`\`\`
2153
+
2154
+ {{/each}}
2155
+ {{/if}}`;
2156
+ var CLOUDFORMATION = `## CloudFormation
2157
+
2158
+ Generated by \`graphddb generate cloudformation\` (see \`{{cfnFile}}\`).
2159
+
2160
+ | Resource | Description |
2161
+ |------|------|
2162
+ {{#each cfnResources}}| \`{{resource}}\` | {{description}} |
2163
+ {{/each}}`;
2164
+ var BUNDLED_PARTIALS = {
2165
+ index: INDEX,
2166
+ "table-overview": TABLE_OVERVIEW,
2167
+ "er-diagram": ER_DIAGRAM,
2168
+ "key-schema": KEY_SCHEMA,
2169
+ "property-matrix": PROPERTY_MATRIX,
2170
+ entity: ENTITY,
2171
+ "access-pattern": ACCESS_PATTERN,
2172
+ maintained: MAINTAINED,
2173
+ cdc: CDC,
2174
+ cloudformation: CLOUDFORMATION
2175
+ };
2176
+
2177
+ // src/codegen/docs.ts
2178
+ var DOCS_INDEX_FILE = "index.md";
2179
+ function registerBuiltinHelpers(hb) {
2180
+ hb.registerHelper("eq", (a, b) => a === b);
2181
+ hb.registerHelper(
2182
+ "join",
2183
+ (list, sep) => Array.isArray(list) ? list.join(typeof sep === "string" ? sep : ", ") : ""
2184
+ );
2185
+ hb.registerHelper("pascal", (value) => {
2186
+ const s = String(value ?? "");
2187
+ return s.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2188
+ });
2189
+ hb.registerHelper("mermaidId", (value) => {
2190
+ const s = String(value ?? "");
2191
+ const id = s.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_");
2192
+ return /^[0-9]/.test(id) ? `n_${id}` : id || "n";
2193
+ });
2194
+ hb.registerHelper("codeOrEmpty", (value) => {
2195
+ const s = String(value ?? "");
2196
+ return s ? new hb.SafeString(`\`${s}\``) : "";
2197
+ });
2198
+ }
2199
+ function partialSource(name, templateDir) {
2200
+ if (templateDir) {
2201
+ const candidate = resolve(templateDir, `${name}.hbs`);
2202
+ if (existsSync(candidate)) {
2203
+ return readFileSync(candidate, "utf8");
2204
+ }
2205
+ }
2206
+ return BUNDLED_PARTIALS[name];
2207
+ }
2208
+ function buildEnvironment(options) {
2209
+ const hb = Handlebars.create();
2210
+ registerBuiltinHelpers(hb);
2211
+ if (options.helpers) {
2212
+ for (const [name, fn] of Object.entries(options.helpers)) {
2213
+ hb.registerHelper(name, fn);
2214
+ }
2215
+ }
2216
+ for (const name of PARTIAL_NAMES) {
2217
+ hb.registerPartial(
2218
+ name,
2219
+ hb.compile(partialSource(name, options.templateDir), { noEscape: true })
2220
+ );
2221
+ }
2222
+ return hb;
2223
+ }
2224
+ function generateDocs(docModel, options = {}) {
2225
+ const hb = buildEnvironment(options);
2226
+ const index = hb.partials["index"];
2227
+ const template = typeof index === "function" ? index : hb.compile(index, { noEscape: true });
2228
+ let out = template(docModel);
2229
+ out = out.replace(/([^\n])\n(---\n)/g, "$1\n\n$2").replace(/(\n---)\n([^\n])/g, "$1\n\n$2").replace(/([^\n])\n(#{1,6} )/g, "$1\n\n$2").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "") + "\n";
2230
+ return { [DOCS_INDEX_FILE]: out };
2231
+ }
2232
+
1453
2233
  // src/codegen/index.ts
1454
2234
  var OUTPUT_FILES = [
1455
2235
  "manifest.json",
@@ -1471,18 +2251,250 @@ function renderBundle(queries = {}, commands = {}, registry = MetadataRegistry,
1471
2251
  "manifest.json": JSON.stringify(bundle.manifest, null, 2) + "\n",
1472
2252
  "operations.json": JSON.stringify(bundle.operations, null, 2) + "\n",
1473
2253
  "types.py": generateTypes(bundle.manifest, queries, txSpecs, options),
1474
- "repositories.py": generateRepositories(queries, commands, txSpecs),
2254
+ "repositories.py": generateRepositories(
2255
+ queries,
2256
+ commands,
2257
+ txSpecs,
2258
+ bundle.manifest,
2259
+ bundle.operations.queries
2260
+ ),
1475
2261
  "__init__.py": generateInit(queries, commands, txSpecs)
1476
2262
  };
1477
2263
  }
1478
2264
 
2265
+ // src/codegen/jsdoc.ts
2266
+ async function loadTypescript() {
2267
+ try {
2268
+ const mod = await import("typescript");
2269
+ return mod.default ?? mod;
2270
+ } catch {
2271
+ return void 0;
2272
+ }
2273
+ }
2274
+ var warnedTypescriptMissing = false;
2275
+ var TYPESCRIPT_MISSING_WARNING = "warning: JSDoc description extraction skipped \u2014 'typescript' is not installed. Install typescript to enable it, or use explicit `description` fields.\n";
2276
+ function docCommentFor(ts, node) {
2277
+ const jsDoc = node.jsDoc;
2278
+ if (!jsDoc || jsDoc.length === 0) return void 0;
2279
+ const last = jsDoc[jsDoc.length - 1];
2280
+ const comment = last.comment;
2281
+ if (typeof comment === "string") {
2282
+ const t = comment.trim();
2283
+ return t === "" ? void 0 : t;
2284
+ }
2285
+ if (Array.isArray(comment)) {
2286
+ const joined = ts.getTextOfJSDocComment(comment);
2287
+ if (joined) {
2288
+ const t = joined.trim();
2289
+ return t === "" ? void 0 : t;
2290
+ }
2291
+ }
2292
+ return void 0;
2293
+ }
2294
+ function hasModelDecorator(ts, node) {
2295
+ const decorators = ts.getDecorators?.(node) ?? [];
2296
+ for (const dec of decorators) {
2297
+ const expr = dec.expression;
2298
+ const callee = ts.isCallExpression(expr) ? expr.expression : expr;
2299
+ if (ts.isIdentifier(callee) && callee.text === "model") return true;
2300
+ }
2301
+ return false;
2302
+ }
2303
+ function gsiIndexNameOf(ts, init) {
2304
+ if (!init || !ts.isCallExpression(init)) return void 0;
2305
+ const callee = init.expression;
2306
+ if (!ts.isIdentifier(callee) || callee.text !== "gsi") return void 0;
2307
+ const first = init.arguments[0];
2308
+ if (first && ts.isStringLiteralLike(first)) return first.text;
2309
+ return void 0;
2310
+ }
2311
+ function collectClassDoc(ts, node) {
2312
+ const members = /* @__PURE__ */ new Map();
2313
+ const gsis = /* @__PURE__ */ new Map();
2314
+ for (const member of node.members) {
2315
+ if (ts.isPropertyDeclaration(member) && member.name) {
2316
+ const doc = docCommentFor(ts, member);
2317
+ if (doc === void 0) continue;
2318
+ const gsiName = gsiIndexNameOf(ts, member.initializer);
2319
+ if (gsiName !== void 0) {
2320
+ gsis.set(gsiName, doc);
2321
+ } else if (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) {
2322
+ members.set(member.name.text, doc);
2323
+ }
2324
+ }
2325
+ }
2326
+ return { own: docCommentFor(ts, node), members, gsis };
2327
+ }
2328
+ function collectDefinitionDocs(ts, obj, into, ambiguous) {
2329
+ for (const prop of obj.properties) {
2330
+ if (!ts.isPropertyAssignment(prop)) continue;
2331
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name)) continue;
2332
+ const key = prop.name.text;
2333
+ if (into.has(key) || ambiguous.has(key)) {
2334
+ into.delete(key);
2335
+ ambiguous.add(key);
2336
+ continue;
2337
+ }
2338
+ const params = /* @__PURE__ */ new Map();
2339
+ collectParamDocs(ts, prop.initializer, params);
2340
+ into.set(key, { own: docCommentFor(ts, prop), params });
2341
+ }
2342
+ }
2343
+ function collectParamDocs(ts, node, into) {
2344
+ const visit = (n) => {
2345
+ if (ts.isPropertyAssignment(n)) {
2346
+ const isParamCall = ts.isCallExpression(n.initializer) && ts.isPropertyAccessExpression(n.initializer.expression) && ts.isIdentifier(n.initializer.expression.expression) && n.initializer.expression.expression.text === "param";
2347
+ if (isParamCall && (ts.isIdentifier(n.name) || ts.isStringLiteralLike(n.name))) {
2348
+ const doc = docCommentFor(ts, n);
2349
+ if (doc !== void 0) into.set(n.name.text, doc);
2350
+ }
2351
+ }
2352
+ ts.forEachChild(n, visit);
2353
+ };
2354
+ ts.forEachChild(node, visit);
2355
+ }
2356
+ function extract(ts, files) {
2357
+ const program = ts.createProgram(files, {
2358
+ // Parse-only, permissive options: we never emit and never type-check the
2359
+ // world — we only walk the AST of the requested files. `allowJs` lets a `.js`
2360
+ // definition module (pre-compiled input) be parsed too.
2361
+ allowJs: true,
2362
+ noResolve: true,
2363
+ noLib: true,
2364
+ skipLibCheck: true,
2365
+ types: [],
2366
+ // The definition modules use TC39 standard decorators (repo tsconfig has
2367
+ // `experimentalDecorators: false`); parsing is decorator-flavour agnostic for
2368
+ // AST purposes, but keep it aligned.
2369
+ experimentalDecorators: false
2370
+ });
2371
+ const classes = /* @__PURE__ */ new Map();
2372
+ const ambiguousClassNames = /* @__PURE__ */ new Set();
2373
+ const definitions = /* @__PURE__ */ new Map();
2374
+ const ambiguousDefinitionKeys = /* @__PURE__ */ new Set();
2375
+ const fileSet = new Set(files.map((f) => normalize(f)));
2376
+ for (const sf of program.getSourceFiles()) {
2377
+ if (!fileSet.has(normalize(sf.fileName))) continue;
2378
+ const visit = (node) => {
2379
+ if (ts.isClassDeclaration(node) && node.name && hasModelDecorator(ts, node)) {
2380
+ const name = node.name.text;
2381
+ if (classes.has(name) || ambiguousClassNames.has(name)) {
2382
+ classes.delete(name);
2383
+ ambiguousClassNames.add(name);
2384
+ } else {
2385
+ classes.set(name, collectClassDoc(ts, node));
2386
+ }
2387
+ }
2388
+ if (ts.isCallExpression(node)) {
2389
+ const callee = node.expression;
2390
+ if (ts.isIdentifier(callee) && (callee.text === "defineQueries" || callee.text === "defineCommands")) {
2391
+ const arg = node.arguments[0];
2392
+ if (arg && ts.isObjectLiteralExpression(arg)) {
2393
+ collectDefinitionDocs(ts, arg, definitions, ambiguousDefinitionKeys);
2394
+ }
2395
+ }
2396
+ }
2397
+ ts.forEachChild(node, visit);
2398
+ };
2399
+ visit(sf);
2400
+ }
2401
+ return {
2402
+ classes,
2403
+ ambiguousClassNames,
2404
+ definitions,
2405
+ ambiguousDefinitionKeys
2406
+ };
2407
+ }
2408
+ function normalize(p) {
2409
+ return p.replace(/\\/g, "/");
2410
+ }
2411
+ function mergeIntoRegistry(extracted, registry) {
2412
+ const countByName = /* @__PURE__ */ new Map();
2413
+ for (const [cls] of registry.getAll()) {
2414
+ countByName.set(cls.name, (countByName.get(cls.name) ?? 0) + 1);
2415
+ }
2416
+ for (const [cls, meta] of registry.getAll()) {
2417
+ const name = cls.name;
2418
+ if (extracted.ambiguousClassNames.has(name)) continue;
2419
+ if ((countByName.get(name) ?? 0) !== 1) continue;
2420
+ const classDoc = extracted.classes.get(name);
2421
+ if (!classDoc) continue;
2422
+ applyClassDoc(classDoc, meta);
2423
+ }
2424
+ }
2425
+ function applyClassDoc(classDoc, meta) {
2426
+ if (meta.description === void 0 && classDoc.own !== void 0) {
2427
+ meta.description = classDoc.own;
2428
+ }
2429
+ for (const field of meta.fields) {
2430
+ const doc = classDoc.members.get(field.propertyName);
2431
+ if (doc === void 0) continue;
2432
+ if (field.options?.description !== void 0) continue;
2433
+ field.options = { ...field.options ?? {}, description: doc };
2434
+ }
2435
+ for (const rel of meta.relations) {
2436
+ const doc = classDoc.members.get(rel.propertyName);
2437
+ if (doc === void 0) continue;
2438
+ if (rel.options?.description !== void 0) continue;
2439
+ rel.options = { ...rel.options ?? {}, description: doc };
2440
+ }
2441
+ for (const gsi of meta.gsiDefinitions) {
2442
+ const doc = classDoc.gsis.get(gsi.indexName);
2443
+ if (doc === void 0) continue;
2444
+ if (gsi.description !== void 0) continue;
2445
+ gsi.description = doc;
2446
+ }
2447
+ }
2448
+ function mergeIntoDefinitions(extracted, map) {
2449
+ for (const [key, def] of Object.entries(map)) {
2450
+ if (extracted.ambiguousDefinitionKeys.has(key)) continue;
2451
+ const defDoc = extracted.definitions.get(key);
2452
+ if (!defDoc) continue;
2453
+ if (def.description === void 0 && defDoc.own !== void 0) {
2454
+ def.description = defDoc.own;
2455
+ }
2456
+ for (const [paramName, descriptor] of Object.entries(def.params)) {
2457
+ const doc = defDoc.params.get(paramName);
2458
+ if (doc === void 0) continue;
2459
+ if (descriptor.description !== void 0) continue;
2460
+ descriptor.description = doc;
2461
+ }
2462
+ }
2463
+ }
2464
+ async function applyJsDocDescriptions(sources, queries, commands, registry = MetadataRegistry, opts = {}) {
2465
+ const ts = await (opts.loadTs ?? loadTypescript)();
2466
+ if (!ts) {
2467
+ if (!warnedTypescriptMissing) {
2468
+ warnedTypescriptMissing = true;
2469
+ (opts.warn ?? ((m) => void process.stderr.write(m)))(
2470
+ TYPESCRIPT_MISSING_WARNING
2471
+ );
2472
+ }
2473
+ return false;
2474
+ }
2475
+ try {
2476
+ const files = [
2477
+ sources.entry,
2478
+ ...sources.queries ? [sources.queries] : [],
2479
+ ...sources.commands ? [sources.commands] : []
2480
+ ].filter((f, i, a) => a.indexOf(f) === i);
2481
+ const extracted = extract(ts, files);
2482
+ mergeIntoRegistry(extracted, registry);
2483
+ mergeIntoDefinitions(extracted, queries);
2484
+ mergeIntoDefinitions(extracted, commands);
2485
+ return true;
2486
+ } catch {
2487
+ return false;
2488
+ }
2489
+ }
2490
+
1479
2491
  // src/cli/handlers.ts
1480
2492
  function fail(code, message, exitCode) {
1481
2493
  process.stderr.write(JSON.stringify({ code, message }) + "\n");
1482
2494
  process.exit(exitCode);
1483
2495
  }
1484
2496
  function toUrl(file) {
1485
- const abs = isAbsolute(file) ? file : resolve(process.cwd(), file);
2497
+ const abs = isAbsolute(file) ? file : resolve2(process.cwd(), file);
1486
2498
  return pathToFileURL(abs).href;
1487
2499
  }
1488
2500
  var tsxRegistered = false;
@@ -1504,6 +2516,12 @@ async function importModule(file) {
1504
2516
  if (/\.[mc]?ts$/.test(file)) await ensureTsx();
1505
2517
  return await import(toUrl(file));
1506
2518
  }
2519
+ async function importModelEntry(entry) {
2520
+ await importModule(entry);
2521
+ if (MetadataRegistry.linter === null) {
2522
+ MetadataRegistry.linter = createDefaultLinter();
2523
+ }
2524
+ }
1507
2525
  async function loadDefinitionMap(file, exportName) {
1508
2526
  const mod = await importModule(file);
1509
2527
  const value = mod[exportName];
@@ -1537,7 +2555,7 @@ var handlers = {
1537
2555
  if (!entry) fail("INVALID_ARGS", "Missing required option --entry.", 2);
1538
2556
  if (!out) fail("INVALID_ARGS", "Missing required option --out.", 2);
1539
2557
  try {
1540
- await importModule(entry);
2558
+ await importModelEntry(entry);
1541
2559
  const queries = await loadDefinitionMap(queriesPath ?? entry, "queries");
1542
2560
  const commands = await loadDefinitionMap(commandsPath ?? entry, "commands");
1543
2561
  const transactions = await loadOptionalMap(
@@ -1552,24 +2570,34 @@ var handlers = {
1552
2570
  contextsPath ?? contractsPath ?? entry,
1553
2571
  "contexts"
1554
2572
  );
2573
+ const abs = (p) => isAbsolute(p) ? p : resolve2(process.cwd(), p);
2574
+ await applyJsDocDescriptions(
2575
+ {
2576
+ entry: abs(entry),
2577
+ queries: abs(queriesPath ?? entry),
2578
+ commands: abs(commandsPath ?? entry)
2579
+ },
2580
+ queries,
2581
+ commands
2582
+ );
1555
2583
  const files = renderBundle(queries, commands, void 0, transactions, {
1556
2584
  dataclass: Boolean(dataclass),
1557
2585
  contracts: { contracts, contexts }
1558
2586
  });
1559
2587
  void dataclass;
1560
- const outDir = isAbsolute(out) ? out : resolve(process.cwd(), out);
2588
+ const outDir = isAbsolute(out) ? out : resolve2(process.cwd(), out);
1561
2589
  mkdirSync(outDir, { recursive: true });
1562
2590
  for (const name of OUTPUT_FILES) {
1563
- writeFileSync(resolve(outDir, name), files[name], "utf8");
2591
+ writeFileSync(resolve2(outDir, name), files[name], "utf8");
1564
2592
  }
1565
2593
  const emittedFiles = [...OUTPUT_FILES];
1566
2594
  if (cdcProjectedModelNames().length > 0) {
1567
- const entryAbs = isAbsolute(entry) ? entry : resolve(process.cwd(), entry);
2595
+ const entryAbs = isAbsolute(entry) ? entry : resolve2(process.cwd(), entry);
1568
2596
  let modelsModule = relative(outDir, entryAbs).replace(/\\/g, "/").replace(/\.[mc]?ts$/, "");
1569
2597
  if (!modelsModule.startsWith(".")) modelsModule = `./${modelsModule}`;
1570
2598
  const cdcRegistryFile = "cdc-registry.ts";
1571
2599
  writeFileSync(
1572
- resolve(outDir, cdcRegistryFile),
2600
+ resolve2(outDir, cdcRegistryFile),
1573
2601
  generateCdcRegistry({ modelsModule }),
1574
2602
  "utf8"
1575
2603
  );
@@ -1723,7 +2751,7 @@ var handlers = {
1723
2751
  tblClass = tableClass;
1724
2752
  }
1725
2753
  try {
1726
- await importModule(entry);
2754
+ await importModelEntry(entry);
1727
2755
  const manifest = buildManifest();
1728
2756
  const streamMaintenanceDetected = streamMode === "auto" ? detectStreamMaintenance() : false;
1729
2757
  const template = emitCloudformation(manifest, {
@@ -1746,7 +2774,7 @@ var handlers = {
1746
2774
  autoscaleTarget: autoscaleTargetNum
1747
2775
  });
1748
2776
  if (out) {
1749
- const outFile = isAbsolute(out) ? out : resolve(process.cwd(), out);
2777
+ const outFile = isAbsolute(out) ? out : resolve2(process.cwd(), out);
1750
2778
  mkdirSync(dirname(outFile), { recursive: true });
1751
2779
  writeFileSync(outFile, template, "utf8");
1752
2780
  process.stdout.write(
@@ -1763,6 +2791,131 @@ var handlers = {
1763
2791
  const message = err instanceof Error ? err.message : String(err);
1764
2792
  fail("GENERATION_FAILED", message, 1);
1765
2793
  }
2794
+ },
2795
+ /**
2796
+ * `generate docs` (issue #183).
2797
+ *
2798
+ * Loads the entry model module (side-effecting import registers the entities)
2799
+ * plus the optional query / command definitions, builds the serializable
2800
+ * manifest + operations, then the normalized {@link buildDocModel} view model,
2801
+ * and renders the model-spec Markdown (Mermaid fences) via the Handlebars
2802
+ * generator to a single `index.md` in `--out`. `--template-dir` overrides
2803
+ * bundled partials per-name and `--helpers` registers extra Handlebars helpers.
2804
+ * Mirrors `generatePython`'s tsx-based entry loading and JSON error / exit shape.
2805
+ *
2806
+ * The Table Overview billing / streams are resolved from the CFn-shared flags
2807
+ * (`--billing-mode` / `--stream`, `stream auto` detecting `updateMode:'stream'`
2808
+ * maintenance like the CFn generator); deploy-time-only items with no flag
2809
+ * render "as configured at deployment" in the DocModel.
2810
+ */
2811
+ async generateDocs(options) {
2812
+ const {
2813
+ entry,
2814
+ queries: queriesPath,
2815
+ out,
2816
+ format,
2817
+ split,
2818
+ templateDir,
2819
+ helpers: helpersPath,
2820
+ billingMode,
2821
+ stream
2822
+ } = options;
2823
+ if (!entry) fail("INVALID_ARGS", "Missing required option --entry.", 2);
2824
+ if (!out) fail("INVALID_ARGS", "Missing required option --out.", 2);
2825
+ if (format !== void 0 && format !== "markdown") {
2826
+ fail("INVALID_ARGS", `Invalid --format '${format}'. Use markdown.`, 2);
2827
+ }
2828
+ if (split !== void 0 && split !== "none" && split !== "entity") {
2829
+ fail("INVALID_ARGS", `Invalid --split '${split}'. Use none or entity.`, 2);
2830
+ }
2831
+ const outIsFile = out.toLowerCase().endsWith(".md");
2832
+ if (split === "entity" && outIsFile) {
2833
+ fail(
2834
+ "INVALID_ARGS",
2835
+ `--split entity writes multiple files and needs a directory --out, not a .md file ('${out}').`,
2836
+ 2
2837
+ );
2838
+ }
2839
+ if (split === "entity") {
2840
+ fail(
2841
+ "INVALID_ARGS",
2842
+ `--split entity is not yet supported. Use --split none (a single index.md).`,
2843
+ 2
2844
+ );
2845
+ }
2846
+ let billing = "PAY_PER_REQUEST";
2847
+ if (billingMode !== void 0) {
2848
+ if (billingMode !== "PAY_PER_REQUEST" && billingMode !== "PROVISIONED") {
2849
+ fail(
2850
+ "INVALID_ARGS",
2851
+ `Invalid --billing-mode '${billingMode}'. Use PAY_PER_REQUEST or PROVISIONED.`,
2852
+ 2
2853
+ );
2854
+ }
2855
+ billing = billingMode;
2856
+ }
2857
+ let streamMode = "auto";
2858
+ if (stream !== void 0) {
2859
+ if (stream !== "auto" && stream !== "on" && stream !== "off") {
2860
+ fail("INVALID_ARGS", `Invalid --stream '${stream}'. Use auto, on, or off.`, 2);
2861
+ }
2862
+ streamMode = stream;
2863
+ }
2864
+ try {
2865
+ await importModelEntry(entry);
2866
+ const queries = await loadOptionalMap(queriesPath ?? entry, "queries");
2867
+ const commands = await loadOptionalMap(queriesPath ?? entry, "commands");
2868
+ const manifest = buildManifest();
2869
+ const operations = buildOperations(
2870
+ queries,
2871
+ commands
2872
+ );
2873
+ const streamsEnabled = streamMode === "on" ? true : streamMode === "off" ? false : detectStreamMaintenance();
2874
+ const docModel = buildDocModel(manifest, operations, MetadataRegistry, {
2875
+ billingMode: billing,
2876
+ streamsEnabled
2877
+ });
2878
+ let helpers;
2879
+ if (helpersPath) {
2880
+ const mod = await importModule(helpersPath);
2881
+ const exported = mod.helpers ?? mod.default;
2882
+ if (!exported || typeof exported !== "object") {
2883
+ fail(
2884
+ "INVALID_ARGS",
2885
+ `Module '${helpersPath}' must export a 'helpers' (or default) object mapping helper names to functions.`,
2886
+ 2
2887
+ );
2888
+ }
2889
+ helpers = exported;
2890
+ }
2891
+ const files = generateDocs(docModel, {
2892
+ templateDir: templateDir ? isAbsolute(templateDir) ? templateDir : resolve2(process.cwd(), templateDir) : void 0,
2893
+ helpers
2894
+ });
2895
+ const outAbs = isAbsolute(out) ? out : resolve2(process.cwd(), out);
2896
+ const emitted = [];
2897
+ if (outIsFile) {
2898
+ const [contents] = Object.values(files);
2899
+ mkdirSync(dirname(outAbs), { recursive: true });
2900
+ writeFileSync(outAbs, contents, "utf8");
2901
+ emitted.push(out);
2902
+ process.stdout.write(
2903
+ JSON.stringify({ status: "ok", outDir: dirname(out), files: emitted }) + "\n"
2904
+ );
2905
+ } else {
2906
+ mkdirSync(outAbs, { recursive: true });
2907
+ for (const [name, contents] of Object.entries(files)) {
2908
+ writeFileSync(resolve2(outAbs, name), contents, "utf8");
2909
+ emitted.push(name);
2910
+ }
2911
+ process.stdout.write(
2912
+ JSON.stringify({ status: "ok", outDir: out, files: emitted }) + "\n"
2913
+ );
2914
+ }
2915
+ } catch (err) {
2916
+ const message = err instanceof Error ? err.message : String(err);
2917
+ fail("GENERATION_FAILED", message, 1);
2918
+ }
1766
2919
  }
1767
2920
  };
1768
2921