diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65a3ca4b..6efcf6f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,19 +124,23 @@ jobs: node-version: '22' cache: 'npm' - run: npm ci - - name: 'Gate 5a: v19 capability contract drift' - run: npm run check:capabilities - - name: 'Gate 5b: Install pinned Wesley generator' + - name: 'Gate 5a: Install pinned Wesley generator' run: >- cargo install --git https://github.com/flyingrobots/wesley.git --rev 4891a631f888c5b2f70e117e3704538dd1362c2f --locked wesley-cli - - name: 'Gate 5c: Generated SDK source and reference drift' + - name: 'Gate 5b: Wesley vocabulary IR drift' + run: npm run check:vocabulary-ir + - name: 'Gate 5c: v19 capability artifact drift' + run: npm run check:capabilities + - name: 'Gate 5d: Generated SDK source and reference drift' run: npm run check:sdk-fixture - - name: 'Gate 5d: Packed generated SDK with disposable real Git' + - name: 'Gate 5e: Packed generated SDK with disposable real Git' run: npm run test:sdk-fixture + - name: 'Gate 5f: All twelve v19 acceptance gates' + run: npm run test:v19-acceptance type-firewall-docs: runs-on: ubuntu-latest diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c3c45347..70b50f7b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -109,25 +109,24 @@ Full standard: [Systems-Style TypeScript](docs/SYSTEMS_STYLE_TYPESCRIPT.md). ## Public API surface -### `openWarp()` +### `Runtime` -The package root accepts an opaque `WarpStorage` and returns a frozen `Warp`: +The package root exports exactly one runtime value. `Runtime.open()` owns +production storage composition: ```typescript -import { openWarp } from '@git-stunts/git-warp'; -import { GitStorage } from '@git-stunts/git-warp/storage'; +import { Runtime } from '@git-stunts/git-warp'; -const storage = await GitStorage.open({ cwd: '.' }); -const warp = await openWarp({ storage, writer: 'agent-1' }); -const team = await warp.timeline('team'); +const runtime = await Runtime.open({ at: '.', writer: 'agent-1' }); +const team = await runtime.lane('team'); // After the final lane operation: -await storage.close(); +await runtime.close(); ``` -Application code writes with `timeline.write(intent)` and reads with -`timeline.read(reading)`. Formal coordinate reads and receipt inspection live -on the explicit `advanced` and `diagnostics` subpaths. +Application code writes validated Intents with `lane.write(intent)` and runs +Observers with `lane.observe(observer)`. Formal coordinate reads and Receipt +inspection live on the explicit `/advanced` and `/diagnostics` subpaths. ### Storage composition diff --git a/bin/cli/capabilities/V19CapabilityContract.generated.ts b/bin/cli/capabilities/V19CapabilityContract.generated.ts index 35bfea10..40eaa4d5 100644 --- a/bin/cli/capabilities/V19CapabilityContract.generated.ts +++ b/bin/cli/capabilities/V19CapabilityContract.generated.ts @@ -1,296 +1,14 @@ -/* @generated from v19-capabilities.json. Do not edit by hand. */ +/* @generated from schemas/v19-public-vocabulary.graphql by Wesley. Do not edit. */ + +import { V19_MCP_CAPABILITIES } from './V19McpCapabilities.generated.ts'; +import { + V19_PUBLIC_NOUNS, + V19_PUBLIC_VOCABULARY, +} from './V19PublicVocabulary.generated.ts'; + +export { V19_PUBLIC_NOUNS }; export const V19_CAPABILITY_CONTRACT = { - "version": "git-warp.capabilities/v19", - "cli": [ - { - "command": "write", - "summary": "Write one Intent to a Lane.", - "usage": "git warp write --lane [--strand ] --intent " - }, - { - "command": "observe", - "summary": "Run one bounded Observer over a Lane.", - "usage": "git warp observe --lane [--strand ] --observer --reading " - }, - { - "command": "fork", - "summary": "Fork a Lane into a named strand Lane.", - "usage": "git warp fork --lane --name " - }, - { - "command": "settle", - "summary": "Preview or apply an immutable Settlement plan.", - "usage": "git warp settle preview --source --strand --target | settle apply --plan " - }, - { - "command": "receipt", - "summary": "Render a canonical Receipt envelope.", - "usage": "git warp receipt show --input " - }, - { - "command": "doctor", - "summary": "Diagnose the local Runtime and Lane.", - "usage": "git warp doctor --lane " - }, - { - "command": "repair", - "summary": "Apply one explicit Runtime repair.", - "usage": "git warp repair --lane --action materialization" - }, - { - "command": "audit", - "summary": "Verify the local Runtime audit trail.", - "usage": "git warp audit --lane [--writer ]" - }, - { - "command": "mcp", - "summary": "Serve the v19 capabilities over MCP stdio.", - "usage": "git warp mcp --repo --writer " - } - ], - "mcp": [ - { - "name": "warp_lane_describe", - "description": "Describe one Runtime Lane.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - }, - "strand": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "lane" - ], - "additionalProperties": false - } - }, - { - "name": "warp_intent_write", - "description": "Write one validated Intent to a Lane and return its canonical Receipt.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - }, - "strand": { - "type": "string", - "minLength": 1 - }, - "intent": { - "type": "object" - } - }, - "required": [ - "lane", - "intent" - ], - "additionalProperties": false - } - }, - { - "name": "warp_observation_start", - "description": "Start one bounded Observation and retain its Readings for transport.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - }, - "strand": { - "type": "string", - "minLength": 1 - }, - "observerId": { - "type": "string", - "minLength": 1 - }, - "reading": { - "type": "object" - } - }, - "required": [ - "lane", - "observerId", - "reading" - ], - "additionalProperties": false - } - }, - { - "name": "warp_observation_read", - "description": "Read the next bounded transport batch from an Observation.", - "inputSchema": { - "type": "object", - "properties": { - "observationId": { - "type": "string", - "minLength": 1 - }, - "cursor": { - "type": "string", - "minLength": 1 - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 256 - } - }, - "required": [ - "observationId" - ], - "additionalProperties": false - } - }, - { - "name": "warp_observation_cancel", - "description": "Cancel and discard one retained Observation transport.", - "inputSchema": { - "type": "object", - "properties": { - "observationId": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "observationId" - ], - "additionalProperties": false - } - }, - { - "name": "warp_receipt_get", - "description": "Return one canonical Receipt retained by this MCP server.", - "inputSchema": { - "type": "object", - "properties": { - "receiptRef": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "receiptRef" - ], - "additionalProperties": false - } - }, - { - "name": "warp_settlement_preview", - "description": "Preview an immutable Settlement plan between two Lanes.", - "inputSchema": { - "type": "object", - "properties": { - "sourceLane": { - "type": "string", - "minLength": 1 - }, - "sourceStrand": { - "type": "string", - "minLength": 1 - }, - "targetLane": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "sourceLane", - "sourceStrand", - "targetLane" - ], - "additionalProperties": false - } - }, - { - "name": "warp_settlement_apply", - "description": "Revalidate and apply one retained immutable Settlement plan.", - "inputSchema": { - "type": "object", - "properties": { - "planRef": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "planRef" - ], - "additionalProperties": false - } - }, - { - "name": "warp_doctor", - "description": "Diagnose one local Runtime Lane.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "lane" - ], - "additionalProperties": false - } - }, - { - "name": "warp_repair", - "description": "Apply one explicit local Runtime repair.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - }, - "action": { - "type": "string", - "enum": [ - "materialization" - ] - } - }, - "required": [ - "lane", - "action" - ], - "additionalProperties": false - } - }, - { - "name": "warp_audit", - "description": "Verify the local Runtime audit trail.", - "inputSchema": { - "type": "object", - "properties": { - "lane": { - "type": "string", - "minLength": 1 - }, - "writer": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "lane" - ], - "additionalProperties": false - } - } - ] + ...V19_PUBLIC_VOCABULARY, + mcp: V19_MCP_CAPABILITIES, } as const; diff --git a/bin/cli/capabilities/V19McpCapabilities.generated.ts b/bin/cli/capabilities/V19McpCapabilities.generated.ts new file mode 100644 index 00000000..9034d4a2 --- /dev/null +++ b/bin/cli/capabilities/V19McpCapabilities.generated.ts @@ -0,0 +1,246 @@ +/* @generated from schemas/v19-public-vocabulary.graphql by Wesley. Do not edit. */ + +export const V19_MCP_CAPABILITIES = [ + { + "name": "warp_lane_describe", + "description": "Describe one Runtime Lane.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "lane" + ], + "additionalProperties": false + } + }, + { + "name": "warp_intent_write", + "description": "Write one validated Intent to a Lane and return its canonical Receipt.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + }, + "intent": { + "type": "object" + } + }, + "required": [ + "lane", + "intent" + ], + "additionalProperties": false + } + }, + { + "name": "warp_observation_start", + "description": "Start one bounded Observation and retain its Readings for transport.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + }, + "observerId": { + "type": "string", + "minLength": 1 + }, + "reading": { + "type": "object" + } + }, + "required": [ + "lane", + "observerId", + "reading" + ], + "additionalProperties": false + } + }, + { + "name": "warp_observation_read", + "description": "Read the next bounded transport batch from an Observation.", + "inputSchema": { + "type": "object", + "properties": { + "observationId": { + "type": "string", + "minLength": 1 + }, + "cursor": { + "type": "string", + "minLength": 1 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 256 + } + }, + "required": [ + "observationId" + ], + "additionalProperties": false + } + }, + { + "name": "warp_observation_cancel", + "description": "Cancel and discard one retained Observation transport.", + "inputSchema": { + "type": "object", + "properties": { + "observationId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "observationId" + ], + "additionalProperties": false + } + }, + { + "name": "warp_receipt_get", + "description": "Return one canonical Receipt retained by this MCP server.", + "inputSchema": { + "type": "object", + "properties": { + "receiptRef": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "receiptRef" + ], + "additionalProperties": false + } + }, + { + "name": "warp_settlement_preview", + "description": "Preview an immutable Settlement plan between two Lanes.", + "inputSchema": { + "type": "object", + "properties": { + "sourceLane": { + "type": "string", + "minLength": 1 + }, + "sourceStrand": { + "type": "string", + "minLength": 1 + }, + "targetLane": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "sourceLane", + "sourceStrand", + "targetLane" + ], + "additionalProperties": false + } + }, + { + "name": "warp_settlement_apply", + "description": "Revalidate and apply one retained immutable Settlement plan.", + "inputSchema": { + "type": "object", + "properties": { + "planRef": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "planRef" + ], + "additionalProperties": false + } + }, + { + "name": "warp_doctor", + "description": "Diagnose one local Runtime Lane.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "lane" + ], + "additionalProperties": false + } + }, + { + "name": "warp_repair", + "description": "Apply one explicit local Runtime repair.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + }, + "action": { + "type": "string", + "enum": [ + "materialization" + ] + } + }, + "required": [ + "lane", + "action" + ], + "additionalProperties": false + } + }, + { + "name": "warp_audit", + "description": "Verify the local Runtime audit trail.", + "inputSchema": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "minLength": 1 + }, + "writer": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "lane" + ], + "additionalProperties": false + } + } +] as const; diff --git a/bin/cli/capabilities/V19PublicVocabulary.generated.ts b/bin/cli/capabilities/V19PublicVocabulary.generated.ts new file mode 100644 index 00000000..9ccc8cab --- /dev/null +++ b/bin/cli/capabilities/V19PublicVocabulary.generated.ts @@ -0,0 +1,219 @@ +/* @generated from schemas/v19-public-vocabulary.graphql by Wesley. Do not edit. */ + +export const V19_PUBLIC_VOCABULARY = { + "version": "git-warp.capabilities/v19", + "moduleSummary": "Write intents. Observe lanes. Keep receipts.", + "sdkSummary": "Generated SDKs construct validated Intent and Observer values for Runtime Lane workflows.", + "formalIdentifiers": [ + "WitnessReference", + "WitnessReferences", + "witnessRef", + "witnessRefs" + ], + "exceptionPaths": [ + "docs/migrations/", + "docs/topics/api/README.md", + "docs/topics/git-perf.md", + "docs/topics/git-substrate.md", + "diagnostics.ts", + "scripts/v18-to-v19/", + "src/infrastructure/" + ], + "nouns": [ + { + "name": "Runtime", + "summary": "Opens local resources and owns Lane lifecycles." + }, + { + "name": "Lane", + "summary": "Names one causal application history." + }, + { + "name": "Intent", + "summary": "Describes one validated application write." + }, + { + "name": "Observer", + "summary": "Defines one bounded read over a Lane." + }, + { + "name": "Observation", + "summary": "Executes an Observer once and streams Readings." + }, + { + "name": "Reading", + "summary": "Carries a canonical value with bounded support." + }, + { + "name": "Receipt", + "summary": "Records the durable outcome of an operation." + }, + { + "name": "Settlement", + "summary": "Revalidates and applies an immutable plan between Lanes." + } + ], + "forbiddenTerms": [ + { + "phrase": "blob", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "cas", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "commit", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "git", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "object id", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "oid", + "scopes": [ + "ROOT_DECLARATION", + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "plumbing", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "ref", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "sha", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "tree", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "timeline", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "merge", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "graph store", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "generic event", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "dry run", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "session", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "query result page", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + ], + "cli": [ + { + "command": "write", + "summary": "Write one Intent to a Lane.", + "usage": "git warp write --lane [--strand ] --intent " + }, + { + "command": "observe", + "summary": "Run one bounded Observer over a Lane.", + "usage": "git warp observe --lane [--strand ] --observer --reading " + }, + { + "command": "fork", + "summary": "Fork a Lane into a named strand Lane.", + "usage": "git warp fork --lane --name " + }, + { + "command": "settle", + "summary": "Preview or apply an immutable Settlement plan.", + "usage": "git warp settle preview --source --strand --target | settle apply --plan " + }, + { + "command": "receipt", + "summary": "Render a canonical Receipt envelope.", + "usage": "git warp receipt show --input " + }, + { + "command": "doctor", + "summary": "Diagnose the local Runtime and Lane.", + "usage": "git warp doctor --lane " + }, + { + "command": "repair", + "summary": "Apply one explicit Runtime repair.", + "usage": "git warp repair --lane --action materialization" + }, + { + "command": "audit", + "summary": "Verify the local Runtime audit trail.", + "usage": "git warp audit --lane " + }, + { + "command": "mcp", + "summary": "Serve the v19 capabilities over MCP stdio.", + "usage": "git warp mcp --repo --writer " + } + ] +} as const; + +export const V19_PUBLIC_NOUNS = { + "Runtime": "Runtime", + "Lane": "Lane", + "Intent": "Intent", + "Observer": "Observer", + "Observation": "Observation", + "Reading": "Reading", + "Receipt": "Receipt", + "Settlement": "Settlement" +} as const; diff --git a/bin/cli/capabilities/v19-capabilities.json b/bin/cli/capabilities/v19-capabilities.json index 104f1071..ca41cd30 100644 --- a/bin/cli/capabilities/v19-capabilities.json +++ b/bin/cli/capabilities/v19-capabilities.json @@ -1,5 +1,161 @@ { "version": "git-warp.capabilities/v19", + "moduleSummary": "Write intents. Observe lanes. Keep receipts.", + "sdkSummary": "Generated SDKs construct validated Intent and Observer values for Runtime Lane workflows.", + "formalIdentifiers": [ + "WitnessReference", + "WitnessReferences", + "witnessRef", + "witnessRefs" + ], + "exceptionPaths": [ + "docs/migrations/", + "docs/topics/api/README.md", + "docs/topics/git-perf.md", + "docs/topics/git-substrate.md", + "diagnostics.ts", + "scripts/v18-to-v19/", + "src/infrastructure/" + ], + "nouns": [ + { + "name": "Runtime", + "summary": "Opens local resources and owns Lane lifecycles." + }, + { + "name": "Lane", + "summary": "Names one causal application history." + }, + { + "name": "Intent", + "summary": "Describes one validated application write." + }, + { + "name": "Observer", + "summary": "Defines one bounded read over a Lane." + }, + { + "name": "Observation", + "summary": "Executes an Observer once and streams Readings." + }, + { + "name": "Reading", + "summary": "Carries a canonical value with bounded support." + }, + { + "name": "Receipt", + "summary": "Records the durable outcome of an operation." + }, + { + "name": "Settlement", + "summary": "Revalidates and applies an immutable plan between Lanes." + } + ], + "forbiddenTerms": [ + { + "phrase": "blob", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "cas", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "commit", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "git", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "object id", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "oid", + "scopes": [ + "ROOT_DECLARATION", + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "plumbing", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "ref", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "sha", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "tree", + "scopes": [ + "ROOT_DECLARATION" + ] + }, + { + "phrase": "timeline", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "merge", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "graph store", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "generic event", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "dry run", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "session", + "scopes": [ + "PUBLIC_SURFACE" + ] + }, + { + "phrase": "query result page", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + ], "cli": [ { "command": "write", @@ -39,7 +195,7 @@ { "command": "audit", "summary": "Verify the local Runtime audit trail.", - "usage": "git warp audit --lane [--writer ]" + "usage": "git warp audit --lane " }, { "command": "mcp", @@ -54,10 +210,18 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 }, - "strand": { "type": "string", "minLength": 1 } + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + } }, - "required": ["lane"], + "required": [ + "lane" + ], "additionalProperties": false } }, @@ -67,11 +231,22 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 }, - "strand": { "type": "string", "minLength": 1 }, - "intent": { "type": "object" } + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + }, + "intent": { + "type": "object" + } }, - "required": ["lane", "intent"], + "required": [ + "lane", + "intent" + ], "additionalProperties": false } }, @@ -81,12 +256,27 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 }, - "strand": { "type": "string", "minLength": 1 }, - "observerId": { "type": "string", "minLength": 1 }, - "reading": { "type": "object" } + "lane": { + "type": "string", + "minLength": 1 + }, + "strand": { + "type": "string", + "minLength": 1 + }, + "observerId": { + "type": "string", + "minLength": 1 + }, + "reading": { + "type": "object" + } }, - "required": ["lane", "observerId", "reading"], + "required": [ + "lane", + "observerId", + "reading" + ], "additionalProperties": false } }, @@ -96,11 +286,23 @@ "inputSchema": { "type": "object", "properties": { - "observationId": { "type": "string", "minLength": 1 }, - "cursor": { "type": "string", "minLength": 1 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 256 } + "observationId": { + "type": "string", + "minLength": 1 + }, + "cursor": { + "type": "string", + "minLength": 1 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 256 + } }, - "required": ["observationId"], + "required": [ + "observationId" + ], "additionalProperties": false } }, @@ -110,9 +312,14 @@ "inputSchema": { "type": "object", "properties": { - "observationId": { "type": "string", "minLength": 1 } + "observationId": { + "type": "string", + "minLength": 1 + } }, - "required": ["observationId"], + "required": [ + "observationId" + ], "additionalProperties": false } }, @@ -122,9 +329,14 @@ "inputSchema": { "type": "object", "properties": { - "receiptRef": { "type": "string", "minLength": 1 } + "receiptRef": { + "type": "string", + "minLength": 1 + } }, - "required": ["receiptRef"], + "required": [ + "receiptRef" + ], "additionalProperties": false } }, @@ -134,11 +346,24 @@ "inputSchema": { "type": "object", "properties": { - "sourceLane": { "type": "string", "minLength": 1 }, - "sourceStrand": { "type": "string", "minLength": 1 }, - "targetLane": { "type": "string", "minLength": 1 } + "sourceLane": { + "type": "string", + "minLength": 1 + }, + "sourceStrand": { + "type": "string", + "minLength": 1 + }, + "targetLane": { + "type": "string", + "minLength": 1 + } }, - "required": ["sourceLane", "sourceStrand", "targetLane"], + "required": [ + "sourceLane", + "sourceStrand", + "targetLane" + ], "additionalProperties": false } }, @@ -148,9 +373,14 @@ "inputSchema": { "type": "object", "properties": { - "planRef": { "type": "string", "minLength": 1 } + "planRef": { + "type": "string", + "minLength": 1 + } }, - "required": ["planRef"], + "required": [ + "planRef" + ], "additionalProperties": false } }, @@ -160,9 +390,14 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 } + "lane": { + "type": "string", + "minLength": 1 + } }, - "required": ["lane"], + "required": [ + "lane" + ], "additionalProperties": false } }, @@ -172,10 +407,21 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 }, - "action": { "type": "string", "enum": ["materialization"] } + "lane": { + "type": "string", + "minLength": 1 + }, + "action": { + "type": "string", + "enum": [ + "materialization" + ] + } }, - "required": ["lane", "action"], + "required": [ + "lane", + "action" + ], "additionalProperties": false } }, @@ -185,10 +431,18 @@ "inputSchema": { "type": "object", "properties": { - "lane": { "type": "string", "minLength": 1 }, - "writer": { "type": "string", "minLength": 1 } + "lane": { + "type": "string", + "minLength": 1 + }, + "writer": { + "type": "string", + "minLength": 1 + } }, - "required": ["lane"], + "required": [ + "lane" + ], "additionalProperties": false } } diff --git a/bin/cli/shared.ts b/bin/cli/shared.ts index 2327761a..16db3cdc 100644 --- a/bin/cli/shared.ts +++ b/bin/cli/shared.ts @@ -6,8 +6,8 @@ import WebCryptoAdapter from '../../src/infrastructure/adapters/WebCryptoAdapter import { openRuntimeHostProduct } from '../../src/domain/warp/RuntimeHostProduct.ts'; import { REF_PREFIX } from '../../src/domain/utils/RefLayout.ts'; import { HookInstaller, type FsAdapter } from '../../src/domain/services/HookInstaller.ts'; +import GitStorage from '../../src/application/GitStorage.ts'; import { usageError, notFoundError } from './infrastructure.ts'; -import { GitStorage } from '../../storage.ts'; import { resolveWarpStorage, type WarpStorageBinding, diff --git a/bin/cli/v19/V19DomainInput.ts b/bin/cli/v19/V19DomainInput.ts index 45cb9e63..61dc905b 100644 --- a/bin/cli/v19/V19DomainInput.ts +++ b/bin/cli/v19/V19DomainInput.ts @@ -5,6 +5,9 @@ import type Intent from '../../../src/domain/api/Intent.ts'; import type Observer from '../../../src/domain/api/Observer.ts'; import type { ReadingValue } from '../../../src/domain/api/ReadingValue.ts'; import type { McpJsonValue } from '../commands/mcp/McpJsonValue.ts'; +import { + V19_PUBLIC_NOUNS, +} from '../capabilities/V19CapabilityContract.generated.ts'; import { usageErrorFrom } from '../infrastructure.ts'; type JsonInput = @@ -79,7 +82,10 @@ export function intentFromText(text: string): Intent { try { return intentFromValue(JSON.parse(text)); } catch (error) { - throw usageErrorFrom('Invalid Intent JSON', error); + throw usageErrorFrom( + `Invalid ${V19_PUBLIC_NOUNS.Intent} JSON`, + error, + ); } } @@ -106,7 +112,7 @@ function parseIntentDescriptor( try { return INTENT_SCHEMA.parse(value); } catch (error) { - throw usageErrorFrom('Invalid Intent', error); + throw usageErrorFrom(`Invalid ${V19_PUBLIC_NOUNS.Intent}`, error); } } @@ -117,7 +123,10 @@ export function observerFromText( try { return observerFromValue(observerId, JSON.parse(text)); } catch (error) { - throw usageErrorFrom('Invalid Observer JSON', error); + throw usageErrorFrom( + `Invalid ${V19_PUBLIC_NOUNS.Observer} JSON`, + error, + ); } } @@ -141,7 +150,10 @@ function parseReadingDescriptor( try { return READING_SCHEMA.parse(value); } catch (error) { - throw usageErrorFrom('Invalid Observer reading', error); + throw usageErrorFrom( + `Invalid ${V19_PUBLIC_NOUNS.Observer} ${V19_PUBLIC_NOUNS.Reading}`, + error, + ); } } diff --git a/bin/cli/v19/V19SettlementReview.ts b/bin/cli/v19/V19SettlementReview.ts index f70a7e71..b59f4837 100644 --- a/bin/cli/v19/V19SettlementReview.ts +++ b/bin/cli/v19/V19SettlementReview.ts @@ -7,6 +7,9 @@ import type SettlementPlan from '../../../src/domain/settlement/SettlementPlan.t import type { SettlementPlanFields } from '../../../src/domain/settlement/SettlementPlan.ts'; import WarpError from '../../../src/domain/errors/WarpError.ts'; import type { McpJsonValue } from '../commands/mcp/McpJsonValue.ts'; +import { + V19_PUBLIC_NOUNS, +} from '../capabilities/V19CapabilityContract.generated.ts'; import { usageErrorFrom } from '../infrastructure.ts'; export type SettlementSelector = Readonly<{ @@ -78,7 +81,10 @@ export function reviewedSettlementFromValue( try { reviewed = REVIEW_SCHEMA.parse(value); } catch (error) { - throw usageErrorFrom('Invalid reviewed Settlement', error); + throw usageErrorFrom( + `Invalid reviewed ${V19_PUBLIC_NOUNS.Settlement}`, + error, + ); } return Object.freeze({ selector: freezeSelector(reviewed.selector), @@ -93,7 +99,7 @@ export async function applyReviewedSettlement( const current = await previewReviewedSettlement(runtime, reviewed.selector); if (!plansEqual(reviewed.plan, current.plan)) { throw new WarpError( - 'Reviewed Settlement plan no longer matches the current Runtime preview', + `Reviewed ${V19_PUBLIC_NOUNS.Settlement} plan no longer matches the current ${V19_PUBLIC_NOUNS.Runtime} preview`, 'E_RUNTIME_SETTLEMENT_REVIEW_MISMATCH', ); } diff --git a/diagnostics.ts b/diagnostics.ts index da3339ef..e6cb1b43 100644 --- a/diagnostics.ts +++ b/diagnostics.ts @@ -3,15 +3,10 @@ */ import { resolveReceiptProvenance } from './src/application/ReceiptProvenanceRegistry.ts'; -import type WarpStorage from './src/application/WarpStorage.ts'; import WarpError from './src/domain/errors/WarpError.ts'; import type ReadIdentity from './src/domain/services/optic/ReadIdentity.ts'; import type { Receipt } from './src/domain/api/Receipt.ts'; -export type InspectReceiptOptions = { - readonly storage: WarpStorage; -}; - export type ReceiptSubstrateInspection = | { readonly operation: 'write'; @@ -37,11 +32,8 @@ export type ReceiptInspection = { readonly substrate: ReceiptSubstrateInspection; }; -export function inspectReceipt( - receipt: Receipt, - options: InspectReceiptOptions -): ReceiptInspection { - const provenance = resolveReceiptProvenance(receipt, requireInspectStorage(options)); +export function inspectReceipt(receipt: Receipt): ReceiptInspection { + const provenance = resolveReceiptProvenance(receipt); if (provenance.operation !== receipt.operation) { throw new WarpError( 'Receipt provenance operation does not match the receipt', @@ -60,16 +52,6 @@ export function inspectReceipt( }); } -function requireInspectStorage(options: InspectReceiptOptions): WarpStorage { - if (typeof options !== 'object' || options === null || !('storage' in options)) { - throw new WarpError( - 'Receipt inspection requires an explicit storage context', - 'E_RECEIPT_INSPECTION_OPTIONS' - ); - } - return options.storage; -} - function receiptObjectIds(provenance: ReceiptSubstrateInspection): string[] { if (provenance.operation === 'write') { return provenance.patchSha === undefined ? [] : [provenance.patchSha]; diff --git a/docs/migrations/v19/README.md b/docs/migrations/v19/README.md index 36eeaf97..ec6327d4 100644 --- a/docs/migrations/v19/README.md +++ b/docs/migrations/v19/README.md @@ -308,9 +308,9 @@ durable ontology as a graph. Its first shipped Observer is a one-hop, bounded, cursor-page neighborhood chart. `/testing` provides an isolated real-Git `Runtime` harness without exposing storage construction at package root. -There is no public `/graph`, `/browser`, or `/legacy` package. The transitional -`/storage` export remains only until testing and diagnostics no longer require -the explicit handle; ordinary v19 application code must use `Runtime.open()`. +There is no public `/graph`, `/browser`, `/legacy`, or `/storage` package. +Production storage composition belongs to `Runtime.open()`; tests use the +explicit `/testing` harness. ## Symbol Map @@ -343,7 +343,7 @@ the explicit handle; ordinary v19 application code must use `Runtime.open()`. 7. Match all four admission variants exhaustively. 8. Keep existing cross-lane join code isolated until settlement plans land. 9. Replace graph-shaped reads with bounded `/charts` observers. -10. Remove imports from `/storage` when explicit diagnostics work is complete. +10. Verify that no import from the removed `/storage` subpath remains. ## Validation diff --git a/docs/topics/README.md b/docs/topics/README.md index 35ff6bc5..16188297 100644 --- a/docs/topics/README.md +++ b/docs/topics/README.md @@ -18,6 +18,8 @@ replay-backed. Operator workflows live outside the topic shelf in write a patch, read it back, and sync WARP refs. - [v19 public vocabulary checkpoint](api/): follow the accepted Runtime, Lane, Intent, Observer, Observation, Reading, and Receipt contract. +- [Generated v19 public vocabulary](vocabulary.generated.md): use the canonical + noun summaries lowered from the Wesley/GraphQL registry. - [Querying](querying.md): choose between worldlines, observers, optic reads, query builders, and strand sources. diff --git a/docs/topics/api/README.md b/docs/topics/api/README.md index a3f5a5c8..0afb1d59 100644 --- a/docs/topics/api/README.md +++ b/docs/topics/api/README.md @@ -1,13 +1,13 @@ # v19 Public Vocabulary Checkpoint -> **Status:** Accepted target for `v19.0.0`. +> **Status:** Implemented checkpoint for `v19.0.0`. > > This document is the normative product vocabulary and public-surface design. -> It is not implementation evidence. The Runtime, worldline Lane, Observer, -> streaming Observation, Reading, and Receipt core is implemented and covered -> by boundary tests. Fork, settlement, charts, and the generated SDK fixture -> are implemented. CLI/MCP convergence and release evidence remain part of the -> open v19 goalpost. +> The Runtime, Lane, Intent, Observer, streaming Observation, Reading, Receipt, +> settlement, charts, generated SDK, CLI, and MCP surfaces are implemented. +> One generated contract drives their public vocabulary and all twelve +> acceptance gates execute in CI. The release witness remains the final +> publication evidence. The product doctrine is: @@ -724,5 +724,6 @@ following: 12. Legacy vocabulary is rejected from public surfaces with explicit migration and substrate exceptions. -Until those gates pass, this document is a frozen target and the v19 public API -goalpost remains open. +The executable mapping for these gates lives in +`scripts/RunV19AcceptanceGates.ts`; CI and the release witness must run the +mapping without omissions before v19 publication. diff --git a/docs/topics/reference.md b/docs/topics/reference.md index c34394b8..22b4f90c 100644 --- a/docs/topics/reference.md +++ b/docs/topics/reference.md @@ -11,18 +11,16 @@ public API export, CLI command, package entrypoint, or public error class. | npm bin | `git-warp` | `./bin/git-warp` | `package.json#L23` | | npm bin | `git-warp-v18-to-v19` | `./dist/scripts/v18-to-v19/migrate.js` | `package.json#L24` | | npm export | `.` | `types=./dist/index.d.ts; import=./dist/index.js; default=./dist/index.js` | `package.json#L27` | -| npm export | `./storage` | `types=./dist/storage.d.ts; import=./dist/storage.js; default=./dist/storage.js` | `package.json#L32` | -| npm export | `./advanced` | `types=./dist/advanced.d.ts; import=./dist/advanced.js; default=./dist/advanced.js` | `package.json#L37` | -| npm export | `./diagnostics` | `types=./dist/diagnostics.d.ts; import=./dist/diagnostics.js; default=./dist/diagnostics.js` | `package.json#L42` | -| npm export | `./charts` | `types=./dist/charts.d.ts; import=./dist/charts.js; default=./dist/charts.js` | `package.json#L47` | -| npm export | `./testing` | `types=./dist/testing.d.ts; import=./dist/testing.js; default=./dist/testing.js` | `package.json#L52` | -| npm export | `./package.json` | `./package.json` | `package.json#L57` | +| npm export | `./advanced` | `types=./dist/advanced.d.ts; import=./dist/advanced.js; default=./dist/advanced.js` | `package.json#L32` | +| npm export | `./diagnostics` | `types=./dist/diagnostics.d.ts; import=./dist/diagnostics.js; default=./dist/diagnostics.js` | `package.json#L37` | +| npm export | `./charts` | `types=./dist/charts.d.ts; import=./dist/charts.js; default=./dist/charts.js` | `package.json#L42` | +| npm export | `./testing` | `types=./dist/testing.d.ts; import=./dist/testing.js; default=./dist/testing.js` | `package.json#L47` | +| npm export | `./package.json` | `./package.json` | `package.json#L52` | | JSR export | `.` | `./index.ts` | `jsr.json#L8` | -| JSR export | `./storage` | `./storage.ts` | `jsr.json#L9` | -| JSR export | `./advanced` | `./advanced.ts` | `jsr.json#L10` | -| JSR export | `./diagnostics` | `./diagnostics.ts` | `jsr.json#L11` | -| JSR export | `./charts` | `./charts.ts` | `jsr.json#L12` | -| JSR export | `./testing` | `./testing.ts` | `jsr.json#L13` | +| JSR export | `./advanced` | `./advanced.ts` | `jsr.json#L9` | +| JSR export | `./diagnostics` | `./diagnostics.ts` | `jsr.json#L10` | +| JSR export | `./charts` | `./charts.ts` | `jsr.json#L11` | +| JSR export | `./testing` | `./testing.ts` | `jsr.json#L12` | ## Root API export surface @@ -73,26 +71,6 @@ WitnessReference @ index.ts#L39 WriteReceipt @ index.ts#L45 ``` -## Storage export surface - -Transitional explicit storage composition; first-use applications use `Runtime.open()`. - -### Value exports - -Source: `storage.ts`. Count: 1. - -```text -GitStorage @ storage.ts#L3 -``` - -### Type exports - -Source: `storage.ts`. Count: 1. - -```text -GitStorageOptions @ storage.ts#L4 -``` - ## Advanced export surface Bounded formal reads and runtime-backed construction for generated SDK infrastructure. @@ -137,17 +115,16 @@ Operator inspection helpers that consume public receipt handles. Source: `diagnostics.ts`. Count: 1. ```text -inspectReceipt @ diagnostics.ts#L40 +inspectReceipt @ diagnostics.ts#L35 ``` ### Type exports -Source: `diagnostics.ts`. Count: 3. +Source: `diagnostics.ts`. Count: 2. ```text -InspectReceiptOptions @ diagnostics.ts#L11 -ReceiptInspection @ diagnostics.ts#L29 -ReceiptSubstrateInspection @ diagnostics.ts#L15 +ReceiptInspection @ diagnostics.ts#L24 +ReceiptSubstrateInspection @ diagnostics.ts#L10 ``` ## Charts export surface diff --git a/docs/topics/vocabulary.generated.md b/docs/topics/vocabulary.generated.md new file mode 100644 index 00000000..3df2a268 --- /dev/null +++ b/docs/topics/vocabulary.generated.md @@ -0,0 +1,16 @@ + + +# Public Vocabulary + +Write intents. Observe lanes. Keep receipts. + +| Noun | Meaning | +| --- | --- | +| Runtime | Opens local resources and owns Lane lifecycles. | +| Lane | Names one causal application history. | +| Intent | Describes one validated application write. | +| Observer | Defines one bounded read over a Lane. | +| Observation | Executes an Observer once and streams Readings. | +| Reading | Carries a canonical value with bounded support. | +| Receipt | Records the durable outcome of an operation. | +| Settlement | Revalidates and applies an immutable plan between Lanes. | diff --git a/jsr.json b/jsr.json index a8bbd056..ecf907a9 100644 --- a/jsr.json +++ b/jsr.json @@ -6,7 +6,6 @@ }, "exports": { ".": "./index.ts", - "./storage": "./storage.ts", "./advanced": "./advanced.ts", "./diagnostics": "./diagnostics.ts", "./charts": "./charts.ts", @@ -15,7 +14,6 @@ "publish": { "include": [ "index.ts", - "storage.ts", "advanced.ts", "diagnostics.ts", "charts.ts", diff --git a/package.json b/package.json index 7c00e181..ceb3ec67 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,6 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, - "./storage": { - "types": "./dist/storage.d.ts", - "import": "./dist/storage.js", - "default": "./dist/storage.js" - }, "./advanced": { "types": "./dist/advanced.d.ts", "import": "./dist/advanced.js", @@ -72,6 +67,8 @@ }, "scripts": { "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.publish.json", + "generate:vocabulary-ir": "node scripts/GenerateV19VocabularyIr.ts", + "check:vocabulary-ir": "node scripts/GenerateV19VocabularyIr.ts --check", "generate:capabilities": "node scripts/GenerateV19CapabilityContract.ts", "check:capabilities": "node scripts/GenerateV19CapabilityContract.ts --check", "generate:sdk-fixture": "node scripts/generated-sdk/GenerateUsersSdkFixture.ts", @@ -98,6 +95,7 @@ "test:watch": "vitest", "test:coverage": "GIT_WARP_UPDATE_COVERAGE_RATCHET=1 vitest run --coverage test/unit test/conformance/v18*Optic*.test.ts", "test:coverage:ci": "vitest run --coverage test/unit test/conformance/v18*Optic*.test.ts", + "test:v19-acceptance": "node scripts/RunV19AcceptanceGates.ts", "test:sdk-fixture": "bash scripts/smoke-generated-sdk.sh", "benchmark": "sh -c 'if [ \"$GIT_STUNTS_DOCKER\" = \"1\" ]; then vitest bench --run test/benchmark \"$@\"; else docker compose -f docker/docker-compose.yml run --build --rm test npm run benchmark:local -- \"$@\"; fi' --", "benchmark:local": "vitest bench --run test/benchmark", diff --git a/schemas/v19-public-vocabulary.graphql b/schemas/v19-public-vocabulary.graphql new file mode 100644 index 00000000..628587cb --- /dev/null +++ b/schemas/v19-public-vocabulary.graphql @@ -0,0 +1,305 @@ +directive @registry( + version: String! + moduleSummary: String! + sdkSummary: String! + formalIdentifiers: [String!]! + exceptionPaths: [String!]! +) on OBJECT + +directive @noun( + name: String! + summary: String! +) on FIELD_DEFINITION + +directive @capability( + cliOrder: Int + cliCommand: String + cliSummary: String + cliUsage: String + mcpOrder: Int + mcpName: String + mcpDescription: String +) on FIELD_DEFINITION + +directive @forbidden( + phrase: String! + scopes: [VocabularyScope!]! +) on FIELD_DEFINITION + +directive @range( + minimum: Int! + maximum: Int! +) on ARGUMENT_DEFINITION + +directive @allowedValues( + values: [String!]! +) on ARGUMENT_DEFINITION + +enum VocabularyScope { + ROOT_DECLARATION + PUBLIC_SURFACE +} + +scalar CapabilityResult +scalar JsonObject +scalar RegistryEntry + +type PublicVocabulary + @registry( + version: "git-warp.capabilities/v19" + moduleSummary: "Write intents. Observe lanes. Keep receipts." + sdkSummary: "Generated SDKs construct validated Intent and Observer values for Runtime Lane workflows." + formalIdentifiers: [ + "WitnessReference" + "WitnessReferences" + "witnessRef" + "witnessRefs" + ] + exceptionPaths: [ + "docs/migrations/" + "docs/topics/api/README.md" + "docs/topics/git-perf.md" + "docs/topics/git-substrate.md" + "diagnostics.ts" + "scripts/v18-to-v19/" + "src/infrastructure/" + ] + ) { + runtime: RegistryEntry + @noun( + name: "Runtime" + summary: "Opens local resources and owns Lane lifecycles." + ) + lane: RegistryEntry + @noun( + name: "Lane" + summary: "Names one causal application history." + ) + intent: RegistryEntry + @noun( + name: "Intent" + summary: "Describes one validated application write." + ) + observer: RegistryEntry + @noun( + name: "Observer" + summary: "Defines one bounded read over a Lane." + ) + observation: RegistryEntry + @noun( + name: "Observation" + summary: "Executes an Observer once and streams Readings." + ) + reading: RegistryEntry + @noun( + name: "Reading" + summary: "Carries a canonical value with bounded support." + ) + receipt: RegistryEntry + @noun( + name: "Receipt" + summary: "Records the durable outcome of an operation." + ) + settlement: RegistryEntry + @noun( + name: "Settlement" + summary: "Revalidates and applies an immutable plan between Lanes." + ) +} + +type PublicCapabilities { + laneDescribe( + lane: String! + strand: String + ): CapabilityResult + @capability( + mcpOrder: 1 + mcpName: "warp_lane_describe" + mcpDescription: "Describe one Runtime Lane." + ) + + write( + lane: String! + strand: String + intent: JsonObject! + ): CapabilityResult + @capability( + cliOrder: 1 + cliCommand: "write" + cliSummary: "Write one Intent to a Lane." + cliUsage: "git warp write --lane [--strand ] --intent " + mcpOrder: 2 + mcpName: "warp_intent_write" + mcpDescription: "Write one validated Intent to a Lane and return its canonical Receipt." + ) + + observeStart( + lane: String! + strand: String + observerId: String! + reading: JsonObject! + ): CapabilityResult + @capability( + cliOrder: 2 + cliCommand: "observe" + cliSummary: "Run one bounded Observer over a Lane." + cliUsage: "git warp observe --lane [--strand ] --observer --reading " + mcpOrder: 3 + mcpName: "warp_observation_start" + mcpDescription: "Start one bounded Observation and retain its Readings for transport." + ) + + observationRead( + observationId: String! + cursor: String + limit: Int @range(minimum: 1, maximum: 256) + ): CapabilityResult + @capability( + mcpOrder: 4 + mcpName: "warp_observation_read" + mcpDescription: "Read the next bounded transport batch from an Observation." + ) + + observationCancel( + observationId: String! + ): CapabilityResult + @capability( + mcpOrder: 5 + mcpName: "warp_observation_cancel" + mcpDescription: "Cancel and discard one retained Observation transport." + ) + + fork: CapabilityResult + @capability( + cliOrder: 3 + cliCommand: "fork" + cliSummary: "Fork a Lane into a named strand Lane." + cliUsage: "git warp fork --lane --name " + ) + + receiptGet( + receiptRef: String! + ): CapabilityResult + @capability( + cliOrder: 5 + cliCommand: "receipt" + cliSummary: "Render a canonical Receipt envelope." + cliUsage: "git warp receipt show --input " + mcpOrder: 6 + mcpName: "warp_receipt_get" + mcpDescription: "Return one canonical Receipt retained by this MCP server." + ) + + settlementPreview( + sourceLane: String! + sourceStrand: String! + targetLane: String! + ): CapabilityResult + @capability( + cliOrder: 4 + cliCommand: "settle" + cliSummary: "Preview or apply an immutable Settlement plan." + cliUsage: "git warp settle preview --source --strand --target | settle apply --plan " + mcpOrder: 7 + mcpName: "warp_settlement_preview" + mcpDescription: "Preview an immutable Settlement plan between two Lanes." + ) + + settlementApply( + planRef: String! + ): CapabilityResult + @capability( + mcpOrder: 8 + mcpName: "warp_settlement_apply" + mcpDescription: "Revalidate and apply one retained immutable Settlement plan." + ) + + doctor( + lane: String! + ): CapabilityResult + @capability( + cliOrder: 6 + cliCommand: "doctor" + cliSummary: "Diagnose the local Runtime and Lane." + cliUsage: "git warp doctor --lane " + mcpOrder: 9 + mcpName: "warp_doctor" + mcpDescription: "Diagnose one local Runtime Lane." + ) + + repair( + lane: String! + action: String! @allowedValues(values: ["materialization"]) + ): CapabilityResult + @capability( + cliOrder: 7 + cliCommand: "repair" + cliSummary: "Apply one explicit Runtime repair." + cliUsage: "git warp repair --lane --action materialization" + mcpOrder: 10 + mcpName: "warp_repair" + mcpDescription: "Apply one explicit local Runtime repair." + ) + + audit( + lane: String! + writer: String + ): CapabilityResult + @capability( + cliOrder: 8 + cliCommand: "audit" + cliSummary: "Verify the local Runtime audit trail." + cliUsage: "git warp audit --lane " + mcpOrder: 11 + mcpName: "warp_audit" + mcpDescription: "Verify the local Runtime audit trail." + ) + + serveMcp: CapabilityResult + @capability( + cliOrder: 9 + cliCommand: "mcp" + cliSummary: "Serve the v19 capabilities over MCP stdio." + cliUsage: "git warp mcp --repo --writer " + ) +} + +type RejectedVocabulary { + blob: RegistryEntry + @forbidden(phrase: "blob", scopes: [ROOT_DECLARATION]) + cas: RegistryEntry + @forbidden(phrase: "cas", scopes: [ROOT_DECLARATION]) + commit: RegistryEntry + @forbidden(phrase: "commit", scopes: [ROOT_DECLARATION]) + git: RegistryEntry + @forbidden(phrase: "git", scopes: [ROOT_DECLARATION]) + objectId: RegistryEntry + @forbidden(phrase: "object id", scopes: [ROOT_DECLARATION]) + oid: RegistryEntry + @forbidden( + phrase: "oid" + scopes: [ROOT_DECLARATION, PUBLIC_SURFACE] + ) + plumbing: RegistryEntry + @forbidden(phrase: "plumbing", scopes: [ROOT_DECLARATION]) + ref: RegistryEntry + @forbidden(phrase: "ref", scopes: [ROOT_DECLARATION]) + sha: RegistryEntry + @forbidden(phrase: "sha", scopes: [ROOT_DECLARATION]) + tree: RegistryEntry + @forbidden(phrase: "tree", scopes: [ROOT_DECLARATION]) + timeline: RegistryEntry + @forbidden(phrase: "timeline", scopes: [PUBLIC_SURFACE]) + merge: RegistryEntry + @forbidden(phrase: "merge", scopes: [PUBLIC_SURFACE]) + graphStore: RegistryEntry + @forbidden(phrase: "graph store", scopes: [PUBLIC_SURFACE]) + genericEvent: RegistryEntry + @forbidden(phrase: "generic event", scopes: [PUBLIC_SURFACE]) + dryRun: RegistryEntry + @forbidden(phrase: "dry run", scopes: [PUBLIC_SURFACE]) + session: RegistryEntry + @forbidden(phrase: "session", scopes: [PUBLIC_SURFACE]) + queryResultPage: RegistryEntry + @forbidden(phrase: "query result page", scopes: [PUBLIC_SURFACE]) +} diff --git a/schemas/v19-public-vocabulary.wesley.generated.json b/schemas/v19-public-vocabulary.wesley.generated.json new file mode 100644 index 00000000..aec3b8d6 --- /dev/null +++ b/schemas/v19-public-vocabulary.wesley.generated.json @@ -0,0 +1,921 @@ +{ + "version": "1.0.0", + "types": [ + { + "name": "CapabilityResult", + "kind": "SCALAR", + "directives": {} + }, + { + "name": "JsonObject", + "kind": "SCALAR", + "directives": {} + }, + { + "name": "PublicCapabilities", + "kind": "OBJECT", + "directives": {}, + "fields": [ + { + "name": "laneDescribe", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "strand", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "mcpOrder": 1, + "mcpName": "warp_lane_describe", + "mcpDescription": "Describe one Runtime Lane." + } + } + }, + { + "name": "write", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "strand", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + }, + { + "name": "intent", + "type": { + "base": "JsonObject", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 1, + "cliCommand": "write", + "cliSummary": "Write one Intent to a Lane.", + "cliUsage": "git warp write --lane [--strand ] --intent ", + "mcpOrder": 2, + "mcpName": "warp_intent_write", + "mcpDescription": "Write one validated Intent to a Lane and return its canonical Receipt." + } + } + }, + { + "name": "observeStart", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "strand", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + }, + { + "name": "observerId", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "reading", + "type": { + "base": "JsonObject", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 2, + "cliCommand": "observe", + "cliSummary": "Run one bounded Observer over a Lane.", + "cliUsage": "git warp observe --lane [--strand ] --observer --reading ", + "mcpOrder": 3, + "mcpName": "warp_observation_start", + "mcpDescription": "Start one bounded Observation and retain its Readings for transport." + } + } + }, + { + "name": "observationRead", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "observationId", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "cursor", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + }, + { + "name": "limit", + "type": { + "base": "Int", + "nullable": true, + "isList": false + }, + "directives": { + "range": { + "minimum": 1, + "maximum": 256 + } + } + } + ], + "directives": { + "capability": { + "mcpOrder": 4, + "mcpName": "warp_observation_read", + "mcpDescription": "Read the next bounded transport batch from an Observation." + } + } + }, + { + "name": "observationCancel", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "observationId", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "mcpOrder": 5, + "mcpName": "warp_observation_cancel", + "mcpDescription": "Cancel and discard one retained Observation transport." + } + } + }, + { + "name": "fork", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "directives": { + "capability": { + "cliOrder": 3, + "cliCommand": "fork", + "cliSummary": "Fork a Lane into a named strand Lane.", + "cliUsage": "git warp fork --lane --name " + } + } + }, + { + "name": "receiptGet", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "receiptRef", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 5, + "cliCommand": "receipt", + "cliSummary": "Render a canonical Receipt envelope.", + "cliUsage": "git warp receipt show --input ", + "mcpOrder": 6, + "mcpName": "warp_receipt_get", + "mcpDescription": "Return one canonical Receipt retained by this MCP server." + } + } + }, + { + "name": "settlementPreview", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "sourceLane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "sourceStrand", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "targetLane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 4, + "cliCommand": "settle", + "cliSummary": "Preview or apply an immutable Settlement plan.", + "cliUsage": "git warp settle preview --source --strand --target | settle apply --plan ", + "mcpOrder": 7, + "mcpName": "warp_settlement_preview", + "mcpDescription": "Preview an immutable Settlement plan between two Lanes." + } + } + }, + { + "name": "settlementApply", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "planRef", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "mcpOrder": 8, + "mcpName": "warp_settlement_apply", + "mcpDescription": "Revalidate and apply one retained immutable Settlement plan." + } + } + }, + { + "name": "doctor", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 6, + "cliCommand": "doctor", + "cliSummary": "Diagnose the local Runtime and Lane.", + "cliUsage": "git warp doctor --lane ", + "mcpOrder": 9, + "mcpName": "warp_doctor", + "mcpDescription": "Diagnose one local Runtime Lane." + } + } + }, + { + "name": "repair", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "action", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "allowedValues": { + "values": [ + "materialization" + ] + } + } + } + ], + "directives": { + "capability": { + "cliOrder": 7, + "cliCommand": "repair", + "cliSummary": "Apply one explicit Runtime repair.", + "cliUsage": "git warp repair --lane --action materialization", + "mcpOrder": 10, + "mcpName": "warp_repair", + "mcpDescription": "Apply one explicit local Runtime repair." + } + } + }, + { + "name": "audit", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "arguments": [ + { + "name": "lane", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "writer", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + } + ], + "directives": { + "capability": { + "cliOrder": 8, + "cliCommand": "audit", + "cliSummary": "Verify the local Runtime audit trail.", + "cliUsage": "git warp audit --lane ", + "mcpOrder": 11, + "mcpName": "warp_audit", + "mcpDescription": "Verify the local Runtime audit trail." + } + } + }, + { + "name": "serveMcp", + "type": { + "base": "CapabilityResult", + "nullable": true, + "isList": false + }, + "directives": { + "capability": { + "cliOrder": 9, + "cliCommand": "mcp", + "cliSummary": "Serve the v19 capabilities over MCP stdio.", + "cliUsage": "git warp mcp --repo --writer " + } + } + } + ] + }, + { + "name": "PublicVocabulary", + "kind": "OBJECT", + "directives": { + "registry": { + "version": "git-warp.capabilities/v19", + "moduleSummary": "Write intents. Observe lanes. Keep receipts.", + "sdkSummary": "Generated SDKs construct validated Intent and Observer values for Runtime Lane workflows.", + "formalIdentifiers": [ + "WitnessReference", + "WitnessReferences", + "witnessRef", + "witnessRefs" + ], + "exceptionPaths": [ + "docs/migrations/", + "docs/topics/api/README.md", + "docs/topics/git-perf.md", + "docs/topics/git-substrate.md", + "diagnostics.ts", + "scripts/v18-to-v19/", + "src/infrastructure/" + ] + } + }, + "fields": [ + { + "name": "runtime", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Runtime", + "summary": "Opens local resources and owns Lane lifecycles." + } + } + }, + { + "name": "lane", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Lane", + "summary": "Names one causal application history." + } + } + }, + { + "name": "intent", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Intent", + "summary": "Describes one validated application write." + } + } + }, + { + "name": "observer", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Observer", + "summary": "Defines one bounded read over a Lane." + } + } + }, + { + "name": "observation", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Observation", + "summary": "Executes an Observer once and streams Readings." + } + } + }, + { + "name": "reading", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Reading", + "summary": "Carries a canonical value with bounded support." + } + } + }, + { + "name": "receipt", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Receipt", + "summary": "Records the durable outcome of an operation." + } + } + }, + { + "name": "settlement", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "noun": { + "name": "Settlement", + "summary": "Revalidates and applies an immutable plan between Lanes." + } + } + } + ] + }, + { + "name": "RegistryEntry", + "kind": "SCALAR", + "directives": {} + }, + { + "name": "RejectedVocabulary", + "kind": "OBJECT", + "directives": {}, + "fields": [ + { + "name": "blob", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "blob", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "cas", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "cas", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "commit", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "commit", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "git", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "git", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "objectId", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "object id", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "oid", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "oid", + "scopes": [ + "ROOT_DECLARATION", + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "plumbing", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "plumbing", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "ref", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "ref", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "sha", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "sha", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "tree", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "tree", + "scopes": [ + "ROOT_DECLARATION" + ] + } + } + }, + { + "name": "timeline", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "timeline", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "merge", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "merge", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "graphStore", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "graph store", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "genericEvent", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "generic event", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "dryRun", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "dry run", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "session", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "session", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + }, + { + "name": "queryResultPage", + "type": { + "base": "RegistryEntry", + "nullable": true, + "isList": false + }, + "directives": { + "forbidden": { + "phrase": "query result page", + "scopes": [ + "PUBLIC_SURFACE" + ] + } + } + } + ] + }, + { + "name": "VocabularyScope", + "kind": "ENUM", + "directives": {}, + "enumValues": [ + "ROOT_DECLARATION", + "PUBLIC_SURFACE" + ] + } + ] +} diff --git a/scripts/GenerateV19CapabilityContract.ts b/scripts/GenerateV19CapabilityContract.ts index 59d25f71..616fc23e 100644 --- a/scripts/GenerateV19CapabilityContract.ts +++ b/scripts/GenerateV19CapabilityContract.ts @@ -1,94 +1,19 @@ -import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { z } from 'zod'; -const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(SCRIPT_DIRECTORY, '..'); -const SOURCE_PATH = resolve( - ROOT, - 'bin/cli/capabilities/v19-capabilities.json', -); -const OUTPUT_PATH = resolve( - ROOT, - 'bin/cli/capabilities/V19CapabilityContract.generated.ts', -); +import { emitV19VocabularyArtifacts } from './V19VocabularyArtifacts.ts'; +import { requireCleanPublicVocabulary } from './V19VocabularyConformance.ts'; +import { readV19VocabularyContract } from './V19VocabularyContract.ts'; -const JSON_VALUE_SCHEMA: z.ZodType< - null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue } -> = z.lazy(() => - z.union([ - z.null(), - z.boolean(), - z.number(), - z.string(), - z.array(JSON_VALUE_SCHEMA), - z.record(z.string(), JSON_VALUE_SCHEMA), - ]), +const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url)); +const root = resolve(SCRIPT_DIRECTORY, '..'); +const contract = readV19VocabularyContract( + resolve(root, 'schemas/v19-public-vocabulary.wesley.generated.json'), ); +requireCleanPublicVocabulary(contract); -type JsonValue = - | null - | boolean - | number - | string - | readonly JsonValue[] - | { readonly [key: string]: JsonValue }; - -const CONTRACT_SCHEMA = z.object({ - version: z.literal('git-warp.capabilities/v19'), - cli: z.array(z.object({ - command: z.string().min(1), - summary: z.string().min(1), - usage: z.string().min(1), - })).min(1), - mcp: z.array(z.object({ - name: z.string().regex(/^warp_[a-z_]+$/u), - description: z.string().min(1), - inputSchema: z.record(z.string(), JSON_VALUE_SCHEMA), - })).min(1), +emitV19VocabularyArtifacts({ + root, + contract, + mode: process.argv.includes('--check') ? 'check' : 'write', }); - -class CapabilityContractDriftError extends Error { - constructor(message: string) { - super(message); - this.name = 'CapabilityContractDriftError'; - } -} - -function renderContract(): string { - const source = CONTRACT_SCHEMA.parse( - JSON.parse(readFileSync(SOURCE_PATH, 'utf8')), - ); - requireUnique(source.cli.map((entry) => entry.command), 'CLI command'); - requireUnique(source.mcp.map((entry) => entry.name), 'MCP tool'); - return [ - '/* @generated from v19-capabilities.json. Do not edit by hand. */', - '', - `export const V19_CAPABILITY_CONTRACT = ${JSON.stringify(source, null, 2)} as const;`, - '', - ].join('\n'); -} - -function requireUnique(values: readonly string[], kind: string): void { - const duplicates = values.filter( - (value, index) => values.indexOf(value) !== index, - ); - if (duplicates.length > 0) { - throw new CapabilityContractDriftError( - `${kind} names must be unique: ${[...new Set(duplicates)].join(', ')}`, - ); - } -} - -const rendered = renderContract(); -if (process.argv.includes('--check')) { - const current = readFileSync(OUTPUT_PATH, 'utf8'); - if (current !== rendered) { - throw new CapabilityContractDriftError( - 'v19 capability contract drifted; run npm run generate:capabilities', - ); - } -} else { - writeFileSync(OUTPUT_PATH, rendered); -} diff --git a/scripts/GenerateV19VocabularyIr.ts b/scripts/GenerateV19VocabularyIr.ts new file mode 100644 index 00000000..cf1c3973 --- /dev/null +++ b/scripts/GenerateV19VocabularyIr.ts @@ -0,0 +1,70 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(SCRIPT_DIRECTORY, '..'); +const SCHEMA_PATH = resolve( + ROOT, + 'schemas/v19-public-vocabulary.graphql', +); +const OUTPUT_PATH = resolve( + ROOT, + 'schemas/v19-public-vocabulary.wesley.generated.json', +); + +class VocabularyIrDriftError extends Error { + constructor(message: string) { + super(message); + this.name = 'VocabularyIrDriftError'; + } +} + +class VocabularyIrUsageError extends Error { + constructor(message: string) { + super(message); + this.name = 'VocabularyIrUsageError'; + } +} + +function wesleyBinary(): string { + const index = process.argv.indexOf('--wesley'); + if (index === -1) { + return 'wesley'; + } + const binary = process.argv[index + 1]; + if (binary === undefined || binary.startsWith('--')) { + throw new VocabularyIrUsageError('--wesley requires an executable path'); + } + return binary; +} + +function lowerVocabularySchema(binary: string): string { + const output = execFileSync( + binary, + ['schema', 'lower', '--schema', SCHEMA_PATH, '--json'], + { + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + timeout: 60_000, + }, + ); + return output.endsWith('\n') ? output : `${output}\n`; +} + +const rendered = lowerVocabularySchema(wesleyBinary()); +if (process.argv.includes('--check')) { + if (!existsSync(OUTPUT_PATH)) { + throw new VocabularyIrDriftError( + 'v19 vocabulary IR is missing; run npm run generate:vocabulary-ir', + ); + } + if (readFileSync(OUTPUT_PATH, 'utf8') !== rendered) { + throw new VocabularyIrDriftError( + 'v19 vocabulary IR drifted; run npm run generate:vocabulary-ir', + ); + } +} else { + writeFileSync(OUTPUT_PATH, rendered); +} diff --git a/scripts/RunV19AcceptanceGates.ts b/scripts/RunV19AcceptanceGates.ts new file mode 100644 index 00000000..0eae0a46 --- /dev/null +++ b/scripts/RunV19AcceptanceGates.ts @@ -0,0 +1,138 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + V19_CAPABILITY_CONTRACT, +} from '../bin/cli/capabilities/V19CapabilityContract.generated.ts'; + +type AcceptanceGate = Readonly<{ + readonly id: number; + readonly evidence: readonly string[]; +}>; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const GATES: readonly AcceptanceGate[] = [ + { + id: 1, + evidence: ['test/unit/scripts/v19-public-api-boundary.test.ts'], + }, + { + id: 2, + evidence: ['test/unit/scripts/v19-public-api-boundary.test.ts'], + }, + { + id: 3, + evidence: ['test/unit/domain/Lane.test.ts'], + }, + { + id: 4, + evidence: [ + 'test/unit/domain/IntentRuntime.test.ts', + 'test/unit/domain/ObserverRuntime.test.ts', + ], + }, + { + id: 5, + evidence: ['test/unit/domain/Observation.test.ts'], + }, + { + id: 6, + evidence: ['test/unit/application/RuntimeLaneAdapter.test.ts'], + }, + { + id: 7, + evidence: [ + 'test/unit/domain/ObservedReading.test.ts', + 'test/unit/cli/commands/mcp.test.ts', + ], + }, + { + id: 8, + evidence: [ + 'test/unit/domain/AdmissionOutcomeRuntime.test.ts', + 'test/unit/domain/ObservedReading.test.ts', + ], + }, + { + id: 9, + evidence: [ + 'test/unit/application/RuntimeSettlement.preview.test.ts', + 'test/unit/domain/settlement/SettlementPlan.test.ts', + ], + }, + { + id: 10, + evidence: [ + 'test/unit/scripts/v19-public-api-boundary.test.ts', + 'test/unit/domain/GraphChartObservers.test.ts', + ], + }, + { + id: 11, + evidence: [ + 'test/unit/scripts/cli-command-registry.test.ts', + 'test/unit/scripts/cli-help-shape.test.ts', + 'test/unit/cli/commands/mcp.test.ts', + ], + }, + { + id: 12, + evidence: [ + 'test/unit/scripts/v19-root-declaration-gate.test.ts', + 'test/unit/scripts/v19-vocabulary-contract.test.ts', + ], + }, +]; + +class V19AcceptanceGateError extends Error { + constructor(message: string) { + super(message); + this.name = 'V19AcceptanceGateError'; + } +} + +function requireCompleteGateMap(): void { + const identifiers = GATES.map((gate) => gate.id); + const expected = Array.from({ length: 12 }, (_, index) => index + 1); + if (JSON.stringify(identifiers) !== JSON.stringify(expected)) { + throw new V19AcceptanceGateError( + 'v19 acceptance evidence must map gates 1 through 12 exactly once', + ); + } + const checkpoint = readFileSync( + resolve(ROOT, 'docs/topics/api/README.md'), + 'utf8', + ); + for (const gate of GATES) { + if (!checkpoint.includes(`\n${gate.id}. `)) { + throw new V19AcceptanceGateError( + `docs/topics/api/README.md is missing acceptance gate ${gate.id}`, + ); + } + if (gate.evidence.length === 0) { + throw new V19AcceptanceGateError( + `acceptance gate ${gate.id} has no executable evidence`, + ); + } + } +} + +function run(command: string, args: readonly string[]): void { + execFileSync(command, args, { + cwd: ROOT, + stdio: 'inherit', + }); +} + +requireCompleteGateMap(); +if (V19_CAPABILITY_CONTRACT.version !== 'git-warp.capabilities/v19') { + throw new V19AcceptanceGateError('unexpected v19 registry version'); +} +run('npm', ['run', 'typecheck:consumer', '--silent']); +run(process.execPath, [ + resolve(ROOT, 'node_modules/vitest/vitest.mjs'), + 'run', + ...new Set(GATES.flatMap((gate) => gate.evidence)), +]); diff --git a/scripts/V19CapabilityMetadata.ts b/scripts/V19CapabilityMetadata.ts new file mode 100644 index 00000000..b57dad3a --- /dev/null +++ b/scripts/V19CapabilityMetadata.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +const CAPABILITY_SCHEMA = z.object({ + cliOrder: z.number().int().positive().optional(), + cliCommand: z.string().min(1).optional(), + cliSummary: z.string().min(1).optional(), + cliUsage: z.string().min(1).optional(), + mcpOrder: z.number().int().positive().optional(), + mcpName: z.string().regex(/^warp_[a-z_]+$/u).optional(), + mcpDescription: z.string().min(1).optional(), +}); + +export type CapabilityMetadata = z.infer; + +export class CapabilityContractError extends Error { + constructor(message: string) { + super(message); + this.name = 'CapabilityContractError'; + } +} + +export function parseCapabilityMetadata( + field: string, + raw: unknown, +): CapabilityMetadata { + if (raw === undefined) { + throw new CapabilityContractError( + `${field} is missing required capability metadata`, + ); + } + const parsed = CAPABILITY_SCHEMA.safeParse(raw); + if (!parsed.success) { + throw new CapabilityContractError( + `${field} has invalid capability metadata: ${parsed.error.issues + .map((issue) => issue.message) + .join('; ')}`, + ); + } + return parsed.data; +} diff --git a/scripts/V19VocabularyArtifacts.ts b/scripts/V19VocabularyArtifacts.ts new file mode 100644 index 00000000..b4d00a0b --- /dev/null +++ b/scripts/V19VocabularyArtifacts.ts @@ -0,0 +1,153 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { z } from 'zod'; + +import { + CapabilityContractError, + type JsonObject, + NOUN_SCHEMA, +} from './V19VocabularyContract.ts'; + +type ArtifactMode = 'check' | 'write'; + +type VocabularyArtifactOptions = { + readonly root: string; + readonly contract: JsonObject; + readonly mode: ArtifactMode; +}; + +const GENERATED_HEADER = + '/* @generated from schemas/v19-public-vocabulary.graphql by Wesley. Do not edit. */'; + +function renderPublicVocabularyTypescript(contract: JsonObject): string { + const nouns = z.array(NOUN_SCHEMA).parse(contract['nouns']); + const publicNouns = Object.fromEntries( + nouns.map((noun) => [noun.name, noun.name]), + ); + const publicVocabulary = Object.fromEntries( + Object.entries(contract).filter(([name]) => name !== 'mcp'), + ); + return [ + GENERATED_HEADER, + '', + `export const V19_PUBLIC_VOCABULARY = ${JSON.stringify(publicVocabulary, null, 2)} as const;`, + '', + `export const V19_PUBLIC_NOUNS = ${JSON.stringify(publicNouns, null, 2)} as const;`, + '', + ].join('\n'); +} + +function renderMcpTypescript(contract: JsonObject): string { + const mcp = contract['mcp']; + if (!Array.isArray(mcp)) { + throw new CapabilityContractError( + 'v19 vocabulary contract requires MCP capabilities', + ); + } + return [ + GENERATED_HEADER, + '', + `export const V19_MCP_CAPABILITIES = ${JSON.stringify(mcp, null, 2)} as const;`, + '', + ].join('\n'); +} + +function renderContractTypescript(): string { + return [ + GENERATED_HEADER, + '', + "import { V19_MCP_CAPABILITIES } from './V19McpCapabilities.generated.ts';", + 'import {', + ' V19_PUBLIC_NOUNS,', + ' V19_PUBLIC_VOCABULARY,', + "} from './V19PublicVocabulary.generated.ts';", + '', + 'export { V19_PUBLIC_NOUNS };', + '', + 'export const V19_CAPABILITY_CONTRACT = {', + ' ...V19_PUBLIC_VOCABULARY,', + ' mcp: V19_MCP_CAPABILITIES,', + '} as const;', + '', + ].join('\n'); +} + +function renderGlossary(contract: JsonObject): string { + const nouns = z.array(NOUN_SCHEMA).parse(contract['nouns']); + return [ + '', + '', + '# Public Vocabulary', + '', + z.string().parse(contract['moduleSummary']), + '', + '| Noun | Meaning |', + '| --- | --- |', + ...nouns.map((noun) => `| ${noun.name} | ${noun.summary} |`), + '', + ].join('\n'); +} + +function requireCurrent(path: string, expected: string): void { + if (!existsSync(path)) { + throw new CapabilityContractError( + `v19 vocabulary artifact is missing: ${path}; run npm run generate:capabilities`, + ); + } + if (readFileSync(path, 'utf8') !== expected) { + throw new CapabilityContractError( + `v19 vocabulary artifact drifted: ${path}; run npm run generate:capabilities`, + ); + } +} + +function emitArtifact( + path: string, + content: string, + mode: ArtifactMode, +): void { + if (mode === 'check') { + requireCurrent(path, content); + return; + } + writeFileSync(path, content); +} + +export function emitV19VocabularyArtifacts( + options: VocabularyArtifactOptions, +): void { + emitArtifact( + resolve(options.root, 'bin/cli/capabilities/v19-capabilities.json'), + `${JSON.stringify(options.contract, null, 2)}\n`, + options.mode, + ); + emitArtifact( + resolve( + options.root, + 'bin/cli/capabilities/V19CapabilityContract.generated.ts', + ), + renderContractTypescript(), + options.mode, + ); + emitArtifact( + resolve( + options.root, + 'bin/cli/capabilities/V19PublicVocabulary.generated.ts', + ), + renderPublicVocabularyTypescript(options.contract), + options.mode, + ); + emitArtifact( + resolve( + options.root, + 'bin/cli/capabilities/V19McpCapabilities.generated.ts', + ), + renderMcpTypescript(options.contract), + options.mode, + ); + emitArtifact( + resolve(options.root, 'docs/topics/vocabulary.generated.md'), + renderGlossary(options.contract), + options.mode, + ); +} diff --git a/scripts/V19VocabularyConformance.ts b/scripts/V19VocabularyConformance.ts new file mode 100644 index 00000000..19f5a359 --- /dev/null +++ b/scripts/V19VocabularyConformance.ts @@ -0,0 +1,100 @@ +import { z } from 'zod'; + +import type { JsonObject } from './V19VocabularyContract.ts'; +import { CapabilityContractError } from './V19VocabularyContract.ts'; +import { + containsVocabularyPhrase, + vocabularyTokens, +} from './V19VocabularyMatching.ts'; + +const CONTRACT_SCHEMA = z.object({ + moduleSummary: z.string(), + sdkSummary: z.string(), + nouns: z.array(z.object({ + name: z.string(), + summary: z.string(), + })), + forbiddenTerms: z.array(z.object({ + phrase: z.string(), + scopes: z.array(z.enum(['ROOT_DECLARATION', 'PUBLIC_SURFACE'])), + })), + cli: z.array(z.object({ + command: z.string(), + summary: z.string(), + usage: z.string(), + })), + mcp: z.array(z.object({ + name: z.string(), + description: z.string(), + })), +}); + +type PublicText = Readonly<{ + readonly field: string; + readonly value: string; +}>; + +export type PublicVocabularyViolation = Readonly<{ + readonly field: string; + readonly phrase: string; + readonly value: string; +}>; + +export function findForbiddenPublicVocabulary( + contract: JsonObject, +): readonly PublicVocabularyViolation[] { + const parsed = CONTRACT_SCHEMA.parse(contract); + const forbidden = parsed.forbiddenTerms.filter( + (term) => term.scopes.includes('PUBLIC_SURFACE'), + ); + return publicText(parsed).flatMap((candidate) => + forbidden + .filter((term) => containsVocabularyPhrase( + vocabularyTokens(candidate.value), + vocabularyTokens(term.phrase), + )) + .map((term) => Object.freeze({ + field: candidate.field, + phrase: term.phrase, + value: candidate.value, + })) + ); +} + +export function requireCleanPublicVocabulary(contract: JsonObject): void { + const violations = findForbiddenPublicVocabulary(contract); + if (violations.length === 0) { + return; + } + const summary = violations.map( + (violation) => `${violation.field}: ${violation.phrase}`, + ).join(', '); + throw new CapabilityContractError( + `v19 public vocabulary contains legacy terms: ${summary}`, + ); +} + +function publicText( + contract: z.infer, +): readonly PublicText[] { + return [ + { field: 'moduleSummary', value: contract.moduleSummary }, + { field: 'sdkSummary', value: contract.sdkSummary }, + ...contract.nouns.flatMap((noun) => [ + { field: `nouns.${noun.name}.name`, value: noun.name }, + { field: `nouns.${noun.name}.summary`, value: noun.summary }, + ]), + ...contract.cli.flatMap((capability) => [ + { field: `cli.${capability.command}.command`, value: capability.command }, + { field: `cli.${capability.command}.summary`, value: capability.summary }, + { field: `cli.${capability.command}.usage`, value: capability.usage }, + ]), + ...contract.mcp.flatMap((capability) => [ + { field: `mcp.${capability.name}.name`, value: capability.name }, + { + field: `mcp.${capability.name}.description`, + value: capability.description, + }, + ]), + ]; +} diff --git a/scripts/V19VocabularyContract.ts b/scripts/V19VocabularyContract.ts new file mode 100644 index 00000000..e7be876a --- /dev/null +++ b/scripts/V19VocabularyContract.ts @@ -0,0 +1,299 @@ +import { readFileSync } from 'node:fs'; +import { z } from 'zod'; + +import { + CapabilityContractError, + parseCapabilityMetadata, + type CapabilityMetadata, +} from './V19CapabilityMetadata.ts'; + +export { CapabilityContractError } from './V19CapabilityMetadata.ts'; +export type JsonValue = + | null + | boolean + | number + | string + | readonly JsonValue[] + | { readonly [key: string]: JsonValue }; + +export type JsonObject = { readonly [key: string]: JsonValue }; + +type OrderedEntry = { + readonly order: number; + readonly payload: JsonObject; +}; + +const JSON_VALUE_SCHEMA: z.ZodType = z.lazy(() => + z.union([ + z.null(), + z.boolean(), + z.number(), + z.string(), + z.array(JSON_VALUE_SCHEMA), + z.record(z.string(), JSON_VALUE_SCHEMA), + ]), +); + +const TYPE_REFERENCE_SCHEMA = z.object({ + base: z.string().min(1), + nullable: z.boolean(), + isList: z.boolean(), + listItemNullable: z.boolean().optional(), +}); + +const ARGUMENT_SCHEMA = z.object({ + name: z.string().min(1), + type: TYPE_REFERENCE_SCHEMA, + directives: z.record(z.string(), JSON_VALUE_SCHEMA), +}); + +const FIELD_SCHEMA = z.object({ + name: z.string().min(1), + type: TYPE_REFERENCE_SCHEMA, + arguments: z.array(ARGUMENT_SCHEMA).default([]), + directives: z.record(z.string(), JSON_VALUE_SCHEMA), +}); + +const LOWERED_TYPE_SCHEMA = z.object({ + name: z.string().min(1), + kind: z.string().min(1), + directives: z.record(z.string(), JSON_VALUE_SCHEMA), + fields: z.array(FIELD_SCHEMA).default([]), + enumValues: z.array(z.string()).optional(), +}); + +const LOWERED_SCHEMA = z.object({ + version: z.string().min(1), + types: z.array(LOWERED_TYPE_SCHEMA), +}); + +const REGISTRY_SCHEMA = z.object({ + version: z.literal('git-warp.capabilities/v19'), + moduleSummary: z.string().min(1), + sdkSummary: z.string().min(1), + formalIdentifiers: z.array(z.string().min(1)), + exceptionPaths: z.array(z.string().min(1)), +}); + +export const NOUN_SCHEMA = z.object({ + name: z.string().min(1), + summary: z.string().min(1), +}); + +type CapabilityField = Readonly<{ + readonly field: z.infer; + readonly metadata: CapabilityMetadata; +}>; + +const FORBIDDEN_SCHEMA = z.object({ + phrase: z.string().min(1), + scopes: z.array(z.enum(['ROOT_DECLARATION', 'PUBLIC_SURFACE'])).min(1), +}); + +const RANGE_SCHEMA = z.object({ + minimum: z.number().int(), + maximum: z.number().int(), +}); + +const ALLOWED_VALUES_SCHEMA = z.object({ + values: z.array(z.string().min(1)).min(1), +}); + +function requiredType( + types: z.infer[], + name: string, +): z.infer { + const found = types.find((type) => type.name === name); + if (found === undefined) { + throw new CapabilityContractError(`Wesley IR is missing ${name}`); + } + return found; +} + +function requiredMetadata( + value: T | undefined, + field: string, + metadata: string, +): T { + if (value === undefined) { + throw new CapabilityContractError( + `${field} is missing required ${metadata} metadata`, + ); + } + return value; +} + +function argumentJsonSchema( + argument: z.infer, +): JsonObject { + if (argument.type.base === 'JsonObject') { + return { type: 'object' }; + } + if (argument.type.base === 'String') { + const allowedValues = argument.directives['allowedValues']; + if (allowedValues !== undefined) { + return { + type: 'string', + enum: ALLOWED_VALUES_SCHEMA.parse(allowedValues).values, + }; + } + return { type: 'string', minLength: 1 }; + } + if (argument.type.base === 'Int') { + const range = argument.directives['range']; + if (range === undefined) { + return { type: 'integer' }; + } + const bounds = RANGE_SCHEMA.parse(range); + return { + type: 'integer', + minimum: bounds.minimum, + maximum: bounds.maximum, + }; + } + throw new CapabilityContractError( + `${argument.name} uses unsupported MCP input type ${argument.type.base}`, + ); +} + +function mcpInputSchema( + field: z.infer, +): JsonObject { + const properties = Object.fromEntries( + field.arguments.map((argument) => [ + argument.name, + argumentJsonSchema(argument), + ]), + ); + return { + type: 'object', + properties, + required: field.arguments + .filter((argument) => !argument.type.nullable) + .map((argument) => argument.name), + additionalProperties: false, + }; +} + +function orderedCli( + capability: CapabilityField, +): OrderedEntry | null { + const { field, metadata } = capability; + if (metadata.cliOrder === undefined) { + return null; + } + return { + order: metadata.cliOrder, + payload: { + command: requiredMetadata( + metadata.cliCommand, + field.name, + 'cliCommand', + ), + summary: requiredMetadata( + metadata.cliSummary, + field.name, + 'cliSummary', + ), + usage: requiredMetadata(metadata.cliUsage, field.name, 'cliUsage'), + }, + }; +} + +function orderedMcp( + capability: CapabilityField, +): OrderedEntry | null { + const { field, metadata } = capability; + if (metadata.mcpOrder === undefined) { + return null; + } + return { + order: metadata.mcpOrder, + payload: { + name: requiredMetadata(metadata.mcpName, field.name, 'mcpName'), + description: requiredMetadata( + metadata.mcpDescription, + field.name, + 'mcpDescription', + ), + inputSchema: mcpInputSchema(field), + }, + }; +} + +function orderedEntries( + fields: CapabilityField[], + project: (field: CapabilityField) => OrderedEntry | null, + kind: string, +): JsonObject[] { + const entries = fields.flatMap((field) => { + const entry = project(field); + return entry === null ? [] : [entry]; + }); + entries.sort((left, right) => left.order - right.order); + return entries.map((entry, index) => { + if (entry.order !== index + 1) { + throw new CapabilityContractError( + `${kind} order must be contiguous from one`, + ); + } + return entry.payload; + }); +} + +function capabilityField(field: z.infer): CapabilityField { + return Object.freeze({ + field, + metadata: parseCapabilityMetadata( + field.name, + field.directives['capability'], + ), + }); +} + +function requireUnique( + entries: JsonObject[], + field: 'command' | 'name', +): void { + const values = entries.map((entry) => + z.string().parse(entry[field]) + ); + const duplicates = values.filter( + (value, index) => values.indexOf(value) !== index, + ); + if (duplicates.length > 0) { + throw new CapabilityContractError( + `${field} values must be unique: ${[...new Set(duplicates)].join(', ')}`, + ); + } +} + +export function readV19VocabularyContract(irPath: string): JsonObject { + const lowered = LOWERED_SCHEMA.parse( + JSON.parse(readFileSync(irPath, 'utf8')), + ); + const vocabulary = requiredType(lowered.types, 'PublicVocabulary'); + const registry = REGISTRY_SCHEMA.parse(vocabulary.directives['registry']); + const capabilities = requiredType(lowered.types, 'PublicCapabilities'); + const rejected = requiredType(lowered.types, 'RejectedVocabulary'); + const capabilityFields = capabilities.fields.map(capabilityField); + const cli = orderedEntries(capabilityFields, orderedCli, 'CLI'); + const mcp = orderedEntries(capabilityFields, orderedMcp, 'MCP'); + requireUnique(cli, 'command'); + requireUnique(mcp, 'name'); + return { + version: registry.version, + moduleSummary: registry.moduleSummary, + sdkSummary: registry.sdkSummary, + formalIdentifiers: registry.formalIdentifiers, + exceptionPaths: registry.exceptionPaths, + nouns: vocabulary.fields.map((field) => + NOUN_SCHEMA.parse(field.directives['noun']) + ), + forbiddenTerms: rejected.fields.map((field) => + FORBIDDEN_SCHEMA.parse(field.directives['forbidden']) + ), + cli, + mcp, + }; +} diff --git a/scripts/V19VocabularyMatching.ts b/scripts/V19VocabularyMatching.ts new file mode 100644 index 00000000..eb30bf87 --- /dev/null +++ b/scripts/V19VocabularyMatching.ts @@ -0,0 +1,39 @@ +export function containsVocabularyPhrase( + candidate: readonly string[], + forbidden: readonly string[], +): boolean { + for ( + let start = 0; + start + forbidden.length <= candidate.length; + start += 1 + ) { + if (forbidden.every( + (token, offset) => vocabularyTokenMatches( + candidate[start + offset], + token, + ), + )) { + return true; + } + } + return false; +} + +export function vocabularyTokens(value: string): readonly string[] { + return value + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .split(/[^A-Za-z0-9]+/) + .filter((token) => token.length > 0) + .map((token) => token.toLowerCase()); +} + +function vocabularyTokenMatches( + candidate: string | undefined, + forbidden: string, +): boolean { + return candidate === forbidden + || ( + candidate?.endsWith('s') === true + && candidate.slice(0, -1) === forbidden + ); +} diff --git a/scripts/check-source-backed-reference.ts b/scripts/check-source-backed-reference.ts index d1e9f1f1..631095c6 100644 --- a/scripts/check-source-backed-reference.ts +++ b/scripts/check-source-backed-reference.ts @@ -239,7 +239,6 @@ function generate(): string { const registrySource = new SourceText('bin/cli/commands/registry.ts'); const cliSource = new SourceText('bin/git-warp.ts'); const rootSource = new SourceText('index.ts'); - const storageSource = new SourceText('storage.ts'); const advancedSource = new SourceText('advanced.ts'); const diagnosticsSource = new SourceText('diagnostics.ts'); const chartsSource = new SourceText('charts.ts'); @@ -266,7 +265,6 @@ function generate(): string { ]), '', ...exportSurface('Root API export surface', rootSource, 'First-use product API: one `Runtime` value plus Lane, Intent, Observer, Observation, Reading, and Receipt types.'), - ...exportSurface('Storage export surface', storageSource, 'Transitional explicit storage composition; first-use applications use `Runtime.open()`.'), ...exportSurface('Advanced export surface', advancedSource, 'Bounded formal reads and runtime-backed construction for generated SDK infrastructure.'), ...exportSurface('Diagnostics export surface', diagnosticsSource, 'Operator inspection helpers that consume public receipt handles.'), ...exportSurface('Charts export surface', chartsSource, 'Bounded graph-shaped derived Observers and Reading values.'), diff --git a/scripts/generated-sdk/RenderUsersSdkFixture.ts b/scripts/generated-sdk/RenderUsersSdkFixture.ts index ae349f15..a0563c71 100644 --- a/scripts/generated-sdk/RenderUsersSdkFixture.ts +++ b/scripts/generated-sdk/RenderUsersSdkFixture.ts @@ -6,6 +6,9 @@ import { queryRoleOfOperation, queryRolesOfOperation, } from '../../test/fixtures/generated-sdk/users.wesley.generated.ts'; +import { + V19_CAPABILITY_CONTRACT, +} from '../../bin/cli/capabilities/V19CapabilityContract.generated.ts'; class GeneratedSdkContractError extends Error { constructor(operation: string) { @@ -96,6 +99,7 @@ function requireRolesOfContract(): void { const SDK_SOURCE = `/* @generated from users.graphql by Wesley and the git-warp SDK fixture renderer. */ +/** ${V19_CAPABILITY_CONTRACT.sdkSummary} */ import type { Intent, Observer, diff --git a/scripts/smoke-packed-artifact.sh b/scripts/smoke-packed-artifact.sh index 7bc83407..7aeb37d8 100755 --- a/scripts/smoke-packed-artifact.sh +++ b/scripts/smoke-packed-artifact.sh @@ -37,7 +37,6 @@ class PackedArtifactSmokeError extends Error { } const mod = await import('@git-stunts/git-warp'); -const storage = await import('@git-stunts/git-warp/storage'); const rootValues = Object.keys(mod).sort(); if (rootValues.length !== 1 || rootValues[0] !== 'Runtime') { @@ -46,14 +45,15 @@ if (rootValues.length !== 1 || rootValues[0] !== 'Runtime') { ); } -for (const name of ['GitStorage']) { - if (!(name in storage)) { - throw new PackedArtifactSmokeError(`storage subpath did not export ${name}`); - } +let storageSubpathImported = false; +try { + await import('@git-stunts/git-warp/storage'); + storageSubpathImported = true; +} catch { + // The v19 export map intentionally hides production storage composition. } - -if ('MemoryStorage' in storage) { - throw new PackedArtifactSmokeError('storage subpath still exported MemoryStorage'); +if (storageSubpathImported) { + throw new PackedArtifactSmokeError('storage subpath remained publicly importable'); } NODE diff --git a/scripts/storage-ownership-policy.ts b/scripts/storage-ownership-policy.ts index c192ede1..46fdd885 100644 --- a/scripts/storage-ownership-policy.ts +++ b/scripts/storage-ownership-policy.ts @@ -1,7 +1,6 @@ export const PRODUCTION_ROOTS = ['src', 'bin'] as const; export const PRODUCTION_ENTRYPOINTS = [ 'index.ts', - 'storage.ts', 'advanced.ts', 'diagnostics.ts', 'charts.ts', diff --git a/scripts/v19-root-declaration-gate.ts b/scripts/v19-root-declaration-gate.ts index 017c1ec4..ddcb9682 100644 --- a/scripts/v19-root-declaration-gate.ts +++ b/scripts/v19-root-declaration-gate.ts @@ -2,29 +2,32 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, relative, resolve } from 'node:path'; import ts from 'typescript'; -const FORBIDDEN_TOKENS = new Set([ - 'blob', - 'cas', - 'commit', - 'git', - 'oid', - 'plumbing', - 'ref', - 'sha', - 'tree', -]); +import { + V19_CAPABILITY_CONTRACT, +} from '../bin/cli/capabilities/V19CapabilityContract.generated.ts'; +import { + containsVocabularyPhrase, + vocabularyTokens, +} from './V19VocabularyMatching.ts'; -const FORBIDDEN_COMPOUNDS = new Map([ - ['objectid', 'object-id'], - ['objectids', 'object-id'], -]); +type ForbiddenPhrase = Readonly<{ + readonly display: string; + readonly tokens: readonly string[]; +}>; -const ALLOWED_FORMAL_IDENTIFIERS = new Set([ - 'WitnessReference', - 'WitnessReferences', - 'witnessRef', - 'witnessRefs', -]); +const FORBIDDEN_PHRASES: readonly ForbiddenPhrase[] = + V19_CAPABILITY_CONTRACT.forbiddenTerms + .filter((term) => term.scopes.some( + (scope) => scope === 'ROOT_DECLARATION', + )) + .map((term) => Object.freeze({ + display: term.phrase.replaceAll(' ', '-'), + tokens: vocabularyTokens(term.phrase), + })); + +const ALLOWED_FORMAL_IDENTIFIERS = new Set( + V19_CAPABILITY_CONTRACT.formalIdentifiers, +); export type DeclarationVocabularyViolation = { readonly file: string; @@ -59,11 +62,7 @@ export function findForbiddenRootDeclarationVocabulary( return; } const tokens = vocabularyTokens(identifier); - for (const rawToken of tokens) { - const token = singularForbiddenToken(rawToken); - if (token === null) { - continue; - } + for (const token of forbiddenMatches(tokens)) { const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); violations.push({ file: relative(declarationRoot, file), @@ -73,19 +72,6 @@ export function findForbiddenRootDeclarationVocabulary( token, }); } - for (let index = 0; index + 1 < tokens.length; index += 1) { - const compound = FORBIDDEN_COMPOUNDS.get(`${tokens[index]}${tokens[index + 1]}`); - if (compound !== undefined) { - const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); - violations.push({ - file: relative(declarationRoot, file), - line: position.line + 1, - column: position.character + 1, - identifier, - token: compound, - }); - } - } } ts.forEachChild(node, visit); }; @@ -95,14 +81,16 @@ export function findForbiddenRootDeclarationVocabulary( return violations; } -function singularForbiddenToken(token: string): string | null { - if (FORBIDDEN_TOKENS.has(token)) { - return token; - } - if (token.endsWith('s') && FORBIDDEN_TOKENS.has(token.slice(0, -1))) { - return token.slice(0, -1); +function forbiddenMatches( + tokens: readonly string[], +): readonly string[] { + const matches: string[] = []; + for (const forbidden of FORBIDDEN_PHRASES) { + if (containsVocabularyPhrase(tokens, forbidden.tokens)) { + matches.push(forbidden.display); + } } - return null; + return matches; } function declarationClosure(entry: string, root: string): readonly string[] { @@ -180,11 +168,3 @@ function resolveDeclarationDependency( } return dependency; } - -function vocabularyTokens(identifier: string): readonly string[] { - return identifier - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .split(/[^A-Za-z0-9]+/) - .filter((token) => token.length > 0) - .map((token) => token.toLowerCase()); -} diff --git a/src/application/ReceiptProvenanceRegistry.ts b/src/application/ReceiptProvenanceRegistry.ts index 214f2978..8c5a4087 100644 --- a/src/application/ReceiptProvenanceRegistry.ts +++ b/src/application/ReceiptProvenanceRegistry.ts @@ -3,11 +3,9 @@ import type { ApiRuntimeContext, ReceiptProvenance } from '../domain/api/ApiRunt import type { Receipt } from '../domain/api/Receipt.ts'; import type CryptoPort from '../ports/CryptoPort.ts'; import type WarpStorage from './WarpStorage.ts'; -import { resolveWarpStorage } from './WarpStorageRegistry.ts'; type ReceiptProvenanceBinding = { readonly provenance: ReceiptProvenance; - readonly storage: WarpStorage; }; type RecoveryNonceState = { @@ -34,7 +32,7 @@ export function createApiRuntimeContext( } RECEIPT_PROVENANCE.set( receipt, - Object.freeze({ storage, provenance: freezeProvenance(provenance) }) + Object.freeze({ provenance: freezeProvenance(provenance) }) ); }, }); @@ -60,24 +58,14 @@ function encodeOpaqueIdPart(part: string | number): string { return `${type}${value.length}:${value}`; } -export function resolveReceiptProvenance( - receipt: Receipt, - storage: WarpStorage -): ReceiptProvenance { - resolveWarpStorage(storage); +export function resolveReceiptProvenance(receipt: Receipt): ReceiptProvenance { const binding = RECEIPT_PROVENANCE.get(receipt); if (binding === undefined) { throw new WarpError( - 'Receipt was not issued by an openWarp runtime', + 'Receipt was not issued by a Runtime', 'E_RECEIPT_PROVENANCE_UNAVAILABLE' ); } - if (binding.storage !== storage) { - throw new WarpError( - 'Receipt does not belong to the supplied storage', - 'E_RECEIPT_STORAGE_MISMATCH' - ); - } return binding.provenance; } diff --git a/storage.ts b/storage.ts deleted file mode 100644 index 534b030c..00000000 --- a/storage.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** Transitional explicit storage constructor. Runtime.open owns this in v19. */ - -export { default as GitStorage } from './src/application/GitStorage.ts'; -export type { GitStorageOptions } from './src/application/GitStorage.ts'; diff --git a/test/conformance/v18FirstUseOpticsHonesty.test.ts b/test/conformance/v18FirstUseOpticsHonesty.test.ts index e9943a28..8e97f9b4 100644 --- a/test/conformance/v18FirstUseOpticsHonesty.test.ts +++ b/test/conformance/v18FirstUseOpticsHonesty.test.ts @@ -178,7 +178,7 @@ describe('v18 first-use optics honesty gate', () => { const property = await timeline.read( reading.property({ subject: NODE_ID, key: PROPERTY_KEY }), ); - const inspection = inspectReceipt(property.receipt, { storage }); + const inspection = inspectReceipt(property.receipt); const basis = await events.prepareOpticBasis(); const coordinate = await events.coordinate(); const node = await coordinate.optic().node(NODE_ID).read(); diff --git a/test/fixtures/generated-sdk/users.generated.ts b/test/fixtures/generated-sdk/users.generated.ts index e54e4f92..25cbbb16 100644 --- a/test/fixtures/generated-sdk/users.generated.ts +++ b/test/fixtures/generated-sdk/users.generated.ts @@ -1,5 +1,6 @@ /* @generated from users.graphql by Wesley and the git-warp SDK fixture renderer. */ +/** Generated SDKs construct validated Intent and Observer values for Runtime Lane workflows. */ import type { Intent, Observer, diff --git a/test/type-check/v19-subpaths.ts b/test/type-check/v19-subpaths.ts index f3610ccd..fad02d9f 100644 --- a/test/type-check/v19-subpaths.ts +++ b/test/type-check/v19-subpaths.ts @@ -1,11 +1,10 @@ /** * v19 explicit subpath consumer fixture -- compile-only. * - * Storage, advanced, charts, diagnostics, and testing imports stay reachable - * only from their named expert surfaces. + * Advanced, charts, diagnostics, and testing imports stay reachable only from + * their named expert surfaces. */ -import { GitStorage, type GitStorageOptions } from '../../storage.ts'; import { type Intent, type Lane, @@ -24,7 +23,6 @@ import { import { graph, GraphNeighborhoodChart, type GraphNeighborhoodOptions } from '../../charts.ts'; import { inspectReceipt, - type InspectReceiptOptions, type ReceiptInspection, type ReceiptSubstrateInspection, } from '../../diagnostics.ts'; @@ -34,9 +32,6 @@ import { type RuntimeHarnessOptions, } from '../../testing.ts'; -declare const gitStorageOptions: GitStorageOptions; - -const gitStorage = await GitStorage.open(gitStorageOptions); declare const lane: Lane; const coordinate: InstanceType = await captureCoordinate(lane); const optic: InstanceType = coordinate.optic(); @@ -58,8 +53,7 @@ const advancedObserver: Observer = createObserver( }, ); declare const receipt: WriteReceipt; -const inspectionOptions: InspectReceiptOptions = { storage: gitStorage }; -const inspection: ReceiptInspection = inspectReceipt(receipt, inspectionOptions); +const inspection: ReceiptInspection = inspectReceipt(receipt); const inspectedLane: string = inspection.lane; const substrate: ReceiptSubstrateInspection = inspection.substrate; const neighborhoodOptions: GraphNeighborhoodOptions = { @@ -73,13 +67,12 @@ const isRuntimeChart: boolean = chart instanceof GraphNeighborhoodChart; const harnessOptions: RuntimeHarnessOptions = { writer: 'agent-1' }; const harness: RuntimeHarness = await createRuntimeHarness(harnessOptions); -// @ts-expect-error diagnostics require explicit storage context. -inspectReceipt(receipt); +// @ts-expect-error receipt inspection accepts no storage composition option. +inspectReceipt(receipt, { storage: null }); // @ts-expect-error diagnostic projections use canonical Lane vocabulary. inspection.timeline; -await gitStorage.close(); await harness.close(); void optic; void witness; diff --git a/test/unit/cli/v19-input-errors.test.ts b/test/unit/cli/v19-input-errors.test.ts index de8f6d1f..a2c951ce 100644 --- a/test/unit/cli/v19-input-errors.test.ts +++ b/test/unit/cli/v19-input-errors.test.ts @@ -9,6 +9,9 @@ import { afterEach, describe, expect, it } from 'vitest'; import handleReceipt from '../../../bin/cli/commands/receipt.ts'; import handleSettle from '../../../bin/cli/commands/settle.ts'; +import { + V19_PUBLIC_NOUNS, +} from '../../../bin/cli/capabilities/V19CapabilityContract.generated.ts'; import type { CliOptions } from '../../../bin/cli/types.ts'; import { intentFromText, @@ -44,13 +47,17 @@ describe('v19 CLI input errors', () => { expect(() => intentFromText('{')).toThrowError( expect.objectContaining({ code: 'E_USAGE', - message: expect.stringContaining('Invalid Intent JSON'), + message: expect.stringContaining( + `Invalid ${V19_PUBLIC_NOUNS.Intent} JSON`, + ), }), ); expect(() => observerFromText('users.role', '{')).toThrowError( expect.objectContaining({ code: 'E_USAGE', - message: expect.stringContaining('Invalid Observer JSON'), + message: expect.stringContaining( + `Invalid ${V19_PUBLIC_NOUNS.Observer} JSON`, + ), }), ); }); diff --git a/test/unit/domain/ReceiptDiagnostics.test.ts b/test/unit/domain/ReceiptDiagnostics.test.ts index 7271d6d9..e013b819 100644 --- a/test/unit/domain/ReceiptDiagnostics.test.ts +++ b/test/unit/domain/ReceiptDiagnostics.test.ts @@ -44,13 +44,13 @@ describe('receipt diagnostics', () => { expect(reopenedNonce).not.toBe(firstNonce); }); - it('recovers exact write provenance only with explicit storage context', async () => { + it('recovers exact write provenance from the Receipt identity', async () => { const storage = MemoryStorage.create(); const warp = await openWarp({ storage, writer: 'agent-1' }); const timeline = await warp.timeline('events'); const receipt = await timeline.write(intent.node.add({ subject: 'user:alice' })); - const inspection = inspectReceipt(receipt, { storage }); + const inspection = inspectReceipt(receipt); expect(inspection).toMatchObject({ operation: 'write', @@ -76,7 +76,7 @@ describe('receipt diagnostics', () => { const timeline = await warp.timeline('events'); const receipt = await timeline.write(intent.node.remove({ subject: 'user:alice' })); - const inspection = inspectReceipt(receipt, { storage }); + const inspection = inspectReceipt(receipt); expect(receipt.outcome.kind).toBe('obstruction'); expect(inspection.objectIds).toEqual([]); @@ -95,7 +95,7 @@ describe('receipt diagnostics', () => { ); const result = await timeline.read(reading.property({ subject: 'user:alice', key: 'role' })); - const inspection = inspectReceipt(result.receipt, { storage }); + const inspection = inspectReceipt(result.receipt); expect(result.receipt.outcome).toBe('accepted'); expect(result.receipt.evidence).not.toHaveProperty('checkpointSha'); @@ -119,7 +119,7 @@ describe('receipt diagnostics', () => { ); const preview = await timeline.previewJoin(draft); - const inspection = inspectReceipt(preview.receipt, { storage }); + const inspection = inspectReceipt(preview.receipt); expect(preview.receipt.evidence?.support).toContainEqual(draftWrite.evidence?.support[0]); expect('patchShas' in preview.receipt).toBe(false); @@ -132,31 +132,35 @@ describe('receipt diagnostics', () => { expect(inspection.objectIds).toEqual(inspection.substrate.patchShas); }); - it('rejects diagnostics requests made with a different storage handle', async () => { + it('keeps Receipt provenance available after storage closes', async () => { const storage = MemoryStorage.create(); - const otherStorage = MemoryStorage.create(); const warp = await openWarp({ storage, writer: 'agent-1' }); const timeline = await warp.timeline('events'); const receipt = await timeline.write(intent.node.add({ subject: 'user:alice' })); - expect(() => inspectReceipt(receipt, { storage: otherStorage })).toThrow( - 'Receipt does not belong to the supplied storage' - ); + await storage.close(); + + expect(inspectReceipt(receipt)).toMatchObject({ + operation: 'write', + lane: 'events', + writer: 'agent-1', + }); }); - it('requires an explicit diagnostics storage context', async () => { + it('returns frozen diagnostic projections', async () => { const storage = MemoryStorage.create(); const warp = await openWarp({ storage, writer: 'agent-1' }); const timeline = await warp.timeline('events'); const receipt = await timeline.write(intent.node.add({ subject: 'user:alice' })); - expect(() => inspectReceipt(receipt, undefined as never)).toThrow( - 'Receipt inspection requires an explicit storage context' - ); + const inspection = inspectReceipt(receipt); + + expect(Object.isFrozen(inspection)).toBe(true); + expect(Object.isFrozen(inspection.objectIds)).toBe(true); + expect(Object.isFrozen(inspection.substrate)).toBe(true); }); - it('rejects receipts that were not issued by an openWarp runtime', () => { - const storage = MemoryStorage.create(); + it('rejects receipts that were not issued by a Runtime', () => { const receipt = new WriteReceipt({ lane: 'events', writer: 'agent-1', @@ -168,9 +172,7 @@ describe('receipt diagnostics', () => { evidence: RECOVERY_EVIDENCE, }); - expect(() => inspectReceipt(receipt, { storage })).toThrow( - 'Receipt was not issued by an openWarp runtime' - ); + expect(() => inspectReceipt(receipt)).toThrow('Receipt was not issued by a Runtime'); }); it('binds raw receipt provenance exactly once', () => { diff --git a/test/unit/scripts/v19-public-api-boundary.test.ts b/test/unit/scripts/v19-public-api-boundary.test.ts index 06e68d4b..ce3d81a8 100644 --- a/test/unit/scripts/v19-public-api-boundary.test.ts +++ b/test/unit/scripts/v19-public-api-boundary.test.ts @@ -194,13 +194,6 @@ describe('v19 public API boundary', () => { expect(moduleSurface().typeExports).toEqual(sorted(ROOT_TYPE_EXPORTS)); }); - it('keeps the storage subpath limited to application adapters', () => { - const surface = moduleSurface('storage.ts'); - expect(surface.starExports).toEqual([]); - expect(surface.valueExports).toEqual(['GitStorage']); - expect(surface.typeExports).toEqual(['GitStorageOptions']); - }); - it('keeps the advanced subpath limited to bounded reads and SDK construction', () => { const surface = moduleSurface('advanced.ts'); expect(surface.starExports).toEqual([]); @@ -236,7 +229,7 @@ describe('v19 public API boundary', () => { expect(surface.starExports).toEqual([]); expect(surface.valueExports).toEqual(['inspectReceipt']); expect(surface.typeExports).toEqual( - sorted(['InspectReceiptOptions', 'ReceiptInspection', 'ReceiptSubstrateInspection']) + sorted(['ReceiptInspection', 'ReceiptSubstrateInspection']) ); }); @@ -274,7 +267,6 @@ describe('v19 public API boundary', () => { './charts', './diagnostics', './package.json', - './storage', './testing', ]); expect(packageExportNames('jsr.json')).toEqual([ @@ -282,7 +274,6 @@ describe('v19 public API boundary', () => { './advanced', './charts', './diagnostics', - './storage', './testing', ]); }); diff --git a/test/unit/scripts/v19-root-declaration-gate.test.ts b/test/unit/scripts/v19-root-declaration-gate.test.ts index f39b8517..35f94c5f 100644 --- a/test/unit/scripts/v19-root-declaration-gate.test.ts +++ b/test/unit/scripts/v19-root-declaration-gate.test.ts @@ -4,6 +4,9 @@ import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { findForbiddenRootDeclarationVocabulary } from '../../../scripts/v19-root-declaration-gate.ts'; +import { + V19_CAPABILITY_CONTRACT, +} from '../../../bin/cli/capabilities/V19CapabilityContract.generated.ts'; const temporaryDirectories: string[] = []; @@ -14,6 +17,35 @@ afterEach(() => { }); describe('v19 root declaration vocabulary gate', () => { + it('enforces every root term from the generated registry', () => { + const directory = mkdtempSync(join(tmpdir(), 'git-warp-v19-dts-')); + temporaryDirectories.push(directory); + const rootTerms = V19_CAPABILITY_CONTRACT.forbiddenTerms + .filter((term) => term.scopes.some( + (scope) => scope === 'ROOT_DECLARATION', + )); + writeFileSync( + join(directory, 'index.d.ts'), + [ + 'export type Leaks = {', + ...rootTerms.map((term) => + ` readonly leaked${pascalIdentifier(term.phrase)}: string;` + ), + '};', + '', + ].join('\n'), + ); + + const found = new Set( + findForbiddenRootDeclarationVocabulary( + join(directory, 'index.d.ts'), + ).map((violation) => violation.token), + ); + expect(found).toEqual(new Set( + rootTerms.map((term) => term.phrase.replaceAll(' ', '-')), + )); + }); + it('walks transitive declaration imports and reports substrate identifiers', () => { const directory = mkdtempSync(join(tmpdir(), 'git-warp-v19-dts-')); temporaryDirectories.push(directory); @@ -93,3 +125,10 @@ describe('v19 root declaration vocabulary gate', () => { ]); }); }); + +function pascalIdentifier(phrase: string): string { + return phrase + .split(' ') + .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) + .join(''); +} diff --git a/test/unit/scripts/v19-vocabulary-contract.test.ts b/test/unit/scripts/v19-vocabulary-contract.test.ts new file mode 100644 index 00000000..31b01077 --- /dev/null +++ b/test/unit/scripts/v19-vocabulary-contract.test.ts @@ -0,0 +1,254 @@ +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import ts from 'typescript'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { + V19_CAPABILITY_CONTRACT, +} from '../../../bin/cli/capabilities/V19CapabilityContract.generated.ts'; +import { + findForbiddenPublicVocabulary, +} from '../../../scripts/V19VocabularyConformance.ts'; +import { + emitV19VocabularyArtifacts, +} from '../../../scripts/V19VocabularyArtifacts.ts'; +import type { + JsonObject, +} from '../../../scripts/V19VocabularyContract.ts'; +import { + readV19VocabularyContract, +} from '../../../scripts/V19VocabularyContract.ts'; +import { + containsVocabularyPhrase, + vocabularyTokens, +} from '../../../scripts/V19VocabularyMatching.ts'; +import { COMMANDS } from '../../../bin/cli/commands/registry.ts'; +import { + listMcpTools, +} from '../../../bin/cli/commands/mcp/McpToolCatalog.ts'; + +const ROOT = resolve(import.meta.dirname, '../../..'); +const VOCABULARY_IR = resolve( + ROOT, + 'schemas/v19-public-vocabulary.wesley.generated.json', +); +const temporaryDirectories: string[] = []; +const CANONICAL_NOUNS = [ + 'Runtime', + 'Lane', + 'Intent', + 'Observer', + 'Observation', + 'Reading', + 'Receipt', + 'Settlement', +] as const; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +describe('generated v19 vocabulary contract', () => { + it('publishes the accepted registry identity and noun order', () => { + expect(V19_CAPABILITY_CONTRACT.version) + .toBe('git-warp.capabilities/v19'); + expect(V19_CAPABILITY_CONTRACT.nouns.map((noun) => noun.name)) + .toEqual(CANONICAL_NOUNS); + }); + + it('keeps the TypeDoc module summary bound to the registry', () => { + const source = readFileSync(resolve(ROOT, 'index.ts'), 'utf8'); + const sourceFile = ts.createSourceFile( + 'index.ts', + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + const comments = ts.getLeadingCommentRanges( + source, + sourceFile.getFullStart(), + ) ?? []; + const moduleDocumentation = comments + .map((comment) => source.slice(comment.pos, comment.end)) + .join('\n'); + expect(moduleDocumentation) + .toContain(V19_CAPABILITY_CONTRACT.moduleSummary); + }); + + it('drives CLI and MCP capability names in registry order', () => { + expect([...COMMANDS.keys()]) + .toEqual(V19_CAPABILITY_CONTRACT.cli.map((entry) => entry.command)); + expect(listMcpTools().map((tool) => tool.name)) + .toEqual(V19_CAPABILITY_CONTRACT.mcp.map((entry) => entry.name)); + }); + + it('keeps JSON and TypeScript artifacts byte-semantically equal', () => { + const json = JSON.parse(readFileSync( + resolve(ROOT, 'bin/cli/capabilities/v19-capabilities.json'), + 'utf8', + )); + expect(json).toEqual(V19_CAPABILITY_CONTRACT); + }); + + it('uses a project-scoped allowed-values directive', () => { + const schema = readFileSync( + resolve(ROOT, 'schemas/v19-public-vocabulary.graphql'), + 'utf8', + ); + expect(schema).toContain('directive @allowedValues('); + expect(schema).not.toContain('directive @oneOf('); + }); + + it('identifies the exact missing generated artifact', () => { + const root = makeTemporaryDirectory(); + const expectedPath = resolve( + root, + 'bin/cli/capabilities/v19-capabilities.json', + ); + expect(() => emitV19VocabularyArtifacts({ + root, + contract: readV19VocabularyContract(VOCABULARY_IR), + mode: 'check', + })).toThrowError(expect.objectContaining({ + message: expect.stringContaining(expectedPath), + })); + }); + + it('names capability fields with missing metadata', () => { + const lowered = JSON.parse( + readFileSync(VOCABULARY_IR, 'utf8'), + ) as LoweredVocabularyFixture; + const capabilities = lowered.types.find( + (type) => type.name === 'PublicCapabilities', + ); + const field = capabilities?.fields[0]; + expect(field).toBeDefined(); + if (field === undefined) { + throw new Error('PublicCapabilities requires at least one field'); + } + delete field.directives['capability']; + const path = join(makeTemporaryDirectory(), 'missing-capability.json'); + writeFileSync(path, JSON.stringify(lowered)); + + expect(() => readV19VocabularyContract(path)).toThrowError( + expect.objectContaining({ + message: expect.stringContaining( + `${field.name} is missing required capability metadata`, + ), + }), + ); + }); + + it('shares camel-case and plural-tolerant vocabulary matching', () => { + const candidate = vocabularyTokens('legacyGraphStores'); + expect(candidate).toEqual(['legacy', 'graph', 'stores']); + expect(containsVocabularyPhrase( + candidate, + vocabularyTokens('graph store'), + )).toBe(true); + expect(containsVocabularyPhrase( + candidate, + vocabularyTokens('graph storage'), + )).toBe(false); + }); + + it('generates glossary and SDK documentation from registry summaries', () => { + const glossary = readFileSync( + resolve(ROOT, 'docs/topics/vocabulary.generated.md'), + 'utf8', + ); + const sdk = readFileSync( + resolve(ROOT, 'test/fixtures/generated-sdk/users.generated.ts'), + 'utf8', + ); + expect(glossary).toContain(V19_CAPABILITY_CONTRACT.moduleSummary); + for (const noun of V19_CAPABILITY_CONTRACT.nouns) { + expect(glossary).toContain(`| ${noun.name} | ${noun.summary} |`); + } + expect(sdk).toContain(V19_CAPABILITY_CONTRACT.sdkSummary); + }); + + it('keeps normative docs aligned on every canonical noun', () => { + const paths = [ + 'README.md', + 'docs/migrations/v19/README.md', + 'docs/topics/api/README.md', + 'docs/topics/reference.md', + ]; + for (const path of paths) { + const text = readFileSync(resolve(ROOT, path), 'utf8'); + for (const noun of CANONICAL_NOUNS) { + expect(text.toLowerCase(), `${path} must name ${noun}`) + .toContain(noun.toLowerCase()); + } + } + }); + + it('limits legacy exceptions to existing explicit boundaries', () => { + expect(V19_CAPABILITY_CONTRACT.exceptionPaths).not.toEqual([]); + for (const path of V19_CAPABILITY_CONTRACT.exceptionPaths) { + expect(existsSync(resolve(ROOT, path)), path).toBe(true); + } + expect(V19_CAPABILITY_CONTRACT.exceptionPaths) + .not.toContain('src/domain/'); + expect(V19_CAPABILITY_CONTRACT.exceptionPaths) + .not.toContain('bin/'); + }); + + it('keeps generated public metadata free of legacy vocabulary', () => { + expect(findForbiddenPublicVocabulary( + V19_CAPABILITY_CONTRACT, + )).toEqual([]); + }); + + it('enforces every public-surface term from the generated registry', () => { + const artifact = readFileSync( + resolve(ROOT, 'bin/cli/capabilities/v19-capabilities.json'), + 'utf8', + ); + const publicTerms = V19_CAPABILITY_CONTRACT.forbiddenTerms.filter( + (term) => term.scopes.some( + (scope) => scope === 'PUBLIC_SURFACE', + ), + ); + for (const term of publicTerms) { + const parsed = JSON.parse(artifact) as JsonObject; + const contract: JsonObject = { + ...parsed, + moduleSummary: `Legacy ${term.phrase} surface`, + }; + expect(findForbiddenPublicVocabulary(contract)).toEqual([ + expect.objectContaining({ + field: 'moduleSummary', + phrase: term.phrase, + }), + ]); + } + }); +}); + +type LoweredVocabularyFixture = { + readonly types: Array<{ + readonly name: string; + readonly fields: Array<{ + readonly name: string; + readonly directives: Record; + }>; + }>; +}; + +function makeTemporaryDirectory(): string { + const directory = mkdtempSync(join(tmpdir(), 'warp-vocabulary-')); + temporaryDirectories.push(directory); + return directory; +} diff --git a/tsconfig.publish.json b/tsconfig.publish.json index 05954ab9..fbb02ae6 100644 --- a/tsconfig.publish.json +++ b/tsconfig.publish.json @@ -10,7 +10,6 @@ }, "include": [ "index.ts", - "storage.ts", "advanced.ts", "diagnostics.ts", "charts.ts", diff --git a/tsconfig.src.json b/tsconfig.src.json index f97281bb..d3588115 100644 --- a/tsconfig.src.json +++ b/tsconfig.src.json @@ -2,7 +2,6 @@ "extends": "./tsconfig.base.json", "include": [ "index.ts", - "storage.ts", "advanced.ts", "diagnostics.ts", "charts.ts",