@frontera-sdk/cli 1.52.38 → 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
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
linkTypeCreateBody,
|
|
7
7
|
linkTypeStructure,
|
|
8
8
|
objectTypeCreateBody,
|
|
9
|
+
objectTypeSensitivity,
|
|
10
|
+
fieldSensitivityOf,
|
|
9
11
|
actionBody,
|
|
12
|
+
declaredPropertyType,
|
|
10
13
|
metricCreateBody,
|
|
11
14
|
metricPatchBody,
|
|
12
15
|
readBacking,
|
|
@@ -18,6 +21,8 @@ import {
|
|
|
18
21
|
sharedPropertyPatchBody,
|
|
19
22
|
type AuthoringIds,
|
|
20
23
|
} from '../../blueprint/compile'
|
|
24
|
+
import { metricRuleProblems } from '../../blueprint/metric-rules'
|
|
25
|
+
import { actionRuleProblems, type ActionRuleProblems, type InheritedConflict } from '../../blueprint/action-rules'
|
|
21
26
|
import { pickCurrentRevision } from '../../blueprint/dataset-revision'
|
|
22
27
|
import {
|
|
23
28
|
assertPrunesLeaveNoDanglingReference,
|
|
@@ -38,11 +43,19 @@ import {
|
|
|
38
43
|
type DefinitionBundle,
|
|
39
44
|
} from '../../blueprint/model'
|
|
40
45
|
import { toFiles } from '../../blueprint/projection'
|
|
46
|
+
import { sourceSystemHint } from '../../local/local-blueprint-errors'
|
|
47
|
+
import { inheritedFromPull, unchangedSincePull } from '../../local/local-check-inherited'
|
|
48
|
+
import { localProject } from '../../local/local-project'
|
|
49
|
+
import { localRuntimeAt } from '../../local/local-runtime-target'
|
|
41
50
|
import { renderDrift, renderPlan } from '../../blueprint/render'
|
|
42
51
|
import {
|
|
43
52
|
SEMANTIC_TYPE_TEMPLATE_NAMES,
|
|
44
53
|
isSemanticTypeTemplate,
|
|
45
54
|
scaffold,
|
|
55
|
+
ACTION_TEMPLATES,
|
|
56
|
+
isActionTemplate,
|
|
57
|
+
type ActionTemplate,
|
|
58
|
+
type ScaffoldOptions,
|
|
46
59
|
type SemanticTypeTemplate,
|
|
47
60
|
} from '../../blueprint/scaffold'
|
|
48
61
|
import { pathFor, readTree, removeFromTree, writeTree } from '../../blueprint/tree'
|
|
@@ -244,12 +257,36 @@ function existingPaths(directory: string): string[] {
|
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
/**
|
|
261
|
+
* What a pulled project says about its Actions' semantic-type conflicts: which
|
|
262
|
+
* are the workspace's (`inheritedFromPull`: the Action and what the conflict
|
|
263
|
+
* rests on unchanged since the pull), and whether this run writes to the
|
|
264
|
+
* project's own local runtime, which takes such an Action as the workspace
|
|
265
|
+
* runs it. Anywhere else the service's own check decides (it refuses only a
|
|
266
|
+
* conflict a write adds), so `plan` is never stricter than the platform.
|
|
267
|
+
*/
|
|
268
|
+
function inheritance(ctx: CommandContext): { conflict?: InheritedConflict; local: boolean } {
|
|
269
|
+
let root: string
|
|
270
|
+
try {
|
|
271
|
+
root = localProject(ctx.cwd).root
|
|
272
|
+
} catch {
|
|
273
|
+
return { local: false }
|
|
274
|
+
}
|
|
275
|
+
const unchanged = unchangedSincePull(root)
|
|
276
|
+
if (!unchanged.refs.size) return { local: false }
|
|
277
|
+
return { conflict: inheritedFromPull(unchanged), local: localRuntimeAt(ctx.cwd, ctx.apiUrl) !== null }
|
|
278
|
+
}
|
|
279
|
+
|
|
247
280
|
async function computePlan(ctx: CommandContext): Promise<{
|
|
248
281
|
plan: Plan
|
|
249
282
|
files: AuthoredFile[]
|
|
250
283
|
/** The draft the plan was computed against — `apply` indexes it for id resolution. */
|
|
251
284
|
definition: DefinitionBundle
|
|
252
285
|
drift: Array<{ objectApiName: string; datasetName: string; currentDigest: string }>
|
|
286
|
+
/** The Action files' rule problems (`action-rules.ts`): what the draft would refuse, and what would not deploy. */
|
|
287
|
+
actionProblems: ActionRuleProblems
|
|
288
|
+
/** The Actions written to the local runtime as the workspace runs them (`inheritedActions`, on the local runtime only). */
|
|
289
|
+
inheritedWrites: ReadonlySet<string>
|
|
253
290
|
}> {
|
|
254
291
|
const client = api(ctx)
|
|
255
292
|
const directory = root(ctx)
|
|
@@ -260,6 +297,15 @@ async function computePlan(ctx: CommandContext): Promise<{
|
|
|
260
297
|
})
|
|
261
298
|
}
|
|
262
299
|
const files = readTree(directory)
|
|
300
|
+
// A measure's filters the draft would refuse: said by `plan`, and before `apply` writes anything.
|
|
301
|
+
const metricProblems = metricRuleProblems(files)
|
|
302
|
+
if (metricProblems.length) {
|
|
303
|
+
throw new CliError(`${metricProblems.length} metric problem${metricProblems.length === 1 ? '' : 's'} the draft would refuse, so nothing was planned or applied:\n${metricProblems.map((p) => ` ✕ ${p}`).join('\n')}`, {
|
|
304
|
+
code: 'VALIDATION_ERROR',
|
|
305
|
+
hint: 'fix each line in its file; a measure\'s `filters` is one condition, e.g. { property: state, op: ne, value: closed }, or several combined as { and: [ …conditions ] }',
|
|
306
|
+
details: { metrics: metricProblems },
|
|
307
|
+
})
|
|
308
|
+
}
|
|
263
309
|
const { definition, revision } = await client.draft()
|
|
264
310
|
const plan = withheldPrunes(diff(files, definition, revision), directory)
|
|
265
311
|
|
|
@@ -290,7 +336,87 @@ async function computePlan(ctx: CommandContext): Promise<{
|
|
|
290
336
|
drift.push({ objectApiName: pin.objectApiName, datasetName, currentDigest: current.schemaDigest })
|
|
291
337
|
}
|
|
292
338
|
}
|
|
293
|
-
|
|
339
|
+
const inherits = inheritance(ctx)
|
|
340
|
+
const actionProblems = actionRuleProblems(files, inherits.conflict)
|
|
341
|
+
const inheritedWrites = new Set(inherits.local ? actionProblems.inheritedActions ?? [] : [])
|
|
342
|
+
return { plan, files, definition, drift, actionProblems: await withServiceActionChecks(client, plan, files, actionProblems, inheritedWrites), inheritedWrites }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The draft route's own checks of each Action this plan writes, run by the
|
|
347
|
+
* service with nothing written (`checkAction`), added to the file rules
|
|
348
|
+
* `actionRuleProblems` checks here. The file rules are a copy of what the
|
|
349
|
+
* route refuses, and a copy misses some: a rule the route has and the copy
|
|
350
|
+
* doesn't passes `plan` and is refused at apply.
|
|
351
|
+
*
|
|
352
|
+
* An Action that names something this same plan creates (an object type, a
|
|
353
|
+
* property added to one, another Action) cannot be checked before that is
|
|
354
|
+
* written; the service says so (`ACTION_REFERENCE_NOT_ON_DRAFT`) and the
|
|
355
|
+
* write checks it in order. A platform without the check is not asked.
|
|
356
|
+
*/
|
|
357
|
+
async function withServiceActionChecks(
|
|
358
|
+
client: BlueprintAuthoringApi,
|
|
359
|
+
plan: Plan,
|
|
360
|
+
files: AuthoredFile[],
|
|
361
|
+
problems: ActionRuleProblems,
|
|
362
|
+
inheritedWrites: ReadonlySet<string> = new Set(),
|
|
363
|
+
): Promise<ActionRuleProblems> {
|
|
364
|
+
const writing = plan.operations.filter((o) => o.kind === 'action' && o.operation !== 'delete' && !problems.refusedActions.includes(o.apiName))
|
|
365
|
+
if (writing.length === 0) return problems
|
|
366
|
+
const planned = new Set(plan.operations.filter((o) => o.operation !== 'delete').map((o) => `${o.kind}:${o.apiName}`))
|
|
367
|
+
const createdHere = (missing: { what?: unknown; name?: unknown } | undefined): boolean => {
|
|
368
|
+
const name = typeof missing?.name === 'string' ? missing.name : ''
|
|
369
|
+
if (missing?.what === 'object type') return planned.has(`object-type:${name}`)
|
|
370
|
+
if (missing?.what === 'property' || missing?.what === 'lifecycle transition') return planned.has(`object-type:${name.split('.')[0]}`)
|
|
371
|
+
if (missing?.what === 'action') return planned.has(`action:${name}`)
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
const refused = [...problems.refused]
|
|
375
|
+
const refusedActions = [...problems.refusedActions]
|
|
376
|
+
for (const operation of writing) {
|
|
377
|
+
const file = files.find((f) => f.kind === 'action' && f.apiName === operation.apiName)
|
|
378
|
+
if (!file) continue
|
|
379
|
+
try {
|
|
380
|
+
await client.checkAction(actionBody(file), inheritedWrites.has(operation.apiName))
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if (!(err instanceof CliError)) throw err
|
|
383
|
+
// An older platform (no such route, or one its key lane does not open), or nothing adopted yet: the write is the check.
|
|
384
|
+
if (err.code === 'NOT_FOUND' || err.code === 'FORBIDDEN') return problems
|
|
385
|
+
const details = err.details as { reason?: unknown; missing?: { what?: unknown; name?: unknown }; issues?: unknown } | undefined
|
|
386
|
+
// A name this plan creates is not a problem, but the fields the platform refused alongside it are.
|
|
387
|
+
const fieldIssues = Array.isArray(details?.issues) && details.issues.length > 0
|
|
388
|
+
if (details?.reason === 'ACTION_REFERENCE_NOT_ON_DRAFT' && createdHere(details.missing) && !fieldIssues) continue
|
|
389
|
+
if (err.code !== 'VALIDATION_ERROR' && err.code !== 'CONFLICT') throw err
|
|
390
|
+
// One line: `local dev` leaves each refused file off by these lines (`refusedByRules`).
|
|
391
|
+
refused.push(`${file.path}: ${err.message.replace(/\s*\n\s*/g, ' ')}`)
|
|
392
|
+
refusedActions.push(operation.apiName)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return { ...problems, refused, refusedActions }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The Action rule problems, said under the plan: a refusal as an error line,
|
|
400
|
+
* what would not deploy as a warning (`local dev` carries `warning:` lines).
|
|
401
|
+
*/
|
|
402
|
+
function renderActionProblems(problems: ActionRuleProblems): string {
|
|
403
|
+
return [
|
|
404
|
+
...problems.refused.map((p) => ` error: ${p}`),
|
|
405
|
+
...problems.undeployable.map((p) => ` warning: ${p} (it applies, but its write path won't deploy)`),
|
|
406
|
+
...(problems.inherited ?? []).map((p) => ` warning: ${p}`),
|
|
407
|
+
].join('\n')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Refused before the first write: an Action the draft route would refuse, in a file this run applies. */
|
|
411
|
+
function assertActionsApplicable(problems: ActionRuleProblems, operations: ModelOperation[]): void {
|
|
412
|
+
const applying = new Set(operations.filter((o) => o.kind === 'action').map((o) => o.apiName))
|
|
413
|
+
if (!problems.refusedActions.some((name) => applying.has(name))) return
|
|
414
|
+
const refused = problems.refused
|
|
415
|
+
throw new CliError(`${refused.length} governed Action rule${refused.length === 1 ? ' is' : 's are'} broken, so nothing was applied:\n${refused.map((p) => ` ✕ ${p}`).join('\n')}`, {
|
|
416
|
+
code: 'VALIDATION_ERROR',
|
|
417
|
+
hint: 'fix each line in its file, then `frontera blueprint plan` checks it again without writing',
|
|
418
|
+
details: { refused, undeployable: problems.undeployable },
|
|
419
|
+
})
|
|
294
420
|
}
|
|
295
421
|
|
|
296
422
|
/**
|
|
@@ -333,7 +459,7 @@ export const blueprintPlan: Command = {
|
|
|
333
459
|
examples: ['frontera blueprint plan', 'frontera blueprint plan --json'],
|
|
334
460
|
},
|
|
335
461
|
async run(ctx) {
|
|
336
|
-
const { plan, files, definition, drift } = await computePlan(ctx)
|
|
462
|
+
const { plan, files, definition, drift, actionProblems } = await computePlan(ctx)
|
|
337
463
|
// Every refusal `apply` will raise, raised here too, and for the reason `plan`
|
|
338
464
|
// exists: a plan that cannot be applied is not one a reader should be shown as if
|
|
339
465
|
// it could. Two independent checks — a prune that would dangle a reference, and an
|
|
@@ -343,10 +469,10 @@ export const blueprintPlan: Command = {
|
|
|
343
469
|
assertPrunesLeaveNoDanglingReference(files, plan.prunes)
|
|
344
470
|
orderForPrune(plan.prunes, liveCompensations(definition))
|
|
345
471
|
}
|
|
346
|
-
const body = [renderPlan(plan, { prune: ctx.flags.prune === true }), renderDrift(drift)]
|
|
472
|
+
const body = [renderPlan(plan, { prune: ctx.flags.prune === true }), renderDrift(drift), renderActionProblems(actionProblems)]
|
|
347
473
|
.filter(Boolean)
|
|
348
474
|
.join('\n')
|
|
349
|
-
return { data: { ...plan, drift }, text: body }
|
|
475
|
+
return { data: { ...plan, drift, actionProblems }, text: body }
|
|
350
476
|
},
|
|
351
477
|
}
|
|
352
478
|
|
|
@@ -367,6 +493,8 @@ export async function applyOperation(
|
|
|
367
493
|
file: AuthoredFile,
|
|
368
494
|
revision: number,
|
|
369
495
|
ids: AuthoringIds,
|
|
496
|
+
/** The Actions written as the workspace runs them (`inheritedWrites`). */
|
|
497
|
+
inheritedWrites: ReadonlySet<string> = new Set(),
|
|
370
498
|
): Promise<number> {
|
|
371
499
|
const resolveDataset = (name: string) => resolveRevision(client, name)
|
|
372
500
|
|
|
@@ -419,6 +547,8 @@ export async function applyOperation(
|
|
|
419
547
|
)
|
|
420
548
|
}
|
|
421
549
|
ids.sharedPropertyIdByApiName.set(operation.apiName, id)
|
|
550
|
+
const createdType = (file.document as { propertyType?: unknown }).propertyType
|
|
551
|
+
if (typeof createdType === 'string') ids.sharedPropertyTypeByApiName?.set(operation.apiName, createdType)
|
|
422
552
|
return nextRevision(created, revision)
|
|
423
553
|
}
|
|
424
554
|
assertApplicable('shared-field', file, operation.liveDocument)
|
|
@@ -436,7 +566,7 @@ export async function applyOperation(
|
|
|
436
566
|
await objectTypeCreateBody(file, revision, resolveDataset, ids),
|
|
437
567
|
revision,
|
|
438
568
|
)
|
|
439
|
-
return nextRevision(created, revision)
|
|
569
|
+
return applyCalculatedFields(client, operation.apiName, file, nextRevision(created, revision))
|
|
440
570
|
}
|
|
441
571
|
// Refused BEFORE the first write, not after some of them. Everything this
|
|
442
572
|
// executor can express is enumerated below; a difference outside that set has no
|
|
@@ -457,9 +587,10 @@ export async function applyOperation(
|
|
|
457
587
|
current = nextRevision(updated, revision)
|
|
458
588
|
}
|
|
459
589
|
current = await applyObjectKeys(client, operation.apiName, file, operation.liveDocument, current)
|
|
460
|
-
|
|
590
|
+
current = await applyPropertyPlan(
|
|
461
591
|
client, operation.apiName, file, operation.liveDocument, current, ids,
|
|
462
592
|
)
|
|
593
|
+
return applyCalculatedFields(client, operation.apiName, file, current)
|
|
463
594
|
}
|
|
464
595
|
|
|
465
596
|
if (operation.kind === 'link-type') {
|
|
@@ -484,9 +615,10 @@ export async function applyOperation(
|
|
|
484
615
|
if (operation.operation !== 'create') {
|
|
485
616
|
assertApplicable('action', { ...file, document: body }, operation.liveDocument)
|
|
486
617
|
}
|
|
618
|
+
const inherited = inheritedWrites.has(operation.apiName)
|
|
487
619
|
const result = operation.operation === 'create'
|
|
488
|
-
? await client.createAction(body, revision)
|
|
489
|
-
: await client.updateAction(operation.apiName, body, revision)
|
|
620
|
+
? await client.createAction(body, revision, inherited)
|
|
621
|
+
: await client.updateAction(operation.apiName, body, revision, inherited)
|
|
490
622
|
return nextRevision(result, revision)
|
|
491
623
|
}
|
|
492
624
|
|
|
@@ -598,10 +730,15 @@ export function assertApplicable(
|
|
|
598
730
|
// The SAME predicate the plan used to decide this artifact changed. An equality
|
|
599
731
|
// comparison here would disagree with it about key order and about the fields a file
|
|
600
732
|
// omits, and refuse the run naming a field the plan never called changed.
|
|
601
|
-
|
|
602
|
-
|
|
733
|
+
// An object type's `governance.sensitivity` has a route (the update's `sensitivity`);
|
|
734
|
+
// the rest of governance (ownership) has none, so only that part is compared here.
|
|
735
|
+
const comparable = kind === 'object-type' ? withoutSensitivity(file.document) : file.document
|
|
736
|
+
const against = kind === 'object-type' ? withoutSensitivity(live) : live
|
|
737
|
+
const unmappable = Object.keys(comparable).filter((field) =>
|
|
738
|
+
!applicable.has(field) && !satisfies(comparable[field], against[field], field))
|
|
603
739
|
if (kind === 'object-type') unmappable.push(...unmappablePropertyFields(file.document, live))
|
|
604
740
|
if (unmappable.length === 0) return
|
|
741
|
+
const absent = NOT_CARRIED[kind]
|
|
605
742
|
throw new CliError(
|
|
606
743
|
`${file.path}: ${unmappable.join(', ')} ${unmappable.length === 1 ? 'differs' : 'differ'} from the draft, `
|
|
607
744
|
+ 'and no authoring route can apply that.',
|
|
@@ -610,15 +747,40 @@ export function assertApplicable(
|
|
|
610
747
|
hint: kind === 'metric' && unmappable.includes('objectType')
|
|
611
748
|
? 'A metric measures one object type for its lifetime. To move it, delete the file and '
|
|
612
749
|
+ '`apply --prune`, then write it again against the new object type.'
|
|
613
|
-
:
|
|
614
|
-
|
|
750
|
+
: absent && unmappable.every((field) => absent.has(field))
|
|
751
|
+
? `A ${kind} has no ${[...absent].join(' or ')}: the Blueprint keeps no such value for it. `
|
|
752
|
+
+ `Remove ${unmappable.map((field) => `\`${field}\``).join(' and ')} from ${file.path} and apply again.`
|
|
753
|
+
: `Remove ${unmappable.length === 1 ? 'that field' : 'those fields'} from ${file.path}, or set `
|
|
754
|
+
+ `${unmappable.length === 1 ? 'it' : 'them'} back to the draft's value, and apply again: a file that `
|
|
755
|
+
+ 'leaves a field out keeps the draft\'s. Applying would report a change that did not happen.',
|
|
615
756
|
},
|
|
616
757
|
)
|
|
617
758
|
}
|
|
618
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Fields a kind's file names in other kinds' shapes but that the kind itself does
|
|
762
|
+
* not have: no route sets them, and no screen does either, so the only fix is to
|
|
763
|
+
* leave them out. A metric's draft entry has no lifecycle status or visibility
|
|
764
|
+
* (`blueprintMetricDefinitionSchemaV1` is strict), and the metric file schema no
|
|
765
|
+
* longer lists them; this names the fix for a file written before it changed.
|
|
766
|
+
*/
|
|
767
|
+
const NOT_CARRIED: Partial<Record<ArtifactKind, Set<string>>> = {
|
|
768
|
+
metric: new Set(['status', 'visibility']),
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** A document with `governance.sensitivity` (and a top-level `sensitivity`) left out: the part of governance no route takes. */
|
|
772
|
+
export function withoutSensitivity(document: Record<string, unknown>): Record<string, unknown> {
|
|
773
|
+
const { sensitivity: _top, ...rest } = document
|
|
774
|
+
const governance = rest.governance
|
|
775
|
+
if (!governance || typeof governance !== 'object' || Array.isArray(governance)) return rest
|
|
776
|
+
const { sensitivity: _nested, ...others } = governance as Record<string, unknown>
|
|
777
|
+
return Object.keys(others).length ? { ...rest, governance: others } : (({ governance: _g, ...noGovernance }) => noGovernance)(rest)
|
|
778
|
+
}
|
|
779
|
+
|
|
619
780
|
/**
|
|
620
781
|
* Property fields the field commands do not carry. A property's `calculation`
|
|
621
|
-
* (a location
|
|
782
|
+
* can only be ADDED (a location, by `applyCalculatedFields`), not changed, and
|
|
783
|
+
* its own `sensitivity` has no route from a file:
|
|
622
784
|
* applying one would report a classification or a formula that never changed.
|
|
623
785
|
* Named as `properties.<apiName>.<field>`.
|
|
624
786
|
*/
|
|
@@ -629,13 +791,76 @@ function unmappablePropertyFields(document: Record<string, unknown>, live: Recor
|
|
|
629
791
|
const liveProperties = Array.isArray(live.properties) ? live.properties as Array<Record<string, unknown>> : []
|
|
630
792
|
const liveByName = new Map(liveProperties.map((property) => [String(property.apiName), property]))
|
|
631
793
|
return declared.flatMap((property) => {
|
|
632
|
-
const
|
|
794
|
+
const live = liveByName.get(String(property.apiName))
|
|
795
|
+
const counterpart = live ?? {}
|
|
633
796
|
return UNAPPLICABLE_PROPERTY_FIELDS
|
|
797
|
+
// A location calculation not on the draft yet is added (`applyCalculatedFields`); one there can't be changed.
|
|
798
|
+
.filter((field) => !(field === 'calculation' && !live && isLocation(property.calculation)))
|
|
634
799
|
.filter((field) => property[field] !== undefined && !satisfies(property[field], counterpart[field], field))
|
|
635
800
|
.map((field) => `properties.${String(property.apiName)}.${field}`)
|
|
636
801
|
})
|
|
637
802
|
}
|
|
638
803
|
|
|
804
|
+
/** A location calculation as a file writes it: `{ kind: location, latitude, longitude }`, by apiName. */
|
|
805
|
+
function isLocation(value: unknown): value is { kind: 'location'; latitude: string; longitude: string } {
|
|
806
|
+
const v = value as { kind?: unknown; latitude?: unknown; longitude?: unknown } | null
|
|
807
|
+
return !!v && v.kind === 'location' && typeof v.latitude === 'string' && typeof v.longitude === 'string'
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* The type's calculated location fields that the draft does not have yet:
|
|
812
|
+
* each added by `add_field` with its calculation, naming its latitude and
|
|
813
|
+
* longitude fields by this draft's ids. The create route and the property
|
|
814
|
+
* plan leave them out, since they read no column.
|
|
815
|
+
*/
|
|
816
|
+
async function applyCalculatedFields(client: BlueprintAuthoringApi, apiName: string, file: AuthoredFile, revision: number): Promise<number> {
|
|
817
|
+
const declared = Array.isArray(file.document.properties) ? (file.document.properties as Array<Record<string, unknown>>) : []
|
|
818
|
+
const calculated = declared.filter((p) => p.calculation !== undefined)
|
|
819
|
+
if (!calculated.length) return revision
|
|
820
|
+
const detail = await client.getObjectTypeDetail(apiName)
|
|
821
|
+
if (!detail?.id) return revision
|
|
822
|
+
const idByApiName = new Map(detail.properties.filter((p) => p.apiName && p.id).map((p) => [String(p.apiName), String(p.id)]))
|
|
823
|
+
let current = revision
|
|
824
|
+
for (const property of calculated) {
|
|
825
|
+
const name = String(property.apiName)
|
|
826
|
+
if (idByApiName.has(name)) continue
|
|
827
|
+
const calc = property.calculation
|
|
828
|
+
if (!isLocation(calc)) {
|
|
829
|
+
throw new CliError(`${file.path}: property "${name}" has a calculation that cannot be authored from a file.`, {
|
|
830
|
+
code: 'USAGE',
|
|
831
|
+
hint: 'A file adds a location: calculation: { kind: location, latitude: <field>, longitude: <field> }.',
|
|
832
|
+
})
|
|
833
|
+
}
|
|
834
|
+
const latitudePropertyId = idByApiName.get(calc.latitude)
|
|
835
|
+
const longitudePropertyId = idByApiName.get(calc.longitude)
|
|
836
|
+
if (!latitudePropertyId || !longitudePropertyId) {
|
|
837
|
+
throw new CliError(`${file.path}: "${name}" is a location from ${calc.latitude} and ${calc.longitude}, and ${!latitudePropertyId ? calc.latitude : calc.longitude} is not one of ${apiName}'s fields.`, {
|
|
838
|
+
code: 'USAGE',
|
|
839
|
+
hint: 'A location names two number fields of the same type, by apiName.',
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
const result = await client.draftCommand({
|
|
843
|
+
kind: 'add_field',
|
|
844
|
+
objectId: detail.id,
|
|
845
|
+
field: {
|
|
846
|
+
id: crypto.randomUUID(),
|
|
847
|
+
apiName: name,
|
|
848
|
+
displayName: String(property.displayName ?? name),
|
|
849
|
+
valueType: 'geopoint',
|
|
850
|
+
propertyType: 'attribute',
|
|
851
|
+
sourceColumn: '',
|
|
852
|
+
calculation: { kind: 'location', latitudePropertyId, longitudePropertyId },
|
|
853
|
+
required: false,
|
|
854
|
+
unique: false,
|
|
855
|
+
status: 'active',
|
|
856
|
+
visibility: 'normal',
|
|
857
|
+
},
|
|
858
|
+
}, current)
|
|
859
|
+
current = result.revision
|
|
860
|
+
}
|
|
861
|
+
return current
|
|
862
|
+
}
|
|
863
|
+
|
|
639
864
|
/**
|
|
640
865
|
* `primaryKey` and `title` — the two references a file states by property apiName.
|
|
641
866
|
*
|
|
@@ -722,6 +947,9 @@ export function objectTypeMetadataChanges(
|
|
|
722
947
|
if (Array.isArray(groups) && JSON.stringify(groups) !== JSON.stringify(live?.groups ?? [])) {
|
|
723
948
|
patch.groups = groups
|
|
724
949
|
}
|
|
950
|
+
// The classification: the update's top-level `sensitivity`, sent when it differs from the draft's.
|
|
951
|
+
const sensitivity = objectTypeSensitivity(document)
|
|
952
|
+
if (sensitivity && !satisfies(sensitivity, live ? objectTypeSensitivity(live) : undefined)) patch.sensitivity = sensitivity
|
|
725
953
|
return patch
|
|
726
954
|
}
|
|
727
955
|
|
|
@@ -768,6 +996,8 @@ function planProperties(
|
|
|
768
996
|
live: Array<Record<string, unknown>>,
|
|
769
997
|
backing: ReturnType<typeof readBacking>,
|
|
770
998
|
idByApiName: Map<string, string>,
|
|
999
|
+
/** The live object type's classification: a field at it takes it, rather than holding its own. */
|
|
1000
|
+
liveObjectSensitivity?: unknown,
|
|
771
1001
|
): PropertyPlan {
|
|
772
1002
|
const declared = Array.isArray(file.document.properties)
|
|
773
1003
|
? (file.document.properties as Array<Record<string, unknown>>)
|
|
@@ -781,7 +1011,8 @@ function planProperties(
|
|
|
781
1011
|
const name = String(property.apiName)
|
|
782
1012
|
const counterpart = liveByName.get(name)
|
|
783
1013
|
if (!counterpart) {
|
|
784
|
-
plan
|
|
1014
|
+
// Added after the plan, once its latitude and longitude fields are there (`applyCalculatedFields`).
|
|
1015
|
+
if (!property.calculation) plan.adds.push(property)
|
|
785
1016
|
continue
|
|
786
1017
|
}
|
|
787
1018
|
const schemaPatch: Record<string, unknown> = {}
|
|
@@ -836,6 +1067,16 @@ function planProperties(
|
|
|
836
1067
|
else metadataPatch.semanticType = wanted
|
|
837
1068
|
}
|
|
838
1069
|
}
|
|
1070
|
+
// The field's own classification, against what it holds now. A read gives every field its
|
|
1071
|
+
// effective one, so a value at the object type's is "takes the object's" on both sides; the
|
|
1072
|
+
// file's absence (or `null`, or its object's own value) is that too, and travels as `null`.
|
|
1073
|
+
// Not on a location, which takes its latitude's and longitude's.
|
|
1074
|
+
if (!property.calculation) {
|
|
1075
|
+
const wanted = fieldSensitivityOf(property, objectTypeSensitivity(file.document)).sensitivity ?? null
|
|
1076
|
+
const liveClass = liveObjectSensitivity && typeof liveObjectSensitivity === 'object' ? (liveObjectSensitivity as { classification?: unknown }).classification : undefined
|
|
1077
|
+
const current = typeof counterpart.sensitivity === 'string' && counterpart.sensitivity !== liveClass ? counterpart.sensitivity : null
|
|
1078
|
+
if (wanted !== current) metadataPatch.sensitivity = wanted
|
|
1079
|
+
}
|
|
839
1080
|
const id = idByApiName.get(name)
|
|
840
1081
|
// A declared property with no live id is either brand new (handled above) or the
|
|
841
1082
|
// draft moved; skipping it quietly is how a change goes missing.
|
|
@@ -881,7 +1122,7 @@ async function applyPropertyPlan(
|
|
|
881
1122
|
const liveProperties = Array.isArray(liveDocument?.properties)
|
|
882
1123
|
? (liveDocument.properties as Array<Record<string, unknown>>)
|
|
883
1124
|
: []
|
|
884
|
-
const plan = planProperties(file, liveProperties, backing, idByApiName)
|
|
1125
|
+
const plan = planProperties(file, liveProperties, backing, idByApiName, objectTypeSensitivity(liveDocument ?? {}))
|
|
885
1126
|
|
|
886
1127
|
// Every add needs a column, and the service offers no name-match fallback. Checked
|
|
887
1128
|
// for ALL of them before the first write, so a partial run cannot start.
|
|
@@ -940,7 +1181,7 @@ async function applyPropertyPlan(
|
|
|
940
1181
|
id: crypto.randomUUID(),
|
|
941
1182
|
apiName: name,
|
|
942
1183
|
displayName: String(property.displayName ?? name),
|
|
943
|
-
propertyType: (property.
|
|
1184
|
+
propertyType: declaredPropertyType(property, ids.sharedPropertyTypeByApiName),
|
|
944
1185
|
valueType: String(property.valueType ?? 'string'),
|
|
945
1186
|
sourceColumn: column,
|
|
946
1187
|
...(sourceField === undefined || sourceField === null ? {} : { sourceField }),
|
|
@@ -955,8 +1196,10 @@ async function applyPropertyPlan(
|
|
|
955
1196
|
// Same collapse: `add_field`'s uuid is not nullable, and a field being
|
|
956
1197
|
// created has no meaning to detach from.
|
|
957
1198
|
...(typeof semanticTypeId === 'string' ? { semanticTypeId } : {}),
|
|
1199
|
+
// Its own classification, when the file gives one other than the object type's.
|
|
1200
|
+
...fieldSensitivityOf(property, objectTypeSensitivity(file.document)),
|
|
958
1201
|
},
|
|
959
|
-
}, current)
|
|
1202
|
+
}, current).catch((err) => { throw stewardRefusal(err, file, name, fieldSensitivityOf(property, objectTypeSensitivity(file.document)).sensitivity !== undefined) })
|
|
960
1203
|
current = result.revision
|
|
961
1204
|
}
|
|
962
1205
|
for (const change of plan.schemaChanges) {
|
|
@@ -984,7 +1227,7 @@ async function applyPropertyPlan(
|
|
|
984
1227
|
...semanticTypeOnWire(file, change.apiName, rest, ids),
|
|
985
1228
|
...(sharedPropertyId === undefined ? {} : { sharedPropertyId }),
|
|
986
1229
|
},
|
|
987
|
-
}, current)
|
|
1230
|
+
}, current).catch((err) => { throw stewardRefusal(err, file, change.apiName, 'sensitivity' in rest) })
|
|
988
1231
|
current = result.revision
|
|
989
1232
|
}
|
|
990
1233
|
for (const removal of plan.removes) {
|
|
@@ -996,6 +1239,20 @@ async function applyPropertyPlan(
|
|
|
996
1239
|
return current
|
|
997
1240
|
}
|
|
998
1241
|
|
|
1242
|
+
/**
|
|
1243
|
+
* A refusal of a command that classifies a field, said as what it needs: a
|
|
1244
|
+
* field's classification is a Blueprint Steward's to set, as its object
|
|
1245
|
+
* type's is, and a key without that role is refused for it alone.
|
|
1246
|
+
*/
|
|
1247
|
+
function stewardRefusal(err: unknown, file: AuthoredFile, field: string, classifies: boolean): unknown {
|
|
1248
|
+
if (!classifies || !(err instanceof CliError) || (err.code !== 'FORBIDDEN' && err.code !== 'UNAUTHORIZED')) return err
|
|
1249
|
+
return new CliError(`${file.path}: field "${field}" sets its own classification (sensitivity), which only a Blueprint Steward may do: ${err.message}`, {
|
|
1250
|
+
code: err.code,
|
|
1251
|
+
hint: 'apply it with a credential of a Blueprint Steward, or leave `sensitivity` out so the field takes its object type\'s',
|
|
1252
|
+
cause: err,
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
|
|
999
1256
|
/**
|
|
1000
1257
|
* The file's `semanticType` name, replaced by the wire's `semanticTypeId`.
|
|
1001
1258
|
*
|
|
@@ -1068,7 +1325,9 @@ export const blueprintApply: Command = {
|
|
|
1068
1325
|
async run(ctx) {
|
|
1069
1326
|
const client = api(ctx)
|
|
1070
1327
|
const prune = ctx.flags.prune === true
|
|
1071
|
-
const { plan, files, definition, drift } = await computePlan(ctx)
|
|
1328
|
+
const { plan, files, definition, drift, actionProblems, inheritedWrites } = await computePlan(ctx)
|
|
1329
|
+
assertActionsApplicable(actionProblems, plan.operations)
|
|
1330
|
+
const actionWarnings = renderActionProblems({ refused: [], undeployable: actionProblems.undeployable, refusedActions: [], ...(actionProblems.inherited ? { inherited: actionProblems.inherited } : {}) })
|
|
1072
1331
|
|
|
1073
1332
|
// BEFORE the first write, not before the prune loop: the operations land first,
|
|
1074
1333
|
// so discovering the contradiction between them and the prunes at the end would
|
|
@@ -1076,7 +1335,7 @@ export const blueprintApply: Command = {
|
|
|
1076
1335
|
if (prune) assertPrunesLeaveNoDanglingReference(files, plan.prunes)
|
|
1077
1336
|
|
|
1078
1337
|
if (isEmpty(plan)) {
|
|
1079
|
-
return { data: { ...plan, applied: [] }, text: renderPlan(plan) }
|
|
1338
|
+
return { data: { ...plan, applied: [], actionProblems }, text: [renderPlan(plan), actionWarnings].filter(Boolean).join('\n') }
|
|
1080
1339
|
}
|
|
1081
1340
|
|
|
1082
1341
|
// Drift is REPORTED, and blocks only the artifacts it is about. It used to throw
|
|
@@ -1117,6 +1376,8 @@ export const blueprintApply: Command = {
|
|
|
1117
1376
|
|
|
1118
1377
|
const byKey = new Map(files.map((file) => [`${file.kind}:${file.apiName}`, file]))
|
|
1119
1378
|
const applied: string[] = []
|
|
1379
|
+
// The step being written when a write fails: said, so a caller can tell which file was refused.
|
|
1380
|
+
let current: string | null = null
|
|
1120
1381
|
try {
|
|
1121
1382
|
// Seeded from the revision the PLAN was computed against, then advanced by what
|
|
1122
1383
|
// each write returns. Never re-read — see `nextRevision`.
|
|
@@ -1129,18 +1390,27 @@ export const blueprintApply: Command = {
|
|
|
1129
1390
|
const ids: AuthoringIds = {
|
|
1130
1391
|
sharedPropertyIdByApiName: live.sharedPropertyIdByApiName,
|
|
1131
1392
|
semanticTypeIdByApiName: live.semanticTypeIdByApiName,
|
|
1393
|
+
sharedPropertyTypeByApiName: new Map(
|
|
1394
|
+
(definition.sharedProperties ?? []).flatMap((shared) => {
|
|
1395
|
+
const type = (shared as { propertyType?: unknown }).propertyType
|
|
1396
|
+
return typeof type === 'string' ? [[shared.apiName, type] as const] : []
|
|
1397
|
+
}),
|
|
1398
|
+
),
|
|
1132
1399
|
}
|
|
1133
1400
|
for (const operation of ordered) {
|
|
1134
1401
|
const file = byKey.get(`${operation.kind}:${operation.apiName}`)!
|
|
1135
|
-
|
|
1136
|
-
|
|
1402
|
+
current = `${operation.operation} ${operation.kind} ${operation.apiName}`
|
|
1403
|
+
revision = await applyOperation(client, operation, file, revision, ids, inheritedWrites)
|
|
1404
|
+
applied.push(current)
|
|
1137
1405
|
}
|
|
1138
1406
|
if (prune) {
|
|
1139
1407
|
for (const operation of orderedPrunes) {
|
|
1408
|
+
current = `delete ${operation.kind} ${operation.apiName}`
|
|
1140
1409
|
revision = await pruneOperation(client, operation, revision)
|
|
1141
|
-
applied.push(
|
|
1410
|
+
applied.push(current)
|
|
1142
1411
|
}
|
|
1143
1412
|
}
|
|
1413
|
+
current = null
|
|
1144
1414
|
} catch (err) {
|
|
1145
1415
|
// What landed is as important as what failed: the draft is shared, and a
|
|
1146
1416
|
// partial run that does not say where it stopped cannot be resumed safely.
|
|
@@ -1152,8 +1422,9 @@ export const blueprintApply: Command = {
|
|
|
1152
1422
|
// failure — a draft that moved — handed out for every failure, so a
|
|
1153
1423
|
// validation refusal read as "Validation failed / re-plan", which no
|
|
1154
1424
|
// amount of re-planning fixes.
|
|
1155
|
-
const inner = err instanceof CliError ? err.hint : undefined
|
|
1156
|
-
|
|
1425
|
+
const inner = sourceSystemHint(`${(err as Error).message}\n${err instanceof CliError ? err.hint ?? '' : ''}`) ?? (err instanceof CliError ? err.hint : undefined)
|
|
1426
|
+
const stopped = current ? `Stopped at: ${current}\n` : ''
|
|
1427
|
+
throw new CliError(`${done}${stopped}${(err as Error).message}`, {
|
|
1157
1428
|
code: err instanceof CliError ? err.code : 'FAILURE',
|
|
1158
1429
|
hint: inner ?? 'frontera blueprint plan — re-plan against the current draft, then re-apply.',
|
|
1159
1430
|
})
|
|
@@ -1161,9 +1432,10 @@ export const blueprintApply: Command = {
|
|
|
1161
1432
|
|
|
1162
1433
|
const revision = await client.revision()
|
|
1163
1434
|
return {
|
|
1164
|
-
data: { applied, revision },
|
|
1435
|
+
data: { applied, revision, actionProblems },
|
|
1165
1436
|
text: `${renderPlan(plan, { prune })}\n\nApplied ${applied.length} change`
|
|
1166
|
-
+ `${applied.length === 1 ? '' : 's'}. Draft is at revision ${revision}
|
|
1437
|
+
+ `${applied.length === 1 ? '' : 's'}. Draft is at revision ${revision}.`
|
|
1438
|
+
+ (actionWarnings ? `\n${actionWarnings}` : ''),
|
|
1167
1439
|
}
|
|
1168
1440
|
},
|
|
1169
1441
|
}
|
|
@@ -1372,7 +1644,7 @@ export const blueprintNew: Command = {
|
|
|
1372
1644
|
{ name: 'kind', required: true, description: `One of: ${ARTIFACT_KINDS.join(', ')}` },
|
|
1373
1645
|
{ name: 'apiName', required: true, description: 'The artifact’s API name' },
|
|
1374
1646
|
],
|
|
1375
|
-
flags: { dir: 'string', template: 'string' },
|
|
1647
|
+
flags: { dir: 'string', template: 'string', 'object-type': 'string' },
|
|
1376
1648
|
offline: true,
|
|
1377
1649
|
summary: 'Scaffold an artifact file that validates as written',
|
|
1378
1650
|
examples: [
|
|
@@ -1380,6 +1652,8 @@ export const blueprintNew: Command = {
|
|
|
1380
1652
|
'frontera blueprint new link-type customerHoldsPolicy',
|
|
1381
1653
|
'frontera blueprint new shared-field accountCode',
|
|
1382
1654
|
'frontera blueprint new semantic-type completionRate --template percentage',
|
|
1655
|
+
'frontera blueprint new action assignTicket --object-type Ticket',
|
|
1656
|
+
'frontera blueprint new action logTicket --template create --object-type Ticket',
|
|
1383
1657
|
],
|
|
1384
1658
|
},
|
|
1385
1659
|
async run(ctx) {
|
|
@@ -1403,8 +1677,8 @@ export const blueprintNew: Command = {
|
|
|
1403
1677
|
hint: `${describeApiNameRule(kind)} — and it becomes the file name, so nothing else fits.`,
|
|
1404
1678
|
})
|
|
1405
1679
|
}
|
|
1406
|
-
const template = semanticTypeTemplate(ctx.flags.template, kind)
|
|
1407
1680
|
const directory = root(ctx)
|
|
1681
|
+
const options = scaffoldOptions(ctx, kind, directory)
|
|
1408
1682
|
const relative = pathFor(kind, apiName)
|
|
1409
1683
|
if (existsSync(resolve(directory, relative))) {
|
|
1410
1684
|
throw new CliError(`${relative} already exists.`, {
|
|
@@ -1416,7 +1690,7 @@ export const blueprintNew: Command = {
|
|
|
1416
1690
|
kind,
|
|
1417
1691
|
apiName,
|
|
1418
1692
|
path: relative,
|
|
1419
|
-
document: scaffold(kind, apiName,
|
|
1693
|
+
document: scaffold(kind, apiName, options),
|
|
1420
1694
|
}
|
|
1421
1695
|
writeTree(directory, [file])
|
|
1422
1696
|
return {
|
|
@@ -1426,6 +1700,32 @@ export const blueprintNew: Command = {
|
|
|
1426
1700
|
},
|
|
1427
1701
|
}
|
|
1428
1702
|
|
|
1703
|
+
/**
|
|
1704
|
+
* `--template` and `--object-type`, read for the kind they apply to. An Action's
|
|
1705
|
+
* subject type, when the tree holds it, also gives the key input its name.
|
|
1706
|
+
*/
|
|
1707
|
+
function scaffoldOptions(ctx: CommandContext, kind: ArtifactKind, directory: string): ScaffoldOptions {
|
|
1708
|
+
const objectType = typeof ctx.flags['object-type'] === 'string' ? ctx.flags['object-type'] : undefined
|
|
1709
|
+
if (objectType !== undefined && kind !== 'action') {
|
|
1710
|
+
throw new CliError(`--object-type applies to action, not ${kind}.`, { code: 'USAGE', hint: 'frontera blueprint new action <apiName> --object-type <ObjectType>' })
|
|
1711
|
+
}
|
|
1712
|
+
if (kind !== 'action') {
|
|
1713
|
+
const template = semanticTypeTemplate(ctx.flags.template, kind)
|
|
1714
|
+
return template === undefined ? {} : { template }
|
|
1715
|
+
}
|
|
1716
|
+
const declared = ctx.flags.template
|
|
1717
|
+
if (declared !== undefined && !isActionTemplate(String(declared))) {
|
|
1718
|
+
throw new CliError(`Unknown action template "${String(declared)}".`, { code: 'USAGE', hint: `One of: ${ACTION_TEMPLATES.join(', ')} (edit changes a record; create makes one).` })
|
|
1719
|
+
}
|
|
1720
|
+
const options: ScaffoldOptions = { ...(declared === undefined ? {} : { actionTemplate: String(declared) as ActionTemplate }) }
|
|
1721
|
+
if (objectType) {
|
|
1722
|
+
options.objectType = objectType
|
|
1723
|
+
const type = existsSync(directory) ? readTree(directory).find((f) => f.kind === 'object-type' && f.apiName === objectType) : undefined
|
|
1724
|
+
if (typeof type?.document.primaryKey === 'string') options.primaryKey = type.document.primaryKey
|
|
1725
|
+
}
|
|
1726
|
+
return options
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1429
1729
|
/**
|
|
1430
1730
|
* `--template`, validated where it is read.
|
|
1431
1731
|
*
|