@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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { API_NAME_RULE, describeApiNameRule, KIND_DIRECTORY, type ArtifactKind } from '../blueprint/model'
|
|
2
|
+
import { BLUEPRINT_DATA_TYPES } from '../blueprint-types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Blueprint's file formats (`blueprint/<kind>/<apiName>.yaml`), as JSON
|
|
6
|
+
* Schema, for `frontera local schema blueprint-<kind>`.
|
|
7
|
+
*
|
|
8
|
+
* Only this CLI reads these files: `blueprint apply` turns each into the
|
|
9
|
+
* service's create body (`blueprint/compile.ts`), and `blueprint pull` and
|
|
10
|
+
* `blueprint new` write them (`blueprint/projection.ts`, `blueprint/scaffold.ts`).
|
|
11
|
+
* So the schemas are built from those readers' own rules: the apiName rule per
|
|
12
|
+
* kind (`API_NAME_RULE`), the value types (`BLUEPRINT_DATA_TYPES`), and the
|
|
13
|
+
* keys each reader requires. Every reference is a NAME (`objectType: Customer`),
|
|
14
|
+
* never an id: the tree carries none. What the service checks beyond the shape
|
|
15
|
+
* (a rule's grammar, an expression) is left open and refused, with its path,
|
|
16
|
+
* when the file is applied.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DRAFT = 'https://json-schema.org/draft/2020-12/schema'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The source system an Action names when it writes the Blueprint's own records:
|
|
23
|
+
* the service adds it to the draft with the first object type
|
|
24
|
+
* (`blueprint-draft-catalog.ts`); source systems have no route or file of their own.
|
|
25
|
+
*/
|
|
26
|
+
export const ACTIVE_CATALOG = 'active-catalog'
|
|
27
|
+
|
|
28
|
+
type JsonSchema = Record<string, unknown>
|
|
29
|
+
|
|
30
|
+
function apiName(kind: ArtifactKind): JsonSchema {
|
|
31
|
+
return {
|
|
32
|
+
type: 'string',
|
|
33
|
+
pattern: API_NAME_RULE[kind].source,
|
|
34
|
+
description: `${describeApiNameRule(kind)}. It is the file's name too: blueprint/${KIND_DIRECTORY[kind]}/<apiName>.yaml.`,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A name another file declares, by its apiName. */
|
|
39
|
+
const ref = (what: string, kind: ArtifactKind): JsonSchema => ({ type: 'string', pattern: API_NAME_RULE[kind].source, description: `${what}, by apiName.` })
|
|
40
|
+
|
|
41
|
+
const text = (description?: string): JsonSchema => ({ type: 'string', minLength: 1, ...(description ? { description } : {}) })
|
|
42
|
+
|
|
43
|
+
const VALUE_TYPE: JsonSchema = { enum: [...BLUEPRINT_DATA_TYPES], description: 'The value type.' }
|
|
44
|
+
const PROPERTY_TYPE: JsonSchema = {
|
|
45
|
+
enum: ['attribute', 'measure', 'time'],
|
|
46
|
+
description: 'How the field is read. `attribute`: a value you filter and group by with equals and in (the default for every field but a date or timestamp). `measure`: a number a metric adds up or averages, compared with lt/gt. `time`: a date or timestamp, the default for one; only a time field can be a metric\'s `timeseries` or be compared in a query or workflow `where` (`dueAt < $now` is refused on an attribute), so never write `attribute` on a date or timestamp you compare or chart over time.',
|
|
47
|
+
}
|
|
48
|
+
const SENSITIVITY: JsonSchema = { enum: ['internal', 'confidential', 'restricted'] }
|
|
49
|
+
const STATUS: JsonSchema = { type: 'string', description: 'Lifecycle status the Blueprint shows (for example active, experimental, deprecated).' }
|
|
50
|
+
const VISIBILITY: JsonSchema = { type: 'string', description: 'Where it is shown (for example normal, prominent, hidden).' }
|
|
51
|
+
|
|
52
|
+
/** Metadata every artifact may carry. */
|
|
53
|
+
const DISPLAY = {
|
|
54
|
+
displayName: text('What people see.'),
|
|
55
|
+
description: { type: 'string' },
|
|
56
|
+
status: STATUS,
|
|
57
|
+
visibility: VISIBILITY,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const OBJECT_TYPE: JsonSchema = {
|
|
61
|
+
$schema: DRAFT,
|
|
62
|
+
title: 'Blueprint object type (blueprint/object-types/<ApiName>.yaml)',
|
|
63
|
+
description:
|
|
64
|
+
'A kind of record, read from a dataset. `frontera blueprint apply` creates it with every property and the column each reads; `primaryKey` and `title` name properties by apiName. `validations` and `lifecycle` cannot be created from a file (the create route has no channel for them).',
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
apiName: apiName('object-type'),
|
|
68
|
+
...DISPLAY,
|
|
69
|
+
pluralDisplayName: text('The plural people see.'),
|
|
70
|
+
icon: { type: 'string' },
|
|
71
|
+
color: { type: 'string' },
|
|
72
|
+
groups: { type: 'array', items: { type: 'string' } },
|
|
73
|
+
primaryKey: text('The property that identifies a record, by apiName.'),
|
|
74
|
+
title: text('The property that names a record, by apiName.'),
|
|
75
|
+
rowAccess: { anyOf: [{ type: 'string' }, { type: 'null' }], description: 'How rows are restricted, when they are.' },
|
|
76
|
+
markingsColumn: { anyOf: [{ type: 'string' }, { type: 'null' }], description: 'The column holding each row’s markings; null detaches it.' },
|
|
77
|
+
governance: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
description: 'Ownership and sensitivity. The sensitivity (classification and categories) is applied on create and on update; ownership is set in the Console, and a file whose ownership differs from the draft is refused.',
|
|
80
|
+
properties: {
|
|
81
|
+
ownership: { type: 'object', properties: { kind: { type: 'string' }, ownerId: { type: 'string' } } },
|
|
82
|
+
sensitivity: { type: 'object', properties: { classification: SENSITIVITY, categories: { type: 'array', items: { type: 'string' } } } },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
properties: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
minItems: 1,
|
|
88
|
+
description: 'Every field. Each names the column it reads in backing.mapping.',
|
|
89
|
+
items: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
apiName: { type: 'string', pattern: '^[a-z][A-Za-z0-9]{0,99}$', description: 'camelCase.' },
|
|
93
|
+
displayName: text('Defaults to apiName.'),
|
|
94
|
+
description: { type: 'string', description: 'Sample data reads the values a description lists ("open, pending or closed").' },
|
|
95
|
+
valueType: VALUE_TYPE,
|
|
96
|
+
required: { type: 'boolean' },
|
|
97
|
+
unique: { type: 'boolean' },
|
|
98
|
+
propertyType: PROPERTY_TYPE,
|
|
99
|
+
formatConfig: { type: 'object' },
|
|
100
|
+
sharedField: { anyOf: [ref('The shared field it implements', 'shared-field'), { type: 'null' }], description: 'The shared field it implements, by apiName; null detaches it.' },
|
|
101
|
+
semanticType: { anyOf: [ref('The semantic type it carries', 'semantic-type'), { type: 'null' }], description: 'The semantic type its values follow (allowed values, bounds), by apiName; null detaches it.' },
|
|
102
|
+
calculation: { type: 'object', description: 'A calculated field. A location names its latitude and longitude fields: { kind: location, latitude, longitude }.' },
|
|
103
|
+
sensitivity: {
|
|
104
|
+
anyOf: [{ enum: ['public', 'internal', 'confidential', 'restricted'] }, { type: 'null' }],
|
|
105
|
+
description: "The field's own classification, when it differs from its object type's (`governance.sensitivity.classification`), which every field without one takes: lower for a key that only identifies a record (a `confidential` Customer's `customerId` as `internal`, so references to it resolve), higher for one field of personal data. Leave it out, or `null`, to take the object type's. A location takes its latitude's and longitude's and declares none. Set by a Blueprint Steward, as the object type's classification is.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ['apiName'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
validations: { type: 'array', maxItems: 0, description: 'Must be empty: validations are added in the platform, not from a file.' },
|
|
112
|
+
backing: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
description: 'Where the records come from: the dataset by NAME (its revision is resolved when applied), and one column per property. There is no name-match fallback.',
|
|
115
|
+
properties: {
|
|
116
|
+
dataset: text('The dataset’s apiName (datasets/<apiName>.json).'),
|
|
117
|
+
mapping: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
description: 'property apiName → its column, or { column, field } for a field inside a column.',
|
|
120
|
+
additionalProperties: {
|
|
121
|
+
anyOf: [
|
|
122
|
+
{ type: 'string', minLength: 1 },
|
|
123
|
+
{ type: 'object', properties: { column: text(), field: { anyOf: [{ type: 'string' }, { type: 'null' }] } }, required: ['column'], additionalProperties: false },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
editable: { type: 'array', items: { type: 'string' }, description: 'The properties a governed Action may write.' },
|
|
128
|
+
},
|
|
129
|
+
required: ['dataset', 'mapping'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['apiName', 'displayName', 'pluralDisplayName', 'primaryKey', 'title', 'properties', 'backing'],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ENDPOINT: JsonSchema = {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
objectType: ref('The object type at this end', 'object-type'),
|
|
139
|
+
property: text('The property the link joins on, by apiName.'),
|
|
140
|
+
displayName: text('How this end is named from the other one; defaults to the other type.'),
|
|
141
|
+
phrase: { type: 'string', description: 'The verb phrase read from this end ("has", "belongs to").' },
|
|
142
|
+
},
|
|
143
|
+
required: ['objectType', 'property'],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const LINK_TYPE: JsonSchema = {
|
|
147
|
+
$schema: DRAFT,
|
|
148
|
+
title: 'Blueprint link type (blueprint/link-types/<apiName>.yaml)',
|
|
149
|
+
description:
|
|
150
|
+
'A relation between two object types, joined on a property of each. For one_to_many and one_to_one the `to` side’s property holds the `from` side’s key; sample data fills it from the other type’s records.',
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
apiName: apiName('link-type'),
|
|
154
|
+
description: { type: 'string' },
|
|
155
|
+
cardinality: { enum: ['one_to_one', 'one_to_many', 'many_to_many'], default: 'one_to_many' },
|
|
156
|
+
from: ENDPOINT,
|
|
157
|
+
to: ENDPOINT,
|
|
158
|
+
},
|
|
159
|
+
required: ['apiName', 'from', 'to'],
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const SHARED_FIELD: JsonSchema = {
|
|
163
|
+
$schema: DRAFT,
|
|
164
|
+
title: 'Blueprint shared field (blueprint/shared-fields/<apiName>.yaml)',
|
|
165
|
+
description: 'What a field means wherever it is used: identity and semantics, no column. An object type’s property implements it with `sharedField: <apiName>`.',
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
apiName: apiName('shared-field'),
|
|
169
|
+
...DISPLAY,
|
|
170
|
+
valueType: VALUE_TYPE,
|
|
171
|
+
propertyType: PROPERTY_TYPE,
|
|
172
|
+
formatConfig: { type: 'object' },
|
|
173
|
+
semanticType: { anyOf: [ref('The semantic type its values follow', 'semantic-type'), { type: 'null' }] },
|
|
174
|
+
},
|
|
175
|
+
required: ['apiName', 'displayName', 'valueType', 'propertyType'],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const SEMANTIC_TYPE: JsonSchema = {
|
|
179
|
+
$schema: DRAFT,
|
|
180
|
+
title: 'Blueprint semantic type (blueprint/semantic-types/<apiName>.yaml)',
|
|
181
|
+
description:
|
|
182
|
+
'A rule values follow, carried by a property or a shared field (`semanticType: <apiName>`). Sample data generates values within it. Bounds are canonical decimal strings ("0", "100"); the service checks the rule’s grammar when it is applied.',
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
apiName: apiName('semantic-type'),
|
|
186
|
+
...DISPLAY,
|
|
187
|
+
dataType: { enum: ['string', 'number', 'date', 'timestamp'], description: 'Which family the rule is read under.' },
|
|
188
|
+
rule: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
description: 'string: enum, minLength, maxLength; number, date, timestamp: minimum, maximum.',
|
|
191
|
+
properties: {
|
|
192
|
+
enum: { type: 'array', minItems: 1, items: { type: 'string' }, description: 'The allowed values.' },
|
|
193
|
+
minLength: { type: 'integer', minimum: 0 },
|
|
194
|
+
maxLength: { type: 'integer', minimum: 0 },
|
|
195
|
+
minimum: { type: 'string', description: 'A canonical decimal (or date) string.' },
|
|
196
|
+
maximum: { type: 'string', description: 'A canonical decimal (or date) string.' },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ['apiName', 'displayName', 'dataType', 'rule'],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A metric takes `displayName` and `description` from `DISPLAY`, not `status` or
|
|
205
|
+
* `visibility`: the Blueprint keeps neither for a metric (its draft entry has no
|
|
206
|
+
* such field), so `apply` refuses a file that sets one. Listing them here sent an
|
|
207
|
+
* agent that followed the schema into that refusal.
|
|
208
|
+
*/
|
|
209
|
+
const METRIC: JsonSchema = {
|
|
210
|
+
$schema: DRAFT,
|
|
211
|
+
title: 'Blueprint metric (blueprint/metrics/<apiName>.yaml)',
|
|
212
|
+
description:
|
|
213
|
+
'A measure over one object type, carried to the service as written (`objectType` becomes objectTypeApiName). '
|
|
214
|
+
+ 'A metric has no `status` or `visibility`: unlike the other kinds, the Blueprint keeps neither for it, and `apply` refuses a file that sets one.',
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
apiName: apiName('metric'),
|
|
218
|
+
displayName: DISPLAY.displayName,
|
|
219
|
+
description: DISPLAY.description,
|
|
220
|
+
objectType: ref('The object type it measures', 'object-type'),
|
|
221
|
+
formatConfig: { type: 'object', description: 'How the numbers are shown.' },
|
|
222
|
+
definition: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
measures: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
minItems: 1,
|
|
228
|
+
items: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
description: 'An aggregate (agg, property, label, filters…) or a composed measure (operands); the service checks the measure grammar when the file is applied.',
|
|
231
|
+
properties: { agg: { enum: ['sum', 'avg', 'min', 'max', 'count', 'countDistinct', 'share', 'ratio'] }, property: { type: 'string' }, label: text(), apiName: { type: 'string' } },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
dimensions: { type: 'array', items: {} },
|
|
235
|
+
timeseries: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'The time property the metric is charted over, by apiName (a `time` field). A query by grain, or over a window (`--range P7D`, `--start`/`--end`), reads it. A `--range` window ends at the newest value of it in the data, not at now.',
|
|
238
|
+
},
|
|
239
|
+
smallestGrain: { enum: ['day', 'week', 'month', 'quarter', 'year'], description: 'The finest grain the metric is meant to be read at.' },
|
|
240
|
+
},
|
|
241
|
+
required: ['measures'],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
required: ['apiName', 'objectType', 'definition'],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** An Action member's apiName (`apiNameSchema` in the service's Action contract). */
|
|
248
|
+
const MEMBER_NAME = '^[a-z][A-Za-z0-9]{0,99}$'
|
|
249
|
+
/** An outcome's, criterion's or postcondition's code (`stableCodeSchema`). */
|
|
250
|
+
const STABLE_CODE: JsonSchema = { type: 'string', pattern: '^[a-z][a-z0-9_]{0,99}$', description: 'A stable code, lowercase snake_case (`resolution_meaningful`), unique in its list.' }
|
|
251
|
+
|
|
252
|
+
const WHOLE = { type: 'integer', minimum: 0 }
|
|
253
|
+
|
|
254
|
+
/** An Action field's value type, one shape per kind; each takes only its own keys. */
|
|
255
|
+
const ACTION_VALUE_TYPE: JsonSchema = {
|
|
256
|
+
description:
|
|
257
|
+
'One of: string (minLength, maxLength, pattern, enum), number (integer — required — and minimum, maximum as NUMBERS), boolean, date, timestamp, json, geopoint, or object_reference with `objectType: <ApiName>`. Each kind takes only its own keys. A string `enum` is also what sample data writes into the property the input sets.',
|
|
258
|
+
oneOf: [
|
|
259
|
+
{
|
|
260
|
+
title: 'string',
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
kind: { const: 'string' },
|
|
264
|
+
minLength: WHOLE,
|
|
265
|
+
maxLength: WHOLE,
|
|
266
|
+
pattern: {
|
|
267
|
+
type: 'string',
|
|
268
|
+
pattern: '^\\^.*\\$$',
|
|
269
|
+
maxLength: 256,
|
|
270
|
+
description: 'Matches the whole value: starts with ^ and ends with $; characters, character classes and the escapes \\d \\w \\s (and capitals) or an escaped symbol; no groups, alternation or nesting; at most one variable-width repetition (* + ? {m,n}), each at most 64 (`^WO-[0-9A-Z]{4,12}$`).',
|
|
271
|
+
},
|
|
272
|
+
enum: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string' }, description: 'The allowed values, each once; each must fit minLength, maxLength and pattern.' },
|
|
273
|
+
},
|
|
274
|
+
required: ['kind'],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
title: 'number',
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
kind: { const: 'number' },
|
|
282
|
+
integer: { type: 'boolean', description: 'Required: true for a whole number, false for any number.' },
|
|
283
|
+
minimum: { type: 'number', description: 'A number (`minimum: 0`), not a decimal string: decimal strings are a semantic type’s spelling.' },
|
|
284
|
+
maximum: { type: 'number', description: 'A number, at least minimum.' },
|
|
285
|
+
},
|
|
286
|
+
required: ['kind', 'integer'],
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
},
|
|
289
|
+
...['boolean', 'date', 'timestamp', 'json', 'geopoint'].map((kind) => ({
|
|
290
|
+
title: kind,
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: { kind: { const: kind } },
|
|
293
|
+
required: ['kind'],
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
...(kind === 'date' ? { description: 'A date, YYYY-MM-DD.' } : kind === 'timestamp' ? { description: 'An ISO 8601 time with a zone: 2026-10-18T09:00:00Z.' } : kind === 'geopoint' ? { description: '{ lat, lon } in decimal degrees; it writes a location field. Takes no default.' } : {}),
|
|
296
|
+
})),
|
|
297
|
+
{
|
|
298
|
+
title: 'object_reference',
|
|
299
|
+
description: 'A record of the object type: a submission passes the record\'s primary key, as text (`TCH-00026`). Takes no default: only a record\'s UUID fits one, and that is not portable.',
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: { kind: { const: 'object_reference' }, objectType: ref('The object type it points at', 'object-type') },
|
|
302
|
+
required: ['kind', 'objectType'],
|
|
303
|
+
additionalProperties: false,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** An Action's input or output field, by name: the service mints its id. */
|
|
309
|
+
const ACTION_FIELD: JsonSchema = {
|
|
310
|
+
type: 'object',
|
|
311
|
+
properties: {
|
|
312
|
+
apiName: { type: 'string', pattern: MEMBER_NAME, description: 'camelCase. An edit or create on the Blueprint’s records takes the object type’s primary key as a required input of the same name (`id`); with expected_version concurrency it also takes `expectedVersion`.' },
|
|
313
|
+
displayName: text(),
|
|
314
|
+
description: text(),
|
|
315
|
+
valueType: ACTION_VALUE_TYPE,
|
|
316
|
+
required: { type: 'boolean' },
|
|
317
|
+
default: { type: ['string', 'number', 'boolean', 'null'], description: 'A JSON scalar that fits valueType. Only on a field with required: false: a required field cannot carry a default.' },
|
|
318
|
+
sensitivity: SENSITIVITY,
|
|
319
|
+
},
|
|
320
|
+
required: ['apiName', 'displayName', 'valueType', 'required', 'sensitivity'],
|
|
321
|
+
additionalProperties: false,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A value an expected change sets: an input or output by name, or a constant. */
|
|
325
|
+
const CHANGE_VALUE: JsonSchema = {
|
|
326
|
+
description: '{ kind: input, field: <input apiName> } (the usual), or { kind: literal, value: <scalar> } for a constant ("move to assigned": the platform writes it from the published contract). An Action needs at least one property set from an input besides its key, a constant never sets the key, and a property is set one way, not both. { kind: output } is part of the format, but a write over the Blueprint’s records does not deploy with it.',
|
|
327
|
+
oneOf: [
|
|
328
|
+
{ title: 'input', type: 'object', properties: { kind: { const: 'input' }, field: { type: 'string', pattern: MEMBER_NAME, description: 'The input’s apiName.' } }, required: ['kind', 'field'], additionalProperties: false },
|
|
329
|
+
{ title: 'output', type: 'object', properties: { kind: { const: 'output' }, field: { type: 'string', pattern: MEMBER_NAME, description: 'The outcome output’s apiName.' } }, required: ['kind', 'field'], additionalProperties: false },
|
|
330
|
+
{ title: 'literal', type: 'object', properties: { kind: { const: 'literal' }, value: { type: ['string', 'number', 'boolean', 'null'] } }, required: ['kind', 'value'], additionalProperties: false },
|
|
331
|
+
],
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const ON_TYPE = ref('The object type it changes (the subject’s, for an edit or create)', 'object-type')
|
|
335
|
+
|
|
336
|
+
/** One expected change, by kind; each names its object type. */
|
|
337
|
+
const EXPECTED_CHANGE: JsonSchema = {
|
|
338
|
+
oneOf: [
|
|
339
|
+
{
|
|
340
|
+
title: 'property_set',
|
|
341
|
+
type: 'object',
|
|
342
|
+
description: 'Sets one property, which the object type’s backing.editable must list; the input’s kind fits the property’s type (a string property takes string or object_reference, number takes number, date date, timestamp timestamp, boolean boolean, and only a geopoint property takes geopoint). A create never sets its primary key (the record is filed under its key input), a delete sets nothing, and each input feeds one property.',
|
|
343
|
+
properties: { kind: { const: 'property_set' }, objectType: ON_TYPE, property: text('The property it sets, by apiName.'), value: CHANGE_VALUE },
|
|
344
|
+
required: ['kind', 'objectType', 'property', 'value'],
|
|
345
|
+
additionalProperties: false,
|
|
346
|
+
},
|
|
347
|
+
{ title: 'object_created', type: 'object', properties: { kind: { const: 'object_created' }, objectType: ON_TYPE }, required: ['kind', 'objectType'], additionalProperties: false },
|
|
348
|
+
{ title: 'object_deleted', type: 'object', properties: { kind: { const: 'object_deleted' }, objectType: ON_TYPE }, required: ['kind', 'objectType'], additionalProperties: false },
|
|
349
|
+
{
|
|
350
|
+
title: 'lifecycle_transition',
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: { kind: { const: 'lifecycle_transition' }, objectType: ON_TYPE, transition: text('The lifecycle transition, by apiName.') },
|
|
353
|
+
required: ['kind', 'objectType', 'transition'],
|
|
354
|
+
additionalProperties: false,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A condition over the inputs (and, at approval, the record): an expression tree. */
|
|
360
|
+
const EXPRESSION: JsonSchema = {
|
|
361
|
+
type: 'object',
|
|
362
|
+
description:
|
|
363
|
+
'An expression: { kind: literal, value }, { kind: input, field: <input apiName> }, { kind: observation_property, property: <subject property> }, { kind: principal_attribute, attribute, valueType, whenAbsent }, { kind: principal_has_role, role, whenAbsent }, or { kind: call, operator, args: [expressions] } with operator eq, neq, gt, gte, lt, lte, and, or, not, add, subtract, multiply, divide, concat, coalesce, matches, length. At most 32 levels and 256 nodes. No expression takes a timing: in a postcondition, observation_property reads the record after the change (a postcondition reads only properties its outcome sets); in submission criteria and approval conditions it reads the record as it is when the request is checked.',
|
|
364
|
+
properties: {
|
|
365
|
+
kind: { enum: ['literal', 'input', 'observation_property', 'principal_attribute', 'principal_has_role', 'call'] },
|
|
366
|
+
operator: { enum: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'and', 'or', 'not', 'add', 'subtract', 'multiply', 'divide', 'concat', 'coalesce', 'matches', 'length'] },
|
|
367
|
+
args: { type: 'array', items: { type: 'object' } },
|
|
368
|
+
field: { type: 'string', pattern: MEMBER_NAME },
|
|
369
|
+
property: { type: 'string' },
|
|
370
|
+
},
|
|
371
|
+
required: ['kind'],
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const CRITERION: JsonSchema = {
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
code: STABLE_CODE,
|
|
378
|
+
message: text('What the person is told when it fails.'),
|
|
379
|
+
assertion: EXPRESSION,
|
|
380
|
+
},
|
|
381
|
+
required: ['code', 'message', 'assertion'],
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const APPROVAL: JsonSchema = {
|
|
386
|
+
type: 'object',
|
|
387
|
+
description:
|
|
388
|
+
'none; required { threshold 1–10, separationOfDuties: boolean, policyKey? }; conditional { threshold, separationOfDuties, policyKey }; or chain { separationOfDuties: strict, expiresAfter? ("24h", "3d", at most 7 days), stages: [{ key, when: always | <expression>, approver: { members: true } | { role, scope: subject }, quorum }] } — 1 to 5 steps, run one after another.',
|
|
389
|
+
properties: {
|
|
390
|
+
mode: { enum: ['none', 'required', 'conditional', 'chain'] },
|
|
391
|
+
threshold: { type: 'integer', minimum: 1, maximum: 10 },
|
|
392
|
+
separationOfDuties: { anyOf: [{ type: 'boolean' }, { const: 'strict' }] },
|
|
393
|
+
policyKey: { type: 'string', minLength: 1 },
|
|
394
|
+
expiresAfter: { type: 'string', pattern: '^[1-9][0-9]*[hd]$' },
|
|
395
|
+
stages: {
|
|
396
|
+
type: 'array',
|
|
397
|
+
minItems: 1,
|
|
398
|
+
maxItems: 5,
|
|
399
|
+
items: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
key: { type: 'string', description: 'The step’s name, unique in the chain (lowercase snake_case).' },
|
|
403
|
+
when: { anyOf: [{ const: 'always' }, EXPRESSION], description: '"always", or a condition on the inputs and the record.' },
|
|
404
|
+
approver: { type: 'object', description: '{ members: true } — any member who may decide Action Requests — or { role: <App role>, scope: subject }.' },
|
|
405
|
+
quorum: { type: 'integer', minimum: 1, maximum: 10 },
|
|
406
|
+
},
|
|
407
|
+
required: ['key', 'when', 'approver', 'quorum'],
|
|
408
|
+
additionalProperties: false,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ['mode'],
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const ACTION: JsonSchema = {
|
|
416
|
+
$schema: DRAFT,
|
|
417
|
+
title: 'Blueprint governed Action (blueprint/actions/<apiName>.yaml)',
|
|
418
|
+
description:
|
|
419
|
+
'A governed write on one object type. Every reference is a name (subject.objectType, effect.authoritySourceSystem, an expected change’s objectType and property or transition, an input’s field); the service resolves them and mints every id. On the local runtime an Action that writes only the Blueprint’s records (effect create, update or delete) is deployed by `local dev`, and one whose effect reaches another system stays switched off. '
|
|
420
|
+
+ 'An edit (subject.mode existing, effect update or delete) takes the subject’s primary key as a required input of the same name, uses concurrency expected_version, and so takes a required `expectedVersion` input ({ kind: number, integer: true, minimum: 0 }: a query row’s _meta.recordVersion). '
|
|
421
|
+
+ 'A create (subject.mode create, effect create) uses concurrency append_once, takes the primary key as a required input and never sets it in expectedChanges, and declares object_created. A delete declares object_deleted and sets no property. '
|
|
422
|
+
+ 'A postcondition reads only properties its outcome sets; a submission criterion and an approval step’s condition may read the inputs and the record (observation_property). '
|
|
423
|
+
+ 'Each input feeds one property. `frontera blueprint plan` checks these rules before anything is written; `frontera blueprint new action <apiName> --template edit|create --object-type <Type>` writes a file that follows them.',
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {
|
|
426
|
+
apiName: apiName('action'),
|
|
427
|
+
displayName: text(),
|
|
428
|
+
description: text(),
|
|
429
|
+
subject: {
|
|
430
|
+
type: 'object',
|
|
431
|
+
properties: { objectType: ref('The object type it acts on', 'object-type'), mode: { enum: ['existing', 'create'], description: 'existing: it changes a record (effect update, delete, transition or command); create: it makes one (effect create).' } },
|
|
432
|
+
required: ['objectType', 'mode'],
|
|
433
|
+
additionalProperties: false,
|
|
434
|
+
},
|
|
435
|
+
effect: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: {
|
|
438
|
+
kind: { enum: ['create', 'update', 'delete', 'transition', 'command'] },
|
|
439
|
+
authoritySourceSystem: {
|
|
440
|
+
type: 'string',
|
|
441
|
+
minLength: 1,
|
|
442
|
+
default: ACTIVE_CATALOG,
|
|
443
|
+
examples: [ACTIVE_CATALOG],
|
|
444
|
+
description:
|
|
445
|
+
`The source system with authority over the records, by apiName. An Action that writes the Blueprint's own records names \`${ACTIVE_CATALOG}\`: the source system the service adds to the draft with its first object type, and the one it takes when the key is left out and \`${ACTIVE_CATALOG}\` is the draft's only source system. Source systems have no file or command of their own, so another name is refused.`,
|
|
446
|
+
},
|
|
447
|
+
description: text(),
|
|
448
|
+
compensationAction: { type: 'string', description: 'The Action that undoes it, by apiName.' },
|
|
449
|
+
},
|
|
450
|
+
required: ['kind', 'description'],
|
|
451
|
+
additionalProperties: false,
|
|
452
|
+
},
|
|
453
|
+
inputs: { type: 'array', items: ACTION_FIELD },
|
|
454
|
+
businessOutcomes: {
|
|
455
|
+
type: 'array',
|
|
456
|
+
minItems: 1,
|
|
457
|
+
items: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: {
|
|
460
|
+
code: STABLE_CODE,
|
|
461
|
+
displayName: text(),
|
|
462
|
+
description: text(),
|
|
463
|
+
effect: { enum: ['confirmed_applied', 'confirmed_not_applied'] },
|
|
464
|
+
expectedChanges: { type: 'array', description: 'What it changes; none for confirmed_not_applied.', items: EXPECTED_CHANGE },
|
|
465
|
+
postconditions: { type: 'array', items: CRITERION },
|
|
466
|
+
output: { type: 'array', items: ACTION_FIELD },
|
|
467
|
+
},
|
|
468
|
+
required: ['code', 'displayName', 'description', 'effect', 'expectedChanges', 'postconditions', 'output'],
|
|
469
|
+
additionalProperties: false,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
concurrency: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
strategy: { enum: ['expected_version', 'create_if_absent', 'append_once'], description: 'expected_version for an existing subject; append_once for a create on the Blueprint’s records (create_if_absent does not deploy there).' },
|
|
476
|
+
openRequests: {
|
|
477
|
+
enum: ['one_per_subject'],
|
|
478
|
+
description:
|
|
479
|
+
'one_per_subject: a record has at most one open request of this Action from the same requester; another submit is refused with ACTION_REQUEST_ALREADY_OPEN (the CLI exits 3), naming the open request in details.openRequest. Only with subject.mode existing. A workflow sweep that may meet it gives the step continueOnError: true and branches on $steps.<id>.error.code.',
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
required: ['strategy'],
|
|
483
|
+
additionalProperties: false,
|
|
484
|
+
},
|
|
485
|
+
impact: { type: 'object', properties: { maxSubjects: { const: 1 } }, required: ['maxSubjects'], additionalProperties: false },
|
|
486
|
+
submissionCriteria: { type: 'array', items: CRITERION, description: 'Conditions every submission must meet, checked before it is accepted.' },
|
|
487
|
+
governance: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {
|
|
490
|
+
invokeCapability: { type: 'string', pattern: '^[a-z][a-z0-9_-]{0,63}:[a-z][a-z0-9_-]{0,63}$', description: 'resource:action — the capability a role needs to run it.' },
|
|
491
|
+
approval: APPROVAL,
|
|
492
|
+
reason: { enum: ['required', 'optional'] },
|
|
493
|
+
sensitivity: SENSITIVITY,
|
|
494
|
+
},
|
|
495
|
+
required: ['invokeCapability', 'approval', 'reason', 'sensitivity'],
|
|
496
|
+
additionalProperties: false,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
required: ['apiName', 'displayName', 'description', 'subject', 'effect', 'inputs', 'businessOutcomes', 'concurrency', 'impact', 'submissionCriteria', 'governance'],
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** One object set expression, as the Blueprint's query reads it (`query/expression.ts` in the service). */
|
|
503
|
+
const OBJECT_SET_EXPR: JsonSchema = {
|
|
504
|
+
type: 'object',
|
|
505
|
+
description:
|
|
506
|
+
'base { objectType }; filter { objectSet, where }; searchAround { objectSet, link }; union, intersect, subtract { objectSets: [...] }; static { objectType, pks }; reference { objectSetId: `objectSet/<name>` } — another saved set, by its name as people say it (`objectSet/Open work orders`) or its file name. The service checks the expression when the set is saved.',
|
|
507
|
+
properties: {
|
|
508
|
+
type: { enum: ['base', 'filter', 'searchAround', 'union', 'intersect', 'subtract', 'static', 'reference'] },
|
|
509
|
+
objectType: ref('base, static: the object type', 'object-type'),
|
|
510
|
+
objectSet: { type: 'object', description: 'filter, searchAround: the set it narrows or walks from (an expression).' },
|
|
511
|
+
where: { type: 'object', description: 'filter: a condition ({ property, op, value | values }, or { and | or: [...] }, { not }).' },
|
|
512
|
+
link: { type: 'string', description: 'searchAround: the link type to walk, by apiName.' },
|
|
513
|
+
objectSets: { type: 'array', items: { type: 'object' }, description: 'union, intersect, subtract: the sets combined.' },
|
|
514
|
+
pks: { type: 'array', items: { type: ['string', 'number'] } },
|
|
515
|
+
objectSetId: { type: 'string', description: 'reference: `objectSet/<name>`, resolved to this runtime’s id when the set is saved.' },
|
|
516
|
+
},
|
|
517
|
+
required: ['type'],
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const OBJECT_SET: JsonSchema = {
|
|
521
|
+
$schema: DRAFT,
|
|
522
|
+
title: 'Blueprint saved object set (blueprint/object-sets/<name>.yaml)',
|
|
523
|
+
description:
|
|
524
|
+
'A named set of one object type’s records that workflows, Agents and Apps reuse: dynamic (a definition, re-evaluated on every read) or static (a fixed list of primary keys). `frontera local dev` saves each file on the local runtime once the Blueprint is published there, so a reset brings it back. Other files name it `objectSet/<name>`, where the name is the set’s `name` (spaces allowed: `objectSet/Open work orders`) or the file’s name (`objectSet/open-work-orders`).',
|
|
525
|
+
type: 'object',
|
|
526
|
+
properties: {
|
|
527
|
+
name: text('What people call it ("Open work orders"); the file’s name when absent.'),
|
|
528
|
+
description: { type: 'string' },
|
|
529
|
+
objectType: ref('The object type its records are', 'object-type'),
|
|
530
|
+
kind: { enum: ['dynamic', 'static'], default: 'dynamic' },
|
|
531
|
+
definition: { ...OBJECT_SET_EXPR, description: `dynamic: the expression the set reads. ${String(OBJECT_SET_EXPR.description)}` },
|
|
532
|
+
records: { type: 'array', minItems: 1, items: { type: ['string', 'number'] }, description: 'static: the primary keys of the records it lists.' },
|
|
533
|
+
},
|
|
534
|
+
required: ['objectType'],
|
|
535
|
+
additionalProperties: false,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const WORKSPACE_GRANTS: JsonSchema = {
|
|
539
|
+
$schema: DRAFT,
|
|
540
|
+
title: 'Workspace grants beyond read (blueprint/workspace-grants.yaml)',
|
|
541
|
+
description:
|
|
542
|
+
"What the workspace itself (its workflows, Functions and Apps, which read through the workspace rather than as an Agent) may read of an object type beyond plain read: `read_sensitive` is clearance over the type's confidential and restricted fields. `frontera local dev` grants every object type `read` and then these, after each Blueprint publish. An Agent's own clearance is `read_sensitive` in its bindings.json. `frontera local sync pull` writes the file when the workspace holds read_sensitive on a type (none: no file), and `frontera local sync push` proposes a change to it in a change request: each type listed gets the listed permissions when it is applied, a type left out keeps its grant. Deleting the file changes no grant and is not proposed: to take read_sensitive away, list the type with `permissions: [read]`.",
|
|
543
|
+
type: 'object',
|
|
544
|
+
properties: {
|
|
545
|
+
grants: {
|
|
546
|
+
type: 'array',
|
|
547
|
+
items: {
|
|
548
|
+
type: 'object',
|
|
549
|
+
properties: {
|
|
550
|
+
objectType: { type: 'string', pattern: '^(objectType/)?[A-Z][A-Za-z0-9]*$', description: 'The object type, as objectType/<ApiName>.' },
|
|
551
|
+
permissions: { type: 'array', items: { enum: ['read', 'read_sensitive'] }, description: '`read` is always included; `read_sensitive` is the only other permission.' },
|
|
552
|
+
},
|
|
553
|
+
required: ['objectType', 'permissions'],
|
|
554
|
+
additionalProperties: false,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ['grants'],
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export const BLUEPRINT_SCHEMAS = {
|
|
563
|
+
'blueprint-object-type': OBJECT_TYPE,
|
|
564
|
+
'blueprint-link-type': LINK_TYPE,
|
|
565
|
+
'blueprint-shared-field': SHARED_FIELD,
|
|
566
|
+
'blueprint-semantic-type': SEMANTIC_TYPE,
|
|
567
|
+
'blueprint-metric': METRIC,
|
|
568
|
+
'blueprint-action': ACTION,
|
|
569
|
+
'blueprint-object-set': OBJECT_SET,
|
|
570
|
+
'blueprint-workspace-grants': WORKSPACE_GRANTS,
|
|
571
|
+
} as const satisfies Record<string, JsonSchema>
|
|
572
|
+
|
|
573
|
+
export const BLUEPRINT_SCHEMA_FILES: Record<keyof typeof BLUEPRINT_SCHEMAS, string> = {
|
|
574
|
+
'blueprint-object-type': 'blueprint/object-types/<ApiName>.yaml',
|
|
575
|
+
'blueprint-link-type': 'blueprint/link-types/<apiName>.yaml',
|
|
576
|
+
'blueprint-shared-field': 'blueprint/shared-fields/<apiName>.yaml',
|
|
577
|
+
'blueprint-semantic-type': 'blueprint/semantic-types/<apiName>.yaml',
|
|
578
|
+
'blueprint-metric': 'blueprint/metrics/<apiName>.yaml',
|
|
579
|
+
'blueprint-action': 'blueprint/actions/<apiName>.yaml',
|
|
580
|
+
'blueprint-object-set': 'blueprint/object-sets/<name>.yaml',
|
|
581
|
+
'blueprint-workspace-grants': 'blueprint/workspace-grants.yaml',
|
|
582
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { liveActionStates, localActionCalls, type LiveAction } from './local-actions'
|
|
2
|
+
import { liveAppDeployments, workspaceGet, type LiveApp } from './local-app-runtime'
|
|
3
|
+
import { projectBuildRows, projectBuildSummary, type BuildRow, type ProjectBuildSummary } from './local-build-summary'
|
|
4
|
+
import { localSecretPresence, type SecretPresence } from './local-secrets'
|
|
5
|
+
import { isAlive, type LocalState } from './local-state'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the build is counted with besides the project's files, read from the
|
|
9
|
+
* running runtime: each governed Action's deployment (where `frontera action
|
|
10
|
+
* list` reads it) and the secrets it holds (names, and whether each has a
|
|
11
|
+
* value; never a value). `local status` and Studio's build summary read them
|
|
12
|
+
* the same way, so their counts agree. Both null while the runtime is not
|
|
13
|
+
* running or does not answer: the last build's record speaks then.
|
|
14
|
+
*/
|
|
15
|
+
export interface RuntimeBuildInputs {
|
|
16
|
+
live: Map<string, LiveAction> | null
|
|
17
|
+
secrets: SecretPresence | null
|
|
18
|
+
/** Whether the runtime has each Custom App's deployment, by App folder name (`local-app-runtime.ts`). */
|
|
19
|
+
apps?: Map<string, LiveApp> | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runtimeBuildInputs(state: LocalState | null, root?: string): Promise<RuntimeBuildInputs> {
|
|
23
|
+
if (!state?.credentials || state.phase !== 'ready' || !state.supervisorPid || !isAlive(state.supervisorPid)) return { live: null, secrets: null, apps: null }
|
|
24
|
+
const serviceUrl = `http://127.0.0.1:${state.ports.service}`
|
|
25
|
+
const creds = state.credentials
|
|
26
|
+
const [live, secrets, apps] = await Promise.all([
|
|
27
|
+
creds.orgKey ? Promise.resolve().then(() => liveActionStates(localActionCalls(serviceUrl, creds))).catch(() => null) : Promise.resolve(null),
|
|
28
|
+
localSecretPresence(serviceUrl, creds).catch(() => null),
|
|
29
|
+
root ? liveAppDeployments(root, workspaceGet(serviceUrl, creds)).catch(() => null) : Promise.resolve(null),
|
|
30
|
+
])
|
|
31
|
+
return { live, secrets, apps }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The build summary as `local status` counts it, with the runtime read the
|
|
36
|
+
* same way: what Studio's sidebar says ("5 items failed to apply") is the
|
|
37
|
+
* failed count `local status` prints.
|
|
38
|
+
*/
|
|
39
|
+
/** The build's rows as `local status --json` lists them, with the runtime read the same way: Studio's lists mark each from them. */
|
|
40
|
+
export async function currentBuildRows(root: string, id: string, state: LocalState | null, read: (state: LocalState | null, root?: string) => Promise<RuntimeBuildInputs> = runtimeBuildInputs): Promise<BuildRow[] | null> {
|
|
41
|
+
const { live, secrets, apps } = await read(state, root)
|
|
42
|
+
return projectBuildRows(root, id, live, null, secrets, apps ?? null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function currentBuildSummary(root: string, id: string, state: LocalState | null, read: (state: LocalState | null, root?: string) => Promise<RuntimeBuildInputs> = runtimeBuildInputs): Promise<ProjectBuildSummary | null> {
|
|
46
|
+
const { live, secrets, apps } = await read(state, root)
|
|
47
|
+
return projectBuildSummary(root, id, live, secrets, apps ?? null)
|
|
48
|
+
}
|