@frontera-sdk/cli 1.52.39 → 1.52.40
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/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +48 -4
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +331 -31
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +875 -88
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +211 -21
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +180 -7
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +24 -21
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runtimeLink, studioLink, type RuntimeLink } from '../mcp/mcp-runtime'
|
|
2
|
+
import { conversationTraces, traceLink } from '../mcp/mcp-traces'
|
|
3
|
+
import type { LocalProject } from './local-project'
|
|
4
|
+
import type { CaseResult } from './local-test-runner'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a failing Agent case leaves to look at: its conversation, the trace of
|
|
8
|
+
* its last turn and the start of the reply it was judged on, so whoever reads
|
|
9
|
+
* `local check` or `local test` (a coding agent with `--json`) can tell an
|
|
10
|
+
* Agent regression from a check that asks for the wrong thing without
|
|
11
|
+
* matching traces by their time.
|
|
12
|
+
*
|
|
13
|
+
* Traces are kept by the local traces store, outside the platform database:
|
|
14
|
+
* `local check` rolls back the database after its tests (the conversation goes
|
|
15
|
+
* with it), and the trace stays readable (`trace_get`, Studio's Traces page).
|
|
16
|
+
*/
|
|
17
|
+
export interface CaseEvidence {
|
|
18
|
+
conversationId?: string
|
|
19
|
+
traceId?: string
|
|
20
|
+
/** Studio's page for the trace. */
|
|
21
|
+
traceLink?: string
|
|
22
|
+
/** Studio's page for the conversation, while the database still holds it. */
|
|
23
|
+
conversationLink?: string
|
|
24
|
+
/** The reply the case was judged on, its start (`REPLY_EXCERPT` characters). */
|
|
25
|
+
replyExcerpt?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const REPLY_EXCERPT = 300
|
|
29
|
+
|
|
30
|
+
export function replyExcerpt(reply: string): string | undefined {
|
|
31
|
+
const text = reply.replace(/\s+/g, ' ').trim()
|
|
32
|
+
if (!text) return undefined
|
|
33
|
+
return text.length > REPLY_EXCERPT ? `${text.slice(0, REPLY_EXCERPT)}…` : text
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The newest trace of the conversation that called a model (the turn the reply came from), else its newest. */
|
|
37
|
+
async function lastTurnTrace(link: RuntimeLink, conversationId: string, since?: string): Promise<string | null> {
|
|
38
|
+
const traces = (await conversationTraces(link, conversationId, since)).sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp))
|
|
39
|
+
return (traces.find((t) => t.observations.some((o) => o.type === 'GENERATION')) ?? traces[0])?.id ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The evidence of each failing case (never one that passed or couldn't run),
|
|
44
|
+
* by its index in `results`. A trace that has not arrived is looked for again
|
|
45
|
+
* for a few seconds; one that never does leaves its id out, never the case.
|
|
46
|
+
*/
|
|
47
|
+
export async function failingCaseEvidence(project: LocalProject, results: readonly CaseResult[], opts: { waitMs?: number } = {}): Promise<Map<number, CaseEvidence>> {
|
|
48
|
+
const out = new Map<number, CaseEvidence>()
|
|
49
|
+
const failing = results.map((r, i) => [r, i] as const).filter(([r]) => !r.passed && !r.notRun)
|
|
50
|
+
if (!failing.length) return out
|
|
51
|
+
const link = runtimeLink(project)
|
|
52
|
+
const deadline = Date.now() + (opts.waitMs ?? 4_000)
|
|
53
|
+
for (const [r, i] of failing) {
|
|
54
|
+
const evidence: CaseEvidence = {
|
|
55
|
+
...(r.conversationId ? { conversationId: r.conversationId } : {}),
|
|
56
|
+
...(replyExcerpt(r.reply) ? { replyExcerpt: replyExcerpt(r.reply) } : {}),
|
|
57
|
+
}
|
|
58
|
+
if (typeof link !== 'string' && r.conversationId) {
|
|
59
|
+
if (r.agentId) evidence.conversationLink = studioLink(link, `/agents/${encodeURIComponent(r.agentId)}/c/${encodeURIComponent(r.conversationId)}`)
|
|
60
|
+
const since = r.startedAt ? new Date(Date.parse(r.startedAt) - 1_000).toISOString() : undefined
|
|
61
|
+
for (;;) {
|
|
62
|
+
const id = await lastTurnTrace(link, r.conversationId, since).catch(() => null)
|
|
63
|
+
if (id) {
|
|
64
|
+
evidence.traceId = id
|
|
65
|
+
evidence.traceLink = traceLink(link, id)
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
if (Date.now() >= deadline) break
|
|
69
|
+
await new Promise((res) => setTimeout(res, 500))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(evidence).length) out.set(i, evidence)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The trace of each case's last turn, by its index in `results`: every case
|
|
79
|
+
* that has a conversation (passed, failed, or one that could not finish), so
|
|
80
|
+
* `local test --json` and the MCP `test_run` name the trace beside the
|
|
81
|
+
* conversation, and nobody matches traces by their time. A trace that has not
|
|
82
|
+
* arrived is looked for again for a few seconds, across the cases together
|
|
83
|
+
* (a second only, when none of the run's has arrived); one that never does
|
|
84
|
+
* leaves its case without a `traceId`. `known`: the
|
|
85
|
+
* failing cases' evidence already found, not looked for again. A case of several
|
|
86
|
+
* turns names its last turn's trace; `trace_list` with the conversation id
|
|
87
|
+
* lists the others.
|
|
88
|
+
*/
|
|
89
|
+
export async function caseTraces(project: LocalProject, results: readonly CaseResult[], opts: { waitMs?: number; known?: ReadonlyMap<number, CaseEvidence> } = {}): Promise<Map<number, { traceId: string; traceLink: string }>> {
|
|
90
|
+
const out = new Map<number, { traceId: string; traceLink: string }>()
|
|
91
|
+
const link = runtimeLink(project)
|
|
92
|
+
if (typeof link === 'string') return out
|
|
93
|
+
for (const [i, e] of opts.known ?? []) if (e.traceId && e.traceLink) out.set(i, { traceId: e.traceId, traceLink: e.traceLink })
|
|
94
|
+
let missing = results.map((r, i) => [r, i] as const).filter(([r, i]) => !!r.conversationId && !out.has(i))
|
|
95
|
+
const started = Date.now()
|
|
96
|
+
const deadline = started + (opts.waitMs ?? 4_000)
|
|
97
|
+
while (missing.length) {
|
|
98
|
+
for (const [r, i] of missing) {
|
|
99
|
+
const since = r.startedAt ? new Date(Date.parse(r.startedAt) - 1_000).toISOString() : undefined
|
|
100
|
+
const id = await lastTurnTrace(link, r.conversationId!, since).catch(() => null)
|
|
101
|
+
if (id) out.set(i, { traceId: id, traceLink: traceLink(link, id) })
|
|
102
|
+
}
|
|
103
|
+
missing = missing.filter(([, i]) => !out.has(i))
|
|
104
|
+
// A store that has none of this run's traces after a second is not about to: no wait for the rest.
|
|
105
|
+
if (!missing.length || Date.now() >= deadline || (!out.size && Date.now() >= started + 1_000)) break
|
|
106
|
+
await new Promise((res) => setTimeout(res, 500))
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One line under a failing case: "trace 6f5e… (link); conversation c-1; replied: "…"". */
|
|
112
|
+
export function evidenceLine(e: CaseEvidence, opts: { rolledBack?: boolean } = {}): string {
|
|
113
|
+
return [
|
|
114
|
+
e.traceId ? `trace ${e.traceId}${e.traceLink ? ` (${e.traceLink})` : ''}` : e.conversationId ? 'trace not arrived yet (trace_list with the conversation id finds it)' : '',
|
|
115
|
+
e.conversationId ? `conversation ${e.conversationId}${opts.rolledBack ? ' (rolled back with the test data; the trace stays)' : ''}` : '',
|
|
116
|
+
e.replyExcerpt ? `replied: "${e.replyExcerpt}"` : 'no reply',
|
|
117
|
+
]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join('; ')
|
|
120
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Agent case that couldn't run, told apart from one that failed.
|
|
3
|
+
*
|
|
4
|
+
* A case fails when the Agent's reply or tools miss what the case expects:
|
|
5
|
+
* that is what a change broke. A case that never got a reply because the
|
|
6
|
+
* model provider refused the request (out of credit, a rate limit, a refused
|
|
7
|
+
* key, an outage), the turn timed out, or the local runtime did not answer
|
|
8
|
+
* says nothing about the Agent. Such a case still keeps the gate from
|
|
9
|
+
* passing, but it is never counted as newly failing, never recorded as the
|
|
10
|
+
* case's last outcome, and it is said with its cause and the fix.
|
|
11
|
+
*
|
|
12
|
+
* The runtime describes a provider failure as `<status> · <category> ·
|
|
13
|
+
* <message>` (`describeProviderFailure` in the service): `402 ·
|
|
14
|
+
* invalid_request · This request would exceed your available credits…`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type NotRunKind = 'credits' | 'rate-limit' | 'key' | 'provider-error' | 'timeout' | 'runtime-unreachable' | 'runtime-error'
|
|
18
|
+
|
|
19
|
+
export interface CaseNotRun {
|
|
20
|
+
/** Whose side it is on: the model provider's, the turn's clock, or the local runtime's. */
|
|
21
|
+
cause: 'provider' | 'timeout' | 'runtime'
|
|
22
|
+
kind: NotRunKind
|
|
23
|
+
/** After "couldn't run: ": "the model provider refused (402 credits)". */
|
|
24
|
+
what: string
|
|
25
|
+
/** What fixes it, for the person. */
|
|
26
|
+
fix: string
|
|
27
|
+
/** Never sent: an earlier case met the same refusal, and the run stopped sending cases. */
|
|
28
|
+
skipped?: boolean
|
|
29
|
+
/** The provider that refused, when its words name it (`openrouter`, `anthropic`). */
|
|
30
|
+
provider?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Refusals every later case would meet as well: the account's credit or key, or a runtime that does not answer. */
|
|
34
|
+
const STOPS_THE_RUN = new Set<NotRunKind>(['credits', 'key', 'runtime-unreachable'])
|
|
35
|
+
|
|
36
|
+
export function stopsTheRun(n: CaseNotRun): boolean {
|
|
37
|
+
return STOPS_THE_RUN.has(n.kind)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PROVIDERS = 'frontera local providers'
|
|
41
|
+
|
|
42
|
+
/** The fix for each kind; `again` is what the person does once it is fixed ("runs the cases again"). */
|
|
43
|
+
function fixFor(kind: NotRunKind, again = 'runs the cases again'): string {
|
|
44
|
+
switch (kind) {
|
|
45
|
+
case 'credits':
|
|
46
|
+
return `the model provider account is out of credit: the person adds credit there, or picks a model through another provider they hold a key for (\`${PROVIDERS}\` shows which provider each model uses), then ${again}`
|
|
47
|
+
case 'rate-limit':
|
|
48
|
+
return again === 'runs the cases again' ? 'the model provider is limiting requests: run the cases again later, or one at a time with `--concurrency 1`' : `the model provider is limiting requests: wait a minute, then ${again}`
|
|
49
|
+
case 'key':
|
|
50
|
+
return `the model provider refused the key: the person checks it and sets it again (\`frontera local key-set <provider>\`; \`${PROVIDERS}\` shows which provider each model uses), then ${again}`
|
|
51
|
+
case 'provider-error':
|
|
52
|
+
return `the model provider failed to answer: ${again.replace(/^runs /, 'run ')}; if it persists, check the provider’s status`
|
|
53
|
+
case 'timeout':
|
|
54
|
+
return 'the turn did not finish in time: run the case again; if it times out again, open its conversation in Studio (`frontera local ui`) to see where it stopped'
|
|
55
|
+
case 'runtime-unreachable':
|
|
56
|
+
return '`frontera local status` says whether the local runtime is running; `frontera local start` starts it'
|
|
57
|
+
case 'runtime-error':
|
|
58
|
+
return 'the local runtime failed the request: `frontera local logs service` says why; run the cases again once it answers'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The fix for a kind of refusal, with what the person does once it is fixed ("sends the message again"). */
|
|
63
|
+
export function notRunFix(kind: NotRunKind, again?: string): string {
|
|
64
|
+
return fixFor(kind, again)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const notRun = (cause: CaseNotRun['cause'], kind: NotRunKind, what: string): CaseNotRun => ({ cause, kind, what, fix: fixFor(kind) })
|
|
68
|
+
|
|
69
|
+
/** Which provider a refusal's words come from, when they say: OpenRouter's "can only afford", Anthropic's "credit balance is too low". */
|
|
70
|
+
function refusingProvider(text: string): string | null {
|
|
71
|
+
if (/openrouter|can only afford|requires more credits/i.test(text)) return 'openrouter'
|
|
72
|
+
if (/anthropic|credit balance is too low/i.test(text)) return 'anthropic'
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A model offered directly by a provider whose key the runtime holds (`directModelFor` in local-seed). */
|
|
77
|
+
export interface DirectModelHint {
|
|
78
|
+
slug: string
|
|
79
|
+
name: string
|
|
80
|
+
providerLabel: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A credit refusal's fix with the model the person can switch to, when the
|
|
85
|
+
* runtime offers one directly from another provider they hold a key for:
|
|
86
|
+
* its slug, and how to make it the default (`frontera local model-default`),
|
|
87
|
+
* which is theirs to choose. Any other refusal is returned as it is.
|
|
88
|
+
*/
|
|
89
|
+
export function withDirectModel(n: CaseNotRun, direct: DirectModelHint | null, again = 'run the cases again'): CaseNotRun {
|
|
90
|
+
if (n.kind !== 'credits' || !direct) return n
|
|
91
|
+
return {
|
|
92
|
+
...n,
|
|
93
|
+
fix: `the model provider account is out of credit: the person adds credit there, or uses a model through another provider they hold a key for. ${/^[AEIOU]/i.test(direct.providerLabel) ? 'An' : 'A'} ${direct.providerLabel} key is stored, so the runtime offers ${direct.name} directly from ${direct.providerLabel} as \`${direct.slug}\` (\`${PROVIDERS}\` lists it): \`frontera local model-default ${direct.slug}\` makes it the model new Agents and test grading use, and the one an Agent runs on when the runtime lacks the model its agent.json names (\`frontera local dev --once --reapply\` applies that to Agents already applied); an Agent whose agent.json names a model the runtime has keeps it until its file names \`${direct.slug}\`. Then ${again}`,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A provider failure as the runtime words it: its status and category, when it says them. */
|
|
98
|
+
const DESCRIBED = /(?:^|[\s:"'(])(\d{3}) · (?:(provider_unavailable|rate_limited|authentication|invalid_request|out_of_credit|aborted) · )?/
|
|
99
|
+
const CATEGORY_ONLY = /(?:^|[\s:"'(])(provider_unavailable|rate_limited|authentication|out_of_credit) · /
|
|
100
|
+
const CREDIT = /credit|can only afford|insufficient (?:funds|balance|quota)|billing|payment required|quota/i
|
|
101
|
+
const RATE = /rate.?limit|too many requests|in-flight|in flight/i
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Why a case's error means it couldn't run, or null when it is a failure of
|
|
105
|
+
* the case itself (a refused setup, a conversation waiting on a decision, a
|
|
106
|
+
* missing Agent). Only an error is classified, never a verdict on a reply.
|
|
107
|
+
*/
|
|
108
|
+
export function caseCouldNotRun(message: string): CaseNotRun | null {
|
|
109
|
+
const text = message.replace(/\s+/g, ' ').trim()
|
|
110
|
+
if (!text) return null
|
|
111
|
+
const described = DESCRIBED.exec(text)
|
|
112
|
+
const status = described ? Number(described[1]) : undefined
|
|
113
|
+
const category = described?.[2] ?? CATEGORY_ONLY.exec(text)?.[1]
|
|
114
|
+
const fromProvider = !!described || !!category
|
|
115
|
+
// A 402 or 429 the provider worded without the runtime's form ("402 Payment Required: … credits").
|
|
116
|
+
const bare = !fromProvider ? /\b(402|429)\b/.exec(text) : null
|
|
117
|
+
const code = status ?? (bare ? Number(bare[1]) : undefined)
|
|
118
|
+
|
|
119
|
+
// `out_of_credit`: the runtime's own category for a credit refusal, whatever status the provider sent.
|
|
120
|
+
if (code === 402 || category === 'out_of_credit' || ((fromProvider || bare) && CREDIT.test(text) && code !== 429 && category !== 'rate_limited')) {
|
|
121
|
+
const provider = refusingProvider(text)
|
|
122
|
+
return { ...notRun('provider', 'credits', `the model provider refused (${code ?? 402} credits)`), ...(provider ? { provider } : {}) }
|
|
123
|
+
}
|
|
124
|
+
if (code === 429 || category === 'rate_limited' || (bare && RATE.test(text))) return notRun('provider', 'rate-limit', `the model provider refused (${code ?? 429} rate limit)`)
|
|
125
|
+
if (fromProvider && (code === 401 || code === 403 || category === 'authentication')) return notRun('provider', 'key', `the model provider refused the key${code ? ` (${code})` : ''}`)
|
|
126
|
+
if (fromProvider && ((code !== undefined && (code === 408 || code >= 500)) || category === 'provider_unavailable')) return notRun('provider', 'provider-error', `the model provider failed${code ? ` (${code})` : ''}`)
|
|
127
|
+
|
|
128
|
+
// The turn's own clock (the runner gives a turn 180 s, a call 60 s).
|
|
129
|
+
if (/operation timed out|TimeoutError|aborted due to timeout|timed out after/i.test(text)) return notRun('timeout', 'timeout', 'the turn did not finish in time')
|
|
130
|
+
// The local runtime did not answer at all, or failed the request.
|
|
131
|
+
if (/unable to connect|ECONNREFUSED|ConnectionRefused|fetch failed|socket connection was closed unexpectedly/i.test(text)) return notRun('runtime', 'runtime-unreachable', 'the local runtime did not answer')
|
|
132
|
+
const failed = /\banswered (5\d\d)\b/.exec(text)
|
|
133
|
+
if (failed) return notRun('runtime', 'runtime-error', `the local runtime failed (${failed[1]})`)
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The cases that couldn't run, by cause: one line each, with its fix and the cases. */
|
|
138
|
+
export function notRunGroups<T extends { notRun?: CaseNotRun; agent: string; name: string }>(results: readonly T[]): Array<{ what: string; fix: string; cause: CaseNotRun['cause']; cases: string[]; skipped: number }> {
|
|
139
|
+
const groups = new Map<string, { what: string; fix: string; cause: CaseNotRun['cause']; cases: string[]; skipped: number }>()
|
|
140
|
+
for (const r of results) {
|
|
141
|
+
if (!r.notRun) continue
|
|
142
|
+
const g = groups.get(r.notRun.what) ?? { what: r.notRun.what, fix: r.notRun.fix, cause: r.notRun.cause, cases: [], skipped: 0 }
|
|
143
|
+
g.cases.push(`${r.agent} · ${r.name}`)
|
|
144
|
+
if (r.notRun.skipped) g.skipped += 1
|
|
145
|
+
groups.set(r.notRun.what, g)
|
|
146
|
+
}
|
|
147
|
+
return [...groups.values()]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** "13 couldn't run: the model provider refused (402 credits)", causes joined; empty when every case ran. */
|
|
151
|
+
export function notRunWords(groups: ReturnType<typeof notRunGroups>): string {
|
|
152
|
+
return groups.map((g) => `${g.cases.length} couldn’t run: ${g.what}`).join('; ')
|
|
153
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type { RemoteItem } from './local-sync-plan'
|
|
5
|
+
import type { LocalCredentials } from './local-state'
|
|
5
6
|
import { itemRef, referencesIn, toIds, toNames, type Directory } from './local-sync-refs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -32,12 +33,12 @@ export interface ChatAppRow {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** The configuration's sections a project file keeps, in this order. */
|
|
35
|
-
const SECTIONS = ['presentation', 'roster', 'features', 'conversation', 'memory'] as const
|
|
36
|
+
const SECTIONS = ['presentation', 'roster', 'features', 'conversation', 'variables', 'memory'] as const
|
|
36
37
|
|
|
37
38
|
export const chatAppPath = (slug: string) => `chat-apps/${slug}.json`
|
|
38
39
|
|
|
39
40
|
/** Why a Chat App stays a draft locally, as the build and the Pull dialog say it. */
|
|
40
|
-
export const CHAT_APP_DRAFT = 'Saved as the owner’s draft here
|
|
41
|
+
export const CHAT_APP_DRAFT = 'Saved as the owner’s draft here: Studio opens it at /apps/<slug> as its draft preview, its Agents running their drafts. It publishes once its Agents are published (a Chat App publishes only published Agents)'
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* The project's file for an App's live configuration: its Agents by name,
|
|
@@ -83,11 +84,45 @@ export function readChatApps(rows: ChatAppRow[], directory: Directory, wanted: (
|
|
|
83
84
|
|
|
84
85
|
type Call = (method: string, path: string, body?: unknown) => Promise<{ status: number; body: unknown }>
|
|
85
86
|
|
|
87
|
+
/** Calls as the runtime's workspace key: the principal `frontera chat-app` commands act as in the project. */
|
|
88
|
+
export function keyCalls(serviceUrl: string, creds: LocalCredentials, fetcher: typeof fetch = fetch): Call {
|
|
89
|
+
return async (method, path, body) => {
|
|
90
|
+
const res = await fetcher(new URL(path, serviceUrl), {
|
|
91
|
+
method,
|
|
92
|
+
headers: {
|
|
93
|
+
authorization: `Bearer ${creds.workspaceKey ?? ''}`,
|
|
94
|
+
'x-org-id': creds.organizationId,
|
|
95
|
+
'x-workspace-id': creds.workspaceId,
|
|
96
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
97
|
+
},
|
|
98
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
99
|
+
})
|
|
100
|
+
const text = await res.text()
|
|
101
|
+
let parsed: unknown = text
|
|
102
|
+
try {
|
|
103
|
+
parsed = text ? JSON.parse(text) : null
|
|
104
|
+
} catch {
|
|
105
|
+
// Keep the text.
|
|
106
|
+
}
|
|
107
|
+
return { status: res.status, body: parsed }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
const dataOf = (body: unknown) => ((body as { data?: unknown } | null)?.data ?? body) as Record<string, unknown> | null
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
/**
|
|
114
|
+
* A refusal with what the service said was wrong: the configuration's
|
|
115
|
+
* validation (`roster.agents.0.label: Required`) is in its `details`, which a
|
|
116
|
+
* bare "400 Invalid Chat App configuration" hides.
|
|
117
|
+
*/
|
|
118
|
+
export const errorOf = (a: { status: number; body: unknown }) => {
|
|
119
|
+
const b = a.body as { message?: unknown; details?: unknown; data?: { details?: unknown } } | null
|
|
120
|
+
const message = String(typeof b?.message === 'string' ? b.message : JSON.stringify(b)).replace(/\s+/g, ' ').slice(0, 200)
|
|
121
|
+
const raw = b?.details ?? b?.data?.details
|
|
122
|
+
const details = (Array.isArray(raw) ? raw : raw && typeof raw === 'object' ? [raw] : [])
|
|
123
|
+
.map((d) => (typeof d === 'string' ? d : d && typeof d === 'object' ? [(d as { path?: unknown }).path, (d as { message?: unknown }).message].filter((x) => typeof x === 'string' && x).join(': ') || JSON.stringify(d) : String(d)))
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
return details.length ? `${a.status} ${message}: ${details.slice(0, 8).join('; ')}${details.length > 8 ? ` (and ${details.length - 8} more)` : ''}` : `${a.status} ${message}`
|
|
91
126
|
}
|
|
92
127
|
|
|
93
128
|
export interface ChatAppApplied {
|
|
@@ -107,7 +142,7 @@ export interface ChatAppApplied {
|
|
|
107
142
|
* hold is left off the roster, and said; one it holds but the runtime does
|
|
108
143
|
* not have yet holds the App back until it does.
|
|
109
144
|
*/
|
|
110
|
-
export async function applyChatApp(root: string, slug: string, call: Call, directory: () => Promise<Directory>, provides: (ref: string) => boolean): Promise<ChatAppApplied> {
|
|
145
|
+
export async function applyChatApp(root: string, slug: string, call: Call, directory: () => Promise<Directory>, provides: (ref: string) => boolean, keyCall?: Call): Promise<ChatAppApplied> {
|
|
111
146
|
const path = join(root, chatAppPath(slug))
|
|
112
147
|
if (!existsSync(path)) return { ok: false, output: `error [CHAT_APP]: ${chatAppPath(slug)} is missing` }
|
|
113
148
|
let doc: Record<string, unknown>
|
|
@@ -131,8 +166,10 @@ export async function applyChatApp(root: string, slug: string, call: Call, direc
|
|
|
131
166
|
if (waiting.length) {
|
|
132
167
|
return { ok: false, missing: waiting, output: [`error [UNRESOLVED_REFERENCE]: ${chatAppPath(slug)} names what this runtime does not have`, ...waiting.map((w) => ` ${w}: in the project, not applied yet`)].join('\n') }
|
|
133
168
|
}
|
|
134
|
-
//
|
|
135
|
-
|
|
169
|
+
// A draft names no Agent version (null: the one live when it is published), as the platform's
|
|
170
|
+
// editor drafts it; on this runtime its preview runs each Agent's draft. The label and the
|
|
171
|
+
// description are the Agent's own unless the file gives the App's.
|
|
172
|
+
roster.agents = kept.map((e) => ({ ...e, agentVersionNumber: null, label: typeof e.label === 'string' && e.label.trim() ? e.label : null, description: typeof e.description === 'string' && e.description.trim() ? e.description : null }))
|
|
136
173
|
const ids = new Set(kept.map((e) => e.agentConfigId))
|
|
137
174
|
if (typeof roster.defaultAgentConfigId === 'string' && !ids.has(roster.defaultAgentConfigId)) roster.defaultAgentConfigId = kept[0]?.agentConfigId ?? null
|
|
138
175
|
const { name: _name, description: _description, ...configuration } = { ...doc, roster } as Record<string, unknown>
|
|
@@ -151,20 +188,60 @@ export async function applyChatApp(root: string, slug: string, call: Call, direc
|
|
|
151
188
|
}
|
|
152
189
|
const draft = await call('PUT', `/v1/chat-apps/${encodeURIComponent(appId)}/draft`, { configuration: resolved })
|
|
153
190
|
if (draft.status >= 400) return { ok: false, output: `error [CHAT_APP]: ${errorOf(draft)}` }
|
|
191
|
+
// What the runtime found wrong with its `variables` when it saved the draft: an error fails the apply, as it would block a publish.
|
|
192
|
+
const variableFindings = findingsOf(dataOf(draft.body)?.findings).filter((f) => f.code?.startsWith('variables.'))
|
|
193
|
+
const variableErrors = variableFindings.filter((f) => f.level === 'error')
|
|
194
|
+
// Drafts are per principal: the owner's is what Studio previews; the workspace key's is what
|
|
195
|
+
// `frontera chat-app get` reads in this project. Both hold the file.
|
|
196
|
+
if (keyCall) {
|
|
197
|
+
const keyDraft = await keyCall('PUT', `/v1/chat-apps/${encodeURIComponent(appId)}/draft`, { configuration: resolved })
|
|
198
|
+
if (keyDraft.status >= 400) lines.push(` warning: \`frontera chat-app get ${slug}\` shows the workspace key's own draft, which could not be saved (${errorOf(keyDraft)}); Studio shows the applied one`)
|
|
199
|
+
}
|
|
154
200
|
const published = await call('POST', `/v1/chat-apps/${encodeURIComponent(appId)}/publish`, { configuration: resolved })
|
|
155
|
-
if (published.status
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
201
|
+
if (published.status < 400) {
|
|
202
|
+
// Published: its roster's Agents were published here, and people open the live version.
|
|
203
|
+
const version = (dataOf(published.body) as { version?: { version?: unknown } | null; live?: { version?: unknown } | null } | null)
|
|
204
|
+
const v = version?.version?.version ?? version?.live?.version
|
|
205
|
+
lines.push(` published Chat App ${name}${typeof v === 'string' || typeof v === 'number' ? ` v${v}` : ''} here: people open its live version, with its Agents’ published versions; Studio opens it at /apps/${slug} as its draft preview, its Agents running their drafts`)
|
|
206
|
+
} else {
|
|
207
|
+
// Its variables' findings were said from the draft; a runtime that gave none there says them here.
|
|
208
|
+
const list = findingsOf(dataOf(published.body)?.details ?? (published.body as { details?: unknown } | null)?.details).filter((f) => !(variableFindings.length && f.code?.startsWith('variables.')))
|
|
209
|
+
for (const f of list.filter((x) => x.code?.startsWith('variables.') && x.level !== 'error')) lines.push(` warning [${f.code}]: ${f.message ?? ''}`)
|
|
210
|
+
const blocking = list.filter((x) => !x.code?.startsWith('variables.') || x.level === 'error')
|
|
211
|
+
for (const f of blocking.filter((x) => x.code?.startsWith('variables.'))) variableErrors.push(f)
|
|
212
|
+
const rest = blocking.filter((x) => !x.code?.startsWith('variables.'))
|
|
213
|
+
if (rest.length && rest.every((f) => f.code === 'agent.unpublished' || f.code === 'agent.unavailable' || f.code === 'roster.empty')) {
|
|
159
214
|
lines.push(
|
|
160
|
-
|
|
215
|
+
rest.some((f) => f.code === 'roster.empty')
|
|
161
216
|
? ' warning: It has no Agent on its roster here, so it stays a draft'
|
|
162
|
-
: ` warning: ${CHAT_APP_DRAFT}`,
|
|
217
|
+
: ` warning: ${CHAT_APP_DRAFT.replace('<slug>', slug)}`,
|
|
163
218
|
)
|
|
164
|
-
} else {
|
|
165
|
-
lines.push(` warning: It stays a draft here: ${
|
|
219
|
+
} else if (rest.length || !variableErrors.length) {
|
|
220
|
+
lines.push(` warning: It stays a draft here: ${rest.map((f) => f.message).filter(Boolean).join('; ') || errorOf(published)}`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const f of variableFindings.filter((x) => x.level !== 'error')) lines.push(` warning [${f.code}]: ${f.message ?? ''}`)
|
|
224
|
+
if (variableErrors.length) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
output: [
|
|
228
|
+
`error [CHAT_APP_VARIABLES]: ${chatAppPath(slug)}: its variables can't be given as written (the draft is saved, and can't be published until they can)`,
|
|
229
|
+
...variableErrors.map((f) => ` ${f.code}: ${f.message ?? ''}`),
|
|
230
|
+
...lines,
|
|
231
|
+
].join('\n'),
|
|
166
232
|
}
|
|
167
233
|
}
|
|
168
234
|
lines.unshift(` saved Chat App ${name}`)
|
|
169
235
|
return { ok: true, output: lines.join('\n') }
|
|
170
236
|
}
|
|
237
|
+
|
|
238
|
+
/** Validation findings as the runtime gives them (`level`, `code`, `message`), from a response's `findings` or an error's `details`. */
|
|
239
|
+
interface ChatAppFinding {
|
|
240
|
+
level?: 'error' | 'warning' | string
|
|
241
|
+
code?: string
|
|
242
|
+
message?: string
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findingsOf(raw: unknown): ChatAppFinding[] {
|
|
246
|
+
return Array.isArray(raw) ? raw.filter((f): f is ChatAppFinding => !!f && typeof f === 'object') : []
|
|
247
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join, normalize } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { hash, readLock } from './local-sync-plan'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a pulled project holds as the workspace gave it: each item whose files
|
|
8
|
+
* are all byte for byte what the last pull wrote (`frontera.lock`), and each
|
|
9
|
+
* such file. A failure only there is inherited from the workspace: the
|
|
10
|
+
* person's change did not make it, and fixing it means changing the
|
|
11
|
+
* workspace's own item. Empty for a project that was never pulled.
|
|
12
|
+
*/
|
|
13
|
+
export interface Unchanged {
|
|
14
|
+
/** Item references (`function/staff-availability`, `dataset/media_x`) unchanged since the pull. */
|
|
15
|
+
refs: Set<string>
|
|
16
|
+
/** Project paths unchanged since the pull. */
|
|
17
|
+
files: Set<string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unchangedSincePull(root: string): Unchanged {
|
|
21
|
+
const lock = readLock(root)
|
|
22
|
+
const refs = new Set<string>()
|
|
23
|
+
const files = new Set<string>()
|
|
24
|
+
if (!lock) return { refs, files }
|
|
25
|
+
for (const [ref, entry] of Object.entries(lock.items)) {
|
|
26
|
+
let all = Object.keys(entry.files ?? {}).length > 0
|
|
27
|
+
for (const [path, written] of Object.entries(entry.files ?? {})) {
|
|
28
|
+
const full = join(root, path)
|
|
29
|
+
const same = existsSync(full) && hash(readFileSync(full, 'utf8')) === written
|
|
30
|
+
if (same) files.add(normalize(path))
|
|
31
|
+
else all = false
|
|
32
|
+
}
|
|
33
|
+
if (all) refs.add(ref)
|
|
34
|
+
}
|
|
35
|
+
return { refs, files }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether a governed Action's semantic-type conflict is the workspace's: the
|
|
40
|
+
* Action's file, and each file the conflict rests on (the object type, its
|
|
41
|
+
* semantic type, the shared field it comes through), are all still as the
|
|
42
|
+
* pull wrote them. Then the Action deploys as the workspace runs it, with the
|
|
43
|
+
* conflict said as an inherited warning; one the person made (an edited
|
|
44
|
+
* Action, a narrowed type) is refused, as the platform refuses it.
|
|
45
|
+
*/
|
|
46
|
+
export function inheritedFromPull(unchanged: Unchanged): (action: string, refs: readonly string[]) => boolean {
|
|
47
|
+
return (action, refs) => unchanged.refs.has(`blueprint/actions/${action}`) && refs.every((ref) => unchanged.refs.has(ref))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The warnings inherited from the workspace on the governed Actions' rows:
|
|
52
|
+
* each Action deployed as the workspace runs it, with a value it offers that
|
|
53
|
+
* the semantic type it writes refuses. Listed by `local check`; never a
|
|
54
|
+
* failure, even with `--strict`: the Action runs, as it does in the workspace.
|
|
55
|
+
*/
|
|
56
|
+
export function inheritedActionWarnings(rows: ReadonlyArray<{ ref: string; warnings?: string[] }>): string[] {
|
|
57
|
+
return rows.flatMap((r) => (r.ref.startsWith('action/') ? r.warnings ?? [] : []))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The project files a typecheck's output names an error in (`functions/x/src/index.ts(411,5): error TS2322`). */
|
|
61
|
+
export function typecheckErrorFiles(output: string): string[] {
|
|
62
|
+
const out = new Set<string>()
|
|
63
|
+
for (const m of output.matchAll(/^\s*([^\s(:][^():\n]*\.(?:ts|tsx|mts|cts|js|jsx))(?:\(\d+,\d+\)|:\d+:\d+)\s*[:-]?\s*error\b/gm)) out.add(normalize(m[1]!.replace(/^\.\//, '')))
|
|
64
|
+
return [...out]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether every failure is inherited: there is at least one, and each is in
|
|
69
|
+
* an item or file unchanged since the pull. One failure the change could have
|
|
70
|
+
* made (an edited or new file) and the step is the change's.
|
|
71
|
+
*/
|
|
72
|
+
export function allInherited(failures: readonly string[], inherited: ReadonlySet<string>): boolean {
|
|
73
|
+
return failures.length > 0 && failures.every((f) => inherited.has(f))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const BLUEPRINT_ROWS: Record<string, string> = { objectType: 'object-types', action: 'actions', linkType: 'link-types', metric: 'metrics', semanticType: 'semantic-types', sharedField: 'shared-fields', objectSet: 'object-sets' }
|
|
77
|
+
|
|
78
|
+
/** A status row's reference as the lock names the item: `objectType/Receipt` → `blueprint/object-types/Receipt`. */
|
|
79
|
+
export function lockRefOf(rowRef: string): string {
|
|
80
|
+
const at = rowRef.indexOf('/')
|
|
81
|
+
const folder = at > 0 ? BLUEPRINT_ROWS[rowRef.slice(0, at)] : undefined
|
|
82
|
+
return folder ? `blueprint/${folder}/${rowRef.slice(at + 1)}` : rowRef
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cities sample data names for a custom `--region` point (`<lat>,<lon>[,<km>]`):
|
|
3
|
+
* the city a point is near, so a record's `city` agrees with its coordinates
|
|
4
|
+
* (e2e G4: a point on Denver gave every Site the made-up "West Ernesto").
|
|
5
|
+
*
|
|
6
|
+
* The large metropolitan areas of the countries sample data knows, and of a
|
|
7
|
+
* few more, with how far each reaches from its centre. A point near none of
|
|
8
|
+
* them keeps one made-up city for the whole region, as before.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface City {
|
|
12
|
+
name: string
|
|
13
|
+
lat: number
|
|
14
|
+
lon: number
|
|
15
|
+
/** How far the city reaches from its centre, in kilometres. */
|
|
16
|
+
reachKm: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const c = (name: string, lat: number, lon: number, reachKm = 25): City => ({ name, lat, lon, reachKm })
|
|
20
|
+
|
|
21
|
+
export const CITIES: readonly City[] = [
|
|
22
|
+
// United States
|
|
23
|
+
c('New York', 40.71, -74.01, 35), c('Los Angeles', 34.05, -118.24, 40), c('Chicago', 41.88, -87.63, 35), c('Houston', 29.76, -95.37, 35),
|
|
24
|
+
c('Phoenix', 33.45, -112.07, 30), c('Philadelphia', 39.95, -75.17), c('San Antonio', 29.42, -98.49), c('San Diego', 32.72, -117.16),
|
|
25
|
+
c('Dallas', 32.78, -96.8, 35), c('Austin', 30.27, -97.74), c('San Jose', 37.34, -121.89, 20), c('San Francisco', 37.77, -122.42, 20),
|
|
26
|
+
c('Seattle', 47.61, -122.33), c('Denver', 39.74, -104.99, 30), c('Boston', 42.36, -71.06), c('Washington', 38.91, -77.04),
|
|
27
|
+
c('Atlanta', 33.75, -84.39, 30), c('Miami', 25.76, -80.19), c('Minneapolis', 44.98, -93.27), c('Detroit', 42.33, -83.05),
|
|
28
|
+
c('Portland', 45.52, -122.68), c('Las Vegas', 36.17, -115.14), c('Salt Lake City', 40.76, -111.89), c('Nashville', 36.16, -86.78),
|
|
29
|
+
c('Charlotte', 35.23, -80.84), c('Kansas City', 39.1, -94.58), c('St. Louis', 38.63, -90.2), c('Orlando', 28.54, -81.38),
|
|
30
|
+
c('Tampa', 27.95, -82.46), c('Pittsburgh', 40.44, -79.99), c('Cleveland', 41.5, -81.69), c('Columbus', 39.96, -83.0),
|
|
31
|
+
c('Indianapolis', 39.77, -86.16), c('Baltimore', 39.29, -76.61, 20), c('Sacramento', 38.58, -121.49), c('New Orleans', 29.95, -90.07),
|
|
32
|
+
c('Honolulu', 21.31, -157.86), c('Anchorage', 61.22, -149.9), c('Albuquerque', 35.08, -106.65), c('Oklahoma City', 35.47, -97.52),
|
|
33
|
+
c('Raleigh', 35.78, -78.64), c('Milwaukee', 43.04, -87.91),
|
|
34
|
+
// Canada, Mexico and South America
|
|
35
|
+
c('Toronto', 43.65, -79.38, 30), c('Montreal', 45.5, -73.57), c('Vancouver', 49.28, -123.12), c('Calgary', 51.05, -114.07),
|
|
36
|
+
c('Mexico City', 19.43, -99.13, 35), c('São Paulo', -23.55, -46.63, 35), c('Rio de Janeiro', -22.91, -43.17, 30), c('Brasília', -15.79, -47.88),
|
|
37
|
+
c('Belo Horizonte', -19.92, -43.94), c('Salvador', -12.97, -38.5), c('Porto Alegre', -30.03, -51.23), c('Recife', -8.05, -34.88),
|
|
38
|
+
c('Fortaleza', -3.73, -38.53), c('Curitiba', -25.43, -49.27), c('Buenos Aires', -34.6, -58.38, 30), c('Santiago', -33.45, -70.67),
|
|
39
|
+
c('Bogotá', 4.71, -74.07), c('Lima', -12.05, -77.04),
|
|
40
|
+
// Indonesia, Malaysia, Singapore, the Philippines and the rest of Asia
|
|
41
|
+
c('Jakarta', -6.2, 106.82, 30), c('Surabaya', -7.25, 112.75), c('Bandung', -6.92, 107.61), c('Medan', 3.59, 98.67),
|
|
42
|
+
c('Semarang', -6.97, 110.42), c('Makassar', -5.15, 119.43), c('Yogyakarta', -7.8, 110.36, 20), c('Denpasar', -8.65, 115.22, 20),
|
|
43
|
+
c('Palembang', -2.98, 104.75), c('Balikpapan', -1.24, 116.85), c('Kuala Lumpur', 3.14, 101.69), c('George Town', 5.41, 100.33),
|
|
44
|
+
c('Johor Bahru', 1.49, 103.74, 15), c('Kota Kinabalu', 5.98, 116.07), c('Kuching', 1.55, 110.36), c('Singapore', 1.35, 103.82, 15),
|
|
45
|
+
c('Manila', 14.6, 120.98), c('Cebu City', 10.32, 123.89), c('Davao City', 7.19, 125.46), c('Bangkok', 13.76, 100.5, 30),
|
|
46
|
+
c('Ho Chi Minh City', 10.82, 106.63), c('Hanoi', 21.03, 105.85), c('Hong Kong', 22.32, 114.17, 20), c('Seoul', 37.57, 126.98, 30),
|
|
47
|
+
c('Shanghai', 31.23, 121.47, 35), c('Beijing', 39.9, 116.41, 35), c('Tokyo', 35.68, 139.69, 30), c('Osaka', 34.69, 135.5),
|
|
48
|
+
c('Nagoya', 35.18, 136.91), c('Sapporo', 43.06, 141.35), c('Fukuoka', 33.59, 130.4), c('Kyoto', 35.01, 135.77, 15),
|
|
49
|
+
c('Sendai', 38.27, 140.87), c('Hiroshima', 34.39, 132.46), c('Mumbai', 19.08, 72.88, 30), c('Delhi', 28.61, 77.21, 35),
|
|
50
|
+
c('Bengaluru', 12.97, 77.59, 30), c('Hyderabad', 17.39, 78.49, 30), c('Chennai', 13.08, 80.27), c('Kolkata', 22.57, 88.36),
|
|
51
|
+
c('Pune', 18.52, 73.86), c('Ahmedabad', 23.02, 72.57), c('Jaipur', 26.91, 75.79),
|
|
52
|
+
// Oceania
|
|
53
|
+
c('Sydney', -33.87, 151.21, 35), c('Melbourne', -37.81, 144.96, 35), c('Brisbane', -27.47, 153.03, 30), c('Perth', -31.95, 115.86, 30),
|
|
54
|
+
c('Adelaide', -34.93, 138.6), c('Canberra', -35.28, 149.13, 15), c('Hobart', -42.88, 147.33, 15), c('Darwin', -12.46, 130.84, 15),
|
|
55
|
+
c('Gold Coast', -28.02, 153.4, 20), c('Auckland', -36.85, 174.76),
|
|
56
|
+
// Europe
|
|
57
|
+
c('London', 51.51, -0.12, 30), c('Manchester', 53.48, -2.24), c('Birmingham', 52.49, -1.89), c('Leeds', 53.8, -1.55, 20),
|
|
58
|
+
c('Glasgow', 55.86, -4.25, 20), c('Edinburgh', 55.95, -3.19, 15), c('Liverpool', 53.41, -2.98, 20), c('Bristol', 51.45, -2.59, 15),
|
|
59
|
+
c('Cardiff', 51.48, -3.18, 15), c('Belfast', 54.6, -5.93, 15), c('Newcastle upon Tyne', 54.98, -1.61, 15), c('Dublin', 53.35, -6.26),
|
|
60
|
+
c('Berlin', 52.52, 13.4), c('Hamburg', 53.55, 9.99), c('Munich', 48.14, 11.58), c('Cologne', 50.94, 6.96, 20),
|
|
61
|
+
c('Frankfurt', 50.11, 8.68, 20), c('Stuttgart', 48.78, 9.18, 20), c('Düsseldorf', 51.23, 6.77, 15), c('Leipzig', 51.34, 12.37, 15),
|
|
62
|
+
c('Paris', 48.86, 2.35), c('Marseille', 43.3, 5.37), c('Lyon', 45.76, 4.84, 20), c('Toulouse', 43.6, 1.44, 20),
|
|
63
|
+
c('Nice', 43.7, 7.27, 15), c('Bordeaux', 44.84, -0.58, 20), c('Lille', 50.63, 3.06, 15), c('Nantes', 47.22, -1.55, 15),
|
|
64
|
+
c('Strasbourg', 48.57, 7.75, 15), c('Madrid', 40.42, -3.7), c('Barcelona', 41.39, 2.17), c('Valencia', 39.47, -0.38, 20),
|
|
65
|
+
c('Seville', 37.39, -5.98, 20), c('Bilbao', 43.26, -2.93, 15), c('Málaga', 36.72, -4.42, 15), c('Lisbon', 38.72, -9.14, 20),
|
|
66
|
+
c('Amsterdam', 52.37, 4.9, 20), c('Rotterdam', 51.92, 4.48, 15), c('The Hague', 52.08, 4.3, 10), c('Utrecht', 52.09, 5.12, 10),
|
|
67
|
+
c('Eindhoven', 51.44, 5.47, 10), c('Brussels', 50.85, 4.35, 20), c('Zurich', 47.38, 8.54, 15), c('Vienna', 48.21, 16.37, 20),
|
|
68
|
+
c('Rome', 41.9, 12.5), c('Milan', 45.46, 9.19), c('Copenhagen', 55.68, 12.57, 20), c('Stockholm', 59.33, 18.07),
|
|
69
|
+
c('Oslo', 59.91, 10.75, 20), c('Warsaw', 52.23, 21.01), c('Istanbul', 41.01, 28.98, 35),
|
|
70
|
+
// The Middle East and Africa
|
|
71
|
+
c('Dubai', 25.2, 55.27, 30), c('Abu Dhabi', 24.45, 54.38), c('Doha', 25.29, 51.53, 20), c('Riyadh', 24.71, 46.68, 30),
|
|
72
|
+
c('Cairo', 30.04, 31.24, 30), c('Lagos', 6.52, 3.38, 30), c('Nairobi', -1.29, 36.82), c('Johannesburg', -26.2, 28.05, 30),
|
|
73
|
+
c('Cape Town', -33.92, 18.42),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The city a custom region's point is in or near: the nearest city whose reach,
|
|
78
|
+
* widened by the region's own radius (at most 60 km), holds the point. Null
|
|
79
|
+
* when the point is near none of them.
|
|
80
|
+
*/
|
|
81
|
+
export function cityNear(point: { lat: number; lon: number }, radiusKm: number, distanceKm: (a: { lat: number; lon: number }, b: { lat: number; lon: number }) => number): City | null {
|
|
82
|
+
let best: { city: City; d: number } | null = null
|
|
83
|
+
for (const city of CITIES) {
|
|
84
|
+
const d = distanceKm(city, point)
|
|
85
|
+
if (d > city.reachKm + Math.min(radiusKm, 60)) continue
|
|
86
|
+
if (!best || d < best.d) best = { city, d }
|
|
87
|
+
}
|
|
88
|
+
return best?.city ?? null
|
|
89
|
+
}
|