@intentius/chant-lexicon-temporal 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/integrity.json +15 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +26 -0
- package/dist/rules/tmp001.ts +53 -0
- package/dist/rules/tmp002.ts +45 -0
- package/dist/rules/tmp010-cron-syntax.ts +64 -0
- package/dist/rules/tmp011-namespace-reference.ts +49 -0
- package/dist/skills/chant-temporal-ops.md +184 -0
- package/dist/skills/chant-temporal.md +201 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +32 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +23 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate.ts +82 -0
- package/src/codegen/package-cli.ts +14 -0
- package/src/codegen/package.ts +55 -0
- package/src/composites/cloud-stack.ts +74 -0
- package/src/composites/composites.test.ts +131 -0
- package/src/composites/dev-stack.ts +81 -0
- package/src/config.ts +150 -0
- package/src/coverage.test.ts +9 -0
- package/src/coverage.ts +37 -0
- package/src/example.test.ts +59 -0
- package/src/generated/lexicon-temporal.json +26 -0
- package/src/index.ts +29 -0
- package/src/lint/post-synth/post-synth.test.ts +152 -0
- package/src/lint/post-synth/tmp010-cron-syntax.ts +64 -0
- package/src/lint/post-synth/tmp011-namespace-reference.ts +49 -0
- package/src/lint/rules/index.ts +2 -0
- package/src/lint/rules/lint-rules.test.ts +150 -0
- package/src/lint/rules/tmp001.ts +53 -0
- package/src/lint/rules/tmp002.ts +45 -0
- package/src/plugin.test.ts +97 -0
- package/src/plugin.ts +286 -0
- package/src/resources.ts +121 -0
- package/src/serializer.test.ts +292 -0
- package/src/serializer.ts +310 -0
- package/src/skills/chant-temporal-ops.md +184 -0
- package/src/skills/chant-temporal.md +201 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate.test.ts +9 -0
- package/src/validate.ts +92 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-temporal-ops
|
|
3
|
+
description: Signal workflows, diagnose stuck activities, reset checkpoints, and cancel runs via the Temporal CLI and chant run
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Temporal Operations Playbook
|
|
8
|
+
|
|
9
|
+
## Signal a gate (unblock a paused workflow)
|
|
10
|
+
|
|
11
|
+
Workflows that use `setHandler` on a signal pause at gate activities waiting for a named signal. The `chant run signal` command (available in issue #8) forwards signals to the running workflow.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Via chant CLI (requires chant run — issue #8)
|
|
15
|
+
chant run signal <op-name> dnsConfigured
|
|
16
|
+
|
|
17
|
+
# Directly via temporal CLI
|
|
18
|
+
temporal workflow signal \
|
|
19
|
+
--workflow-id <workflow-id> \
|
|
20
|
+
--name dnsConfigured \
|
|
21
|
+
--namespace <namespace>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
List pending signals by querying the workflow:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
temporal workflow query \
|
|
28
|
+
--workflow-id <workflow-id> \
|
|
29
|
+
--type currentPhase \
|
|
30
|
+
--namespace <namespace>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Check run status
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Summary view
|
|
37
|
+
temporal workflow describe --workflow-id <id> --namespace <ns>
|
|
38
|
+
|
|
39
|
+
# Full event history
|
|
40
|
+
temporal workflow show --workflow-id <id> --namespace <ns>
|
|
41
|
+
|
|
42
|
+
# Filter by search attribute (requires registered custom attributes)
|
|
43
|
+
temporal workflow list \
|
|
44
|
+
--namespace <ns> \
|
|
45
|
+
--query 'GcpProject = "my-project"'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Diagnose a stuck activity
|
|
49
|
+
|
|
50
|
+
Activities can be stuck for three distinct reasons:
|
|
51
|
+
|
|
52
|
+
| Symptom | Cause | Fix |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Activity never started | `scheduleToStartTimeout` exceeded — no available workers | Start the worker: `chant run <op>` |
|
|
55
|
+
| Activity started but no heartbeats | `heartbeatTimeout` exceeded — worker crashed mid-activity | Bounce the worker; Temporal auto-retries |
|
|
56
|
+
| Activity running but slow | Normal — long-running activities with heartbeats | Wait, or check heartbeat details |
|
|
57
|
+
|
|
58
|
+
### View activity timeout details
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep -A5 "ActivityTaskScheduled"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Check worker connectivity
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# See which task queues have pollers
|
|
68
|
+
temporal task-queue describe --task-queue <queue> --namespace <ns>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A task queue with `pollerCount: 0` means no workers are running.
|
|
72
|
+
|
|
73
|
+
## Reset a workflow to a previous checkpoint
|
|
74
|
+
|
|
75
|
+
Use `workflow reset` to replay a workflow from a specific event, skipping failed activities:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Reset to just before the most recent failure
|
|
79
|
+
temporal workflow reset \
|
|
80
|
+
--workflow-id <id> \
|
|
81
|
+
--namespace <ns> \
|
|
82
|
+
--event-id <N> \
|
|
83
|
+
--reason "Retrying after infra fix"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Find the event ID to reset to:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# List events — find the last successful ActivityTaskCompleted before the failure
|
|
90
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep -n "ActivityTask"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reset to the beginning of a named phase (requires workflow to record phase transitions as signals or markers):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
temporal workflow reset \
|
|
97
|
+
--workflow-id <id> \
|
|
98
|
+
--namespace <ns> \
|
|
99
|
+
--reapply-type None \
|
|
100
|
+
--type LastWorkflowTask
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Cancel a stuck or unwanted run
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Graceful cancel — workflow receives CancellationError and can clean up
|
|
107
|
+
temporal workflow cancel \
|
|
108
|
+
--workflow-id <id> \
|
|
109
|
+
--namespace <ns>
|
|
110
|
+
|
|
111
|
+
# Forceful terminate — immediate stop, no cleanup
|
|
112
|
+
temporal workflow terminate \
|
|
113
|
+
--workflow-id <id> \
|
|
114
|
+
--namespace <ns> \
|
|
115
|
+
--reason "Terminated by operator"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Pause and resume a schedule
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Pause
|
|
122
|
+
temporal schedule pause \
|
|
123
|
+
--schedule-id <id> \
|
|
124
|
+
--namespace <ns> \
|
|
125
|
+
--note "Paused for maintenance"
|
|
126
|
+
|
|
127
|
+
# Resume
|
|
128
|
+
temporal schedule unpause \
|
|
129
|
+
--schedule-id <id> \
|
|
130
|
+
--namespace <ns>
|
|
131
|
+
|
|
132
|
+
# Trigger immediately (ignores spec)
|
|
133
|
+
temporal schedule trigger \
|
|
134
|
+
--schedule-id <id> \
|
|
135
|
+
--namespace <ns>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Inspect workflow history for debugging
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Show all events in JSON (machine-readable)
|
|
142
|
+
temporal workflow show \
|
|
143
|
+
--workflow-id <id> \
|
|
144
|
+
--namespace <ns> \
|
|
145
|
+
--output json
|
|
146
|
+
|
|
147
|
+
# Filter to activity failures only
|
|
148
|
+
temporal workflow show \
|
|
149
|
+
--workflow-id <id> \
|
|
150
|
+
--namespace <ns> \
|
|
151
|
+
--output json | jq '.[] | select(.eventType == "ActivityTaskFailed")'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Common failure patterns
|
|
155
|
+
|
|
156
|
+
### "no workers polling" after `chant run`
|
|
157
|
+
|
|
158
|
+
The worker started but cannot connect. Check:
|
|
159
|
+
1. `TEMPORAL_ADDRESS` matches the server address
|
|
160
|
+
2. `TEMPORAL_NAMESPACE` matches the namespace the workflow was started in
|
|
161
|
+
3. TLS and API key config match (`tls: true` + `apiKey` for Temporal Cloud)
|
|
162
|
+
|
|
163
|
+
### Activity retrying indefinitely
|
|
164
|
+
|
|
165
|
+
Default retry policy has `maximumAttempts: 0` (unlimited). If an activity is retrying unexpectedly:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Check the current attempt count and last failure
|
|
169
|
+
temporal workflow show --workflow-id <id> --namespace <ns> | grep "attempt\|failure"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Add `maximumAttempts` to the activity's retry policy in the workflow code, or cancel the run.
|
|
173
|
+
|
|
174
|
+
### "workflow execution already started" on re-run
|
|
175
|
+
|
|
176
|
+
`chant run` uses deterministic workflow IDs (e.g. `crdb-deploy-{project}`). If a previous run is still open:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Check if it's still running
|
|
180
|
+
temporal workflow describe --workflow-id <id> --namespace <ns> | grep "status"
|
|
181
|
+
|
|
182
|
+
# If stuck, terminate it first
|
|
183
|
+
temporal workflow terminate --workflow-id <id> --namespace <ns> --reason "Restarting"
|
|
184
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-temporal
|
|
3
|
+
description: Build and manage Temporal server deployment, namespace provisioning, and schedule registration from a chant project
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Temporal Operational Playbook
|
|
8
|
+
|
|
9
|
+
## How chant and Temporal relate
|
|
10
|
+
|
|
11
|
+
chant is a **synthesis compiler** — it compiles TypeScript resource declarations into deployment artifacts. `chant build` does not start Temporal or register anything; synthesis is pure and deterministic. Your job as an agent is to bridge synthesis and deployment:
|
|
12
|
+
|
|
13
|
+
- Use **chant** for: build, lint, diff (local config comparison)
|
|
14
|
+
- Use **docker compose / kubectl / temporal CLI** for: starting the server, applying configs, and all runtime operations
|
|
15
|
+
|
|
16
|
+
The source of truth for Temporal configuration is the TypeScript in `src/`. The generated artifacts in `dist/` are intermediate outputs.
|
|
17
|
+
|
|
18
|
+
## Resources and their outputs
|
|
19
|
+
|
|
20
|
+
| Resource | Emits |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `TemporalServer` | `docker-compose.yml` (primary) + `temporal-helm-values.yaml` |
|
|
23
|
+
| `TemporalNamespace` | `temporal-setup.sh` (namespace create commands) |
|
|
24
|
+
| `SearchAttribute` | `temporal-setup.sh` (search-attribute create commands) |
|
|
25
|
+
| `TemporalSchedule` | `schedules/<id>.ts` (SDK schedule creation script) |
|
|
26
|
+
|
|
27
|
+
## Build and validate
|
|
28
|
+
|
|
29
|
+
### Build the project
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
chant build src/ --output dist/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
- `--watch` — rebuild on source changes
|
|
37
|
+
- `--format json` — not applicable (Temporal outputs are YAML/shell/TypeScript)
|
|
38
|
+
|
|
39
|
+
### Lint the source
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
chant lint src/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Start the Temporal server
|
|
46
|
+
|
|
47
|
+
### Local dev (generated docker-compose.yml)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Start the dev server
|
|
51
|
+
docker compose up -d
|
|
52
|
+
|
|
53
|
+
# Verify it's running
|
|
54
|
+
temporal operator cluster health
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The dev server runs as a single container (`temporal server start-dev`). The Web UI is available at `http://localhost:8080`.
|
|
58
|
+
|
|
59
|
+
### Temporal Cloud
|
|
60
|
+
|
|
61
|
+
No server to start. Configure your worker profile in `chant.config.ts`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import type { TemporalChantConfig } from "@intentius/chant-lexicon-temporal";
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
lexicons: ["temporal"],
|
|
68
|
+
temporal: {
|
|
69
|
+
profiles: {
|
|
70
|
+
cloud: {
|
|
71
|
+
address: "myns.a2dd6.tmprl.cloud:7233",
|
|
72
|
+
namespace: "myns.a2dd6",
|
|
73
|
+
taskQueue: "my-deploy",
|
|
74
|
+
tls: true,
|
|
75
|
+
apiKey: { env: "TEMPORAL_API_KEY" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultProfile: "cloud",
|
|
79
|
+
} satisfies TemporalChantConfig,
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Provision namespaces and search attributes
|
|
84
|
+
|
|
85
|
+
After the server is ready, run the generated setup script:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bash dist/temporal-setup.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This creates namespaces and registers search attributes using the `temporal` CLI. The `TEMPORAL_ADDRESS` env var overrides the default `localhost:7233`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
TEMPORAL_ADDRESS=myns.a2dd6.tmprl.cloud:7233 bash dist/temporal-setup.sh
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Verify:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
temporal operator namespace describe --namespace default
|
|
101
|
+
temporal operator search-attribute list --namespace default
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Deploy with Helm
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
helm repo add temporal https://go.temporal.io/server/helm-charts
|
|
108
|
+
helm repo update
|
|
109
|
+
helm install temporal temporal/temporal -f dist/temporal-helm-values.yaml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Wait for all pods:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
kubectl get pods -l app.kubernetes.io/name=temporal -w
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Register schedules
|
|
119
|
+
|
|
120
|
+
Each `TemporalSchedule` resource generates a standalone TypeScript runner:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Set connection env vars
|
|
124
|
+
export TEMPORAL_ADDRESS=localhost:7233
|
|
125
|
+
export TEMPORAL_NAMESPACE=default
|
|
126
|
+
|
|
127
|
+
# Run the generated schedule creation script
|
|
128
|
+
npx tsx dist/schedules/daily-backup.ts
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Verify the schedule was created:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
temporal schedule list --namespace default
|
|
135
|
+
temporal schedule describe --schedule-id daily-backup --namespace default
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Key resource types
|
|
139
|
+
|
|
140
|
+
| Resource | Purpose |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `TemporalServer` | Server deployment config (dev vs full mode) |
|
|
143
|
+
| `TemporalNamespace` | Namespace with retention policy |
|
|
144
|
+
| `SearchAttribute` | Custom workflow search field |
|
|
145
|
+
| `TemporalSchedule` | Recurring workflow trigger |
|
|
146
|
+
|
|
147
|
+
## Common patterns
|
|
148
|
+
|
|
149
|
+
### Minimal local dev stack
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { TemporalServer, TemporalNamespace } from "@intentius/chant-lexicon-temporal";
|
|
153
|
+
|
|
154
|
+
export const server = new TemporalServer({ mode: "dev" });
|
|
155
|
+
export const ns = new TemporalNamespace({ name: "default", retention: "7d" });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Production namespace with search attributes
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { TemporalNamespace, SearchAttribute } from "@intentius/chant-lexicon-temporal";
|
|
162
|
+
|
|
163
|
+
export const ns = new TemporalNamespace({
|
|
164
|
+
name: "prod-deploy",
|
|
165
|
+
retention: "30d",
|
|
166
|
+
description: "Production deployment workflows",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const gcpProject = new SearchAttribute({
|
|
170
|
+
name: "GcpProject",
|
|
171
|
+
type: "Text",
|
|
172
|
+
namespace: "prod-deploy",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const environment = new SearchAttribute({
|
|
176
|
+
name: "Environment",
|
|
177
|
+
type: "Keyword",
|
|
178
|
+
namespace: "prod-deploy",
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Recurring backup schedule
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { TemporalSchedule } from "@intentius/chant-lexicon-temporal";
|
|
186
|
+
|
|
187
|
+
export const backupSchedule = new TemporalSchedule({
|
|
188
|
+
scheduleId: "daily-backup",
|
|
189
|
+
spec: {
|
|
190
|
+
cronExpressions: ["0 3 * * *"],
|
|
191
|
+
},
|
|
192
|
+
action: {
|
|
193
|
+
workflowType: "backupWorkflow",
|
|
194
|
+
taskQueue: "backup-queue",
|
|
195
|
+
},
|
|
196
|
+
policies: {
|
|
197
|
+
overlap: "Skip",
|
|
198
|
+
pauseOnFailure: true,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { validate } from "./validate";
|
|
3
|
+
|
|
4
|
+
test("validate passes — all 4 resources have correct entityType strings", async () => {
|
|
5
|
+
const result = await validate({ verbose: false });
|
|
6
|
+
expect(result.valid ?? result.failed === 0).toBe(true);
|
|
7
|
+
expect(result.failed).toBe(0);
|
|
8
|
+
expect(result.errors).toHaveLength(0);
|
|
9
|
+
});
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the Temporal lexicon dist/ artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Since all resources are hand-written, validation checks that
|
|
5
|
+
* the packaging step produced correct dist/ artifacts: manifest.json,
|
|
6
|
+
* meta.json (with the 4 expected resource types), types/index.d.ts,
|
|
7
|
+
* and integrity.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
|
|
14
|
+
export interface ValidateResult {
|
|
15
|
+
passed: number;
|
|
16
|
+
failed: number;
|
|
17
|
+
errors: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EXPECTED_RESOURCE_TYPES = [
|
|
21
|
+
"TemporalServer",
|
|
22
|
+
"TemporalNamespace",
|
|
23
|
+
"SearchAttribute",
|
|
24
|
+
"TemporalSchedule",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export async function validate(opts?: { verbose?: boolean; basePath?: string }): Promise<ValidateResult> {
|
|
28
|
+
const pkgDir = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
29
|
+
const distDir = join(pkgDir, "dist");
|
|
30
|
+
const errors: string[] = [];
|
|
31
|
+
|
|
32
|
+
// manifest.json
|
|
33
|
+
const manifestPath = join(distDir, "manifest.json");
|
|
34
|
+
if (!existsSync(manifestPath)) {
|
|
35
|
+
errors.push("dist/manifest.json not found — run npm run bundle");
|
|
36
|
+
} else {
|
|
37
|
+
try {
|
|
38
|
+
const m = JSON.parse(readFileSync(manifestPath, "utf-8")) as Record<string, unknown>;
|
|
39
|
+
if (m["name"] !== "temporal") errors.push(`manifest.json: expected name "temporal", got ${JSON.stringify(m["name"])}`);
|
|
40
|
+
if (m["namespace"] !== "Temporal") errors.push(`manifest.json: expected namespace "Temporal", got ${JSON.stringify(m["namespace"])}`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
errors.push(`manifest.json: parse error — ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// meta.json (lexicon catalog)
|
|
47
|
+
const metaPath = join(distDir, "meta.json");
|
|
48
|
+
if (!existsSync(metaPath)) {
|
|
49
|
+
errors.push("dist/meta.json not found — run npm run bundle");
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
const catalog = JSON.parse(readFileSync(metaPath, "utf-8")) as Record<string, unknown>;
|
|
53
|
+
for (const name of EXPECTED_RESOURCE_TYPES) {
|
|
54
|
+
if (!(name in catalog)) {
|
|
55
|
+
errors.push(`meta.json: missing resource "${name}"`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
errors.push(`meta.json: parse error — ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// types/index.d.ts
|
|
64
|
+
const dtsPath = join(distDir, "types", "index.d.ts");
|
|
65
|
+
if (!existsSync(dtsPath)) {
|
|
66
|
+
errors.push("dist/types/index.d.ts not found — run npm run bundle");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// integrity.json
|
|
70
|
+
const integrityPath = join(distDir, "integrity.json");
|
|
71
|
+
if (!existsSync(integrityPath)) {
|
|
72
|
+
errors.push("dist/integrity.json not found — run npm run bundle");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result: ValidateResult = {
|
|
76
|
+
passed: 4 - errors.length < 0 ? 0 : 4 - errors.length,
|
|
77
|
+
failed: errors.length,
|
|
78
|
+
errors,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (opts?.verbose) {
|
|
82
|
+
if (errors.length === 0) {
|
|
83
|
+
console.error(`Validation passed: all dist/ artifacts present and valid`);
|
|
84
|
+
} else {
|
|
85
|
+
for (const err of errors) {
|
|
86
|
+
console.error(` ✗ ${err}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|