@frontera-sdk/cli 1.52.39 → 1.52.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +48 -4
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +331 -31
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +875 -88
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +211 -21
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +180 -7
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +24 -21
|
@@ -6,7 +6,7 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
|
6
6
|
import type { LocalAction } from './local-actions'
|
|
7
7
|
import { absentWhy, adaptOperating, plainLinks, projectProvides } from './local-adapt'
|
|
8
8
|
import { applyAgentGrants, type AgentGrant } from './local-agent-grants'
|
|
9
|
-
import { oneLine, refWords } from './local-applied'
|
|
9
|
+
import { loadBuild, oneLine, pushedWorkflowVersion, refWords } from './local-applied'
|
|
10
10
|
import { readableName } from './local-sync-names'
|
|
11
11
|
import { ensureObjectDefinitions } from './local-objects'
|
|
12
12
|
import { applyQualityChecks, QUALITY_CHECKS_FILE, readQualityChecks, sourcePaths, sourceRows } from './local-quality-checks'
|
|
@@ -14,14 +14,21 @@ import { readLock } from './local-sync-plan'
|
|
|
14
14
|
import { applyErrorText } from './local-apply-error'
|
|
15
15
|
import { localAdaptation } from './local-blueprint-outline'
|
|
16
16
|
import { actionFit, classify, objectTypeFit, objectTypeNeeds } from './local-portability'
|
|
17
|
+
import { actionSemanticConflicts, semanticFactsOf } from '../blueprint/action-semantic-fit'
|
|
18
|
+
import { inheritedConflictLine } from '../blueprint/action-rules'
|
|
19
|
+
import { inheritedFromPull, unchangedSincePull } from './local-check-inherited'
|
|
17
20
|
import type { SyncProfiles, SyncTarget } from './local-dev-sync'
|
|
18
21
|
import type { LocalProject } from './local-project'
|
|
19
22
|
import { syncClient } from './local-sync-client'
|
|
23
|
+
import { DEFAULT_KNOWLEDGE_THRESHOLD, KEYLESS_KNOWLEDGE_THRESHOLD, PLATFORM_DEFAULT_EMBEDDING, sameEmbedding, WIDENED_KNOWLEDGE_THRESHOLD, type OrgSettings } from './local-org-settings'
|
|
24
|
+
import { isLocalEmbeddings } from './local-seed'
|
|
25
|
+
import { readLink } from './local-sync-link'
|
|
26
|
+
import { readSnapshot } from './local-sync-snapshot'
|
|
20
27
|
import { readProviderSetup } from './local-providers'
|
|
21
28
|
import { fillPluginSecrets, localSecretNames, needsValueLine, NON_SECRET_HEADERS, pluginSecretInputs, pluginSecretName } from './local-secrets'
|
|
22
29
|
import { describeUnresolved, governedActionIds, isUuid, kindsIn, parseRef, referencesIn, toIds, type Directory } from './local-sync-refs'
|
|
23
30
|
import { buildDirectory, rows } from './local-sync-remote'
|
|
24
|
-
import type
|
|
31
|
+
import { isAlive, type LocalCredentials } from './local-state'
|
|
25
32
|
import { addStandInServers, dropPlatformOnly, dropStandInTriggers, platformOnlyNames, standInNames } from './local-stand-ins'
|
|
26
33
|
|
|
27
34
|
/**
|
|
@@ -441,12 +448,43 @@ function parseJson<T>(output: string): T | null {
|
|
|
441
448
|
}
|
|
442
449
|
}
|
|
443
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Why a dataset file is not applied here, when an object type over a Media
|
|
453
|
+
* Set reads it (`media_<set>`, or a type with a media property left off):
|
|
454
|
+
* that type reads a Media Set's catalog, which an empty stand-in Media Set
|
|
455
|
+
* holds here (or nothing, for a type left off), and a CSV of sample rows
|
|
456
|
+
* cannot publish into it ("Only managed Datasets accept file publications").
|
|
457
|
+
* Null for any other dataset.
|
|
458
|
+
*/
|
|
459
|
+
export function mediaCatalogDatasetReason(root: string, dataset: string): string | null {
|
|
460
|
+
const dir = join(root, 'blueprint', 'object-types')
|
|
461
|
+
if (!existsSync(dir)) return null
|
|
462
|
+
for (const name of readdirSync(dir).filter((n) => /\.ya?ml$/.test(n) && !n.startsWith('.'))) {
|
|
463
|
+
let doc: Record<string, any> | null
|
|
464
|
+
try {
|
|
465
|
+
doc = parseYaml(readFileSync(join(dir, name), 'utf8')) as Record<string, any> | null
|
|
466
|
+
} catch {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if (!doc || doc.backing?.dataset !== dataset) continue
|
|
470
|
+
const fit = localAdaptation(doc, () => false)
|
|
471
|
+
if (fit.mediaStub) return `${doc.apiName ?? name} reads it as Media Set ${fit.mediaStub}'s catalog, which an empty Media Set stands in for here (its files stay on the platform); a CSV can't publish into a Media Set's catalog, so datasets/${dataset}.* is left as it is and not applied`
|
|
472
|
+
if (fit.leftOff) return `${doc.apiName ?? name} reads the platform's media files and is left off this runtime, so datasets/${dataset}.* is not applied`
|
|
473
|
+
}
|
|
474
|
+
return null
|
|
475
|
+
}
|
|
476
|
+
|
|
444
477
|
/** How the runtime holds one object type of the Blueprint it was given. */
|
|
445
478
|
export interface ObjectBuild {
|
|
446
479
|
status: 'built' | 'adapted' | 'stubbed' | 'left-off'
|
|
447
480
|
adaptations?: string[]
|
|
481
|
+
/** Left off because the local runtime refused this type's file: the runtime's words. The rest of the Blueprint builds. */
|
|
482
|
+
refused?: string
|
|
448
483
|
}
|
|
449
484
|
|
|
485
|
+
/** A Blueprint artifact the local runtime refused, by `kind:apiName` (`object-type:Receipt`), with its words. */
|
|
486
|
+
export type BlueprintRefusals = ReadonlyMap<string, string>
|
|
487
|
+
|
|
450
488
|
export interface ResolvedBlueprint {
|
|
451
489
|
/** The runtime's copy, when the project's files needed changing; unset otherwise. */
|
|
452
490
|
dir?: string
|
|
@@ -457,13 +495,29 @@ export interface ResolvedBlueprint {
|
|
|
457
495
|
actions: LocalAction[]
|
|
458
496
|
/** Actions left off the copy, with why: they act on what the runtime leaves off. */
|
|
459
497
|
leftOffActions: Record<string, string>
|
|
498
|
+
/** The Actions of `leftOffActions` the runtime refused (their own file), not left off by design. */
|
|
499
|
+
refusedActions?: string[]
|
|
500
|
+
/**
|
|
501
|
+
* Each Action given to the runtime as the workspace runs it although it offers
|
|
502
|
+
* a value the semantic type it writes refuses (`inheritedFromPull`), with the
|
|
503
|
+
* warning lines: it deploys, and the build and `local check` say them as inherited.
|
|
504
|
+
*/
|
|
505
|
+
inheritedWarnings?: Record<string, string[]>
|
|
506
|
+
/**
|
|
507
|
+
* The link types, metrics, semantic types and shared fields left off the copy, by the
|
|
508
|
+
* reference `status` lists them as (`metric/workOrdersRaised`): with why, and whether the
|
|
509
|
+
* runtime refused the file itself (a failure to fix) or it names a type left off.
|
|
510
|
+
*/
|
|
511
|
+
parts?: Record<string, { reason: string; refused: boolean }>
|
|
460
512
|
/** The Media Sets the runtime needs, empty, for the media-backed types it holds: API name → display name. */
|
|
461
513
|
mediaSets?: Record<string, string>
|
|
514
|
+
/** Each type over a Media Set's catalog: its set, and its file in the runtime's copy (`object-types/Receipt.yaml`). */
|
|
515
|
+
mediaTypes?: Record<string, { set: string; file: string }>
|
|
462
516
|
}
|
|
463
517
|
|
|
464
518
|
/**
|
|
465
519
|
* The Blueprint as this runtime can hold it, in a copy under
|
|
466
|
-
* `.frontera/local/resolved/blueprint`; the project's files stay as pulled.
|
|
520
|
+
* `.frontera/local/resolved/run-<pid>/blueprint`; the project's files stay as pulled.
|
|
467
521
|
* The rules are `objectTypeFit` and `actionFit` (`local-portability.ts`):
|
|
468
522
|
* - An object type backed by the platform's media files (a `media_reference`
|
|
469
523
|
* property) cannot be read from a local file dataset: it is left out, with
|
|
@@ -471,13 +525,16 @@ export interface ResolvedBlueprint {
|
|
|
471
525
|
* - An object type whose rows are read by markings, with no markings column
|
|
472
526
|
* named, relies on a column the platform's media datasets publish; over a
|
|
473
527
|
* local file dataset it reads with organization row access instead.
|
|
474
|
-
* - A calculated property is left off the type, and an
|
|
528
|
+
* - A calculated property other than a location is left off the type, and an
|
|
529
|
+
* Action that sets it (a location is added by `blueprint apply`).
|
|
475
530
|
* Each says so. `dir` is unset when nothing needed changing.
|
|
476
531
|
*/
|
|
477
|
-
export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
532
|
+
export function resolveBlueprint(project: LocalProject, refused: BlueprintRefusals = new Map()): ResolvedBlueprint {
|
|
478
533
|
const source = join(project.root, 'blueprint')
|
|
479
534
|
const types = join(source, 'object-types')
|
|
480
535
|
const none: ResolvedBlueprint = { warnings: [], objects: {}, actions: [], leftOffActions: {} }
|
|
536
|
+
const parts: Record<string, { reason: string; refused: boolean }> = {}
|
|
537
|
+
const offWords = (t: string) => (objects[t]?.refused ? `it names ${t}, which the local runtime refused` : `it names ${t}, which reads the platform’s media files`)
|
|
481
538
|
if (!existsSync(types)) return none
|
|
482
539
|
const yamlIn = (dir: string) => (existsSync(join(source, dir)) ? readdirSync(join(source, dir)).filter((n) => /\.ya?ml$/.test(n) && !n.startsWith('.')) : [])
|
|
483
540
|
const read = (dir: string, name: string) => {
|
|
@@ -495,12 +552,22 @@ export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
|
495
552
|
const warnings: string[] = []
|
|
496
553
|
const objects: Record<string, ObjectBuild> = {}
|
|
497
554
|
const mediaSets: Record<string, string> = {}
|
|
555
|
+
const mediaTypes: Record<string, { set: string; file: string }> = {}
|
|
498
556
|
const hasLocalDataset = (dataset: string) => existsSync(join(project.root, 'datasets', `${dataset}.json`))
|
|
499
557
|
for (const name of yamlIn('object-types')) {
|
|
500
558
|
const doc = read('object-types', name)
|
|
501
559
|
if (!doc || typeof doc.apiName !== 'string') continue
|
|
502
560
|
const fit = localAdaptation(doc, hasLocalDataset)
|
|
503
561
|
const adaptations = classify(objectTypeNeeds(doc, hasLocalDataset)).adaptations
|
|
562
|
+
// The runtime refused this type's file on an earlier try: the rest of the Blueprint builds without it.
|
|
563
|
+
const refusal = refused.get(`object-type:${doc.apiName}`)
|
|
564
|
+
if (refusal) {
|
|
565
|
+
dropped.add(doc.apiName)
|
|
566
|
+
removed.push(join('object-types', name))
|
|
567
|
+
objects[doc.apiName] = { status: 'left-off', adaptations: [`is left off: the local runtime refused it: ${refusal}`], refused: refusal }
|
|
568
|
+
warnings.push(` warning: ${doc.apiName} is left off this runtime: the runtime refused it: ${refusal}`)
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
504
571
|
if (fit.leftOff) {
|
|
505
572
|
dropped.add(doc.apiName)
|
|
506
573
|
removed.push(join('object-types', name))
|
|
@@ -514,17 +581,19 @@ export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
|
514
581
|
// A property's set is the platform's id; the runtime derives its own from the dataset.
|
|
515
582
|
if (fit.mediaStub) {
|
|
516
583
|
mediaSets[fit.mediaStub] ??= readableName(fit.mediaStub)
|
|
584
|
+
mediaTypes[doc.apiName] = { set: fit.mediaStub, file: join('object-types', name) }
|
|
517
585
|
const properties = (doc.properties as Array<Record<string, unknown>> | undefined) ?? []
|
|
518
586
|
next = { ...next, properties: properties.map(({ mediaSetId: _set, ...p }) => p) }
|
|
519
|
-
warnings.push(` warning: ${doc.apiName}’s files stay on the platform:
|
|
587
|
+
warnings.push(` warning: ${doc.apiName}’s files stay on the platform: a stand-in Media Set, ${fit.mediaStub}, holds placeholder files for them here`)
|
|
520
588
|
}
|
|
521
|
-
// A
|
|
522
|
-
//
|
|
589
|
+
// A location (from latitude and longitude) is added by `blueprint apply` after the type. Any
|
|
590
|
+
// other calculation has no route from a file: the runtime holds the type without it.
|
|
523
591
|
if (fit.calculated.length) {
|
|
524
|
-
const
|
|
525
|
-
|
|
592
|
+
const off = new Set(fit.calculated)
|
|
593
|
+
const properties = (doc.properties as Array<{ apiName?: string; calculation?: unknown }> | undefined) ?? []
|
|
594
|
+
next = { ...next, properties: properties.filter((p) => !off.has(String(p.apiName))) }
|
|
526
595
|
for (const f of fit.calculated) droppedFields.add(`${doc.apiName}.${f}`)
|
|
527
|
-
warnings.push(` warning: ${doc.apiName}.${fit.calculated.join(`, ${doc.apiName}.`)} is left off this runtime: calculated
|
|
596
|
+
warnings.push(` warning: ${doc.apiName}.${fit.calculated.join(`, ${doc.apiName}.`)} is left off this runtime: only a location is a calculated field a file can add`)
|
|
528
597
|
}
|
|
529
598
|
if (fit.readsEveryRow) {
|
|
530
599
|
next = { ...next, rowAccess: 'organization' }
|
|
@@ -532,17 +601,48 @@ export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
|
532
601
|
}
|
|
533
602
|
if (next !== doc) patched.set(join('object-types', name), stringifyYaml(next, { lineWidth: 0 }))
|
|
534
603
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
604
|
+
// A link type or metric the runtime refused, or that names a type left off, goes too; a refused
|
|
605
|
+
// semantic type or shared field is taken out, and what names it is refused (and left off) next.
|
|
606
|
+
const refusedHere = (kind: string, doc: Record<string, any> | null) => (typeof doc?.apiName === 'string' ? refused.get(`${kind}:${doc.apiName}`) : undefined)
|
|
607
|
+
for (const name of yamlIn('link-types')) {
|
|
608
|
+
const doc = read('link-types', name)
|
|
609
|
+
const refusal = refusedHere('link-type', doc)
|
|
610
|
+
if (refusal) warnings.push(` warning: link type ${doc!.apiName} is left off this runtime: the runtime refused it: ${refusal}`)
|
|
611
|
+
const offType = [doc?.from?.objectType, doc?.to?.objectType].find((t) => typeof t === 'string' && dropped.has(t)) as string | undefined
|
|
612
|
+
if (refusal || offType) removed.push(join('link-types', name))
|
|
613
|
+
if (typeof doc?.apiName === 'string' && (refusal || offType)) parts[`linkType/${doc.apiName}`] = refusal ? { reason: `the local runtime refused it: ${refusal}`, refused: true } : { reason: offWords(offType!), refused: false }
|
|
614
|
+
}
|
|
615
|
+
for (const name of yamlIn('metrics')) {
|
|
616
|
+
const doc = read('metrics', name)
|
|
617
|
+
const refusal = refusedHere('metric', doc)
|
|
618
|
+
if (refusal) warnings.push(` warning: metric ${doc!.apiName} is left off this runtime: the runtime refused it: ${refusal}`)
|
|
619
|
+
if (refusal || dropped.has(doc?.objectType)) removed.push(join('metrics', name))
|
|
620
|
+
if (typeof doc?.apiName === 'string' && (refusal || dropped.has(doc?.objectType))) parts[`metric/${doc.apiName}`] = refusal ? { reason: `the local runtime refused it: ${refusal}`, refused: true } : { reason: offWords(String(doc.objectType)), refused: false }
|
|
621
|
+
}
|
|
622
|
+
for (const [kind, folder] of [['semantic-type', 'semantic-types'], ['shared-field', 'shared-fields']] as const) {
|
|
623
|
+
for (const name of yamlIn(folder)) {
|
|
624
|
+
const doc = read(folder, name)
|
|
625
|
+
const refusal = refusedHere(kind, doc)
|
|
626
|
+
if (!refusal) continue
|
|
627
|
+
removed.push(join(folder, name))
|
|
628
|
+
warnings.push(` warning: ${kind.replace('-', ' ')} ${doc!.apiName} is left off this runtime: the runtime refused it: ${refusal}`)
|
|
629
|
+
parts[`${kind === 'semantic-type' ? 'semanticType' : 'sharedField'}/${doc!.apiName}`] = { reason: `the local runtime refused it: ${refusal}`, refused: true }
|
|
542
630
|
}
|
|
543
631
|
}
|
|
544
632
|
const actions: LocalAction[] = []
|
|
545
633
|
const leftOffActions: Record<string, string> = {}
|
|
634
|
+
const refusedActions: string[] = []
|
|
635
|
+
// A value an Action offers that the semantic type it writes refuses, in an Action and files still as
|
|
636
|
+
// pulled: the workspace's, so the apply takes it (`action-rules.ts`), and it is said here as inherited.
|
|
637
|
+
const inheritedWarnings: Record<string, string[]> = {}
|
|
638
|
+
const unchanged = unchangedSincePull(project.root)
|
|
639
|
+
const inheritedConflict = unchanged.refs.size ? inheritedFromPull(unchanged) : null
|
|
640
|
+
const semantic = inheritedConflict
|
|
641
|
+
? semanticFactsOf((['semantic-types', 'shared-fields', 'object-types'] as const).flatMap((folder) => yamlIn(folder).flatMap((name) => {
|
|
642
|
+
const document = read(folder, name)
|
|
643
|
+
return document ? [{ path: `${folder}/${name}`, document }] : []
|
|
644
|
+
})))
|
|
645
|
+
: null
|
|
546
646
|
for (const name of yamlIn('actions')) {
|
|
547
647
|
const doc = read('actions', name)
|
|
548
648
|
if (!doc || typeof doc.apiName !== 'string') continue
|
|
@@ -552,18 +652,38 @@ export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
|
552
652
|
const touches = [fit.subject, ...changes.map((c) => c?.objectType), ...inputs.map((i) => i?.valueType?.objectType)].filter((t): t is string => typeof t === 'string')
|
|
553
653
|
const offType = touches.find((t) => dropped.has(t))
|
|
554
654
|
const offField = changes.map((c) => `${c?.objectType ?? fit.subject}.${c?.property}`).find((f) => droppedFields.has(f))
|
|
555
|
-
|
|
556
|
-
|
|
655
|
+
const refusal = refused.get(`action:${doc.apiName}`)
|
|
656
|
+
if (offType || offField || refusal) {
|
|
657
|
+
const offWhy = offType && objects[offType]?.refused ? 'which the local runtime refused' : 'which reads the platform’s media files'
|
|
658
|
+
leftOffActions[doc.apiName] = refusal ? `the local runtime refused it: ${refusal}` : offType ? `it acts on ${offType}, ${offWhy}` : `it sets ${offField}, a calculated field the runtime leaves off`
|
|
659
|
+
if (refusal) refusedActions.push(doc.apiName)
|
|
557
660
|
removed.push(join('actions', name))
|
|
558
661
|
warnings.push(` warning: governed Action ${doc.apiName} is left off this runtime: ${leftOffActions[doc.apiName]}`)
|
|
559
662
|
continue
|
|
560
663
|
}
|
|
561
664
|
const capability = doc.governance?.invokeCapability
|
|
562
665
|
actions.push({ apiName: doc.apiName, fit, ...(typeof capability === 'string' ? { invokeCapability: capability } : {}) })
|
|
666
|
+
const conflicts = semantic && inheritedConflict ? actionSemanticConflicts(doc, semantic) : []
|
|
667
|
+
if (conflicts.length && conflicts.every((c) => inheritedConflict!(doc.apiName, c.refs))) {
|
|
668
|
+
inheritedWarnings[doc.apiName] = conflicts.map((c) => inheritedConflictLine(c.message))
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const out: ResolvedBlueprint = {
|
|
672
|
+
warnings, objects, actions, leftOffActions,
|
|
673
|
+
...(refusedActions.length ? { refusedActions } : {}),
|
|
674
|
+
...(Object.keys(inheritedWarnings).length ? { inheritedWarnings } : {}),
|
|
675
|
+
...(Object.keys(parts).length ? { parts } : {}),
|
|
676
|
+
...(Object.keys(mediaSets).length ? { mediaSets, mediaTypes } : {}),
|
|
563
677
|
}
|
|
564
|
-
const out: ResolvedBlueprint = { warnings, objects, actions, leftOffActions, ...(Object.keys(mediaSets).length ? { mediaSets } : {}) }
|
|
565
678
|
if (!patched.size && !removed.length) return out
|
|
566
|
-
|
|
679
|
+
// This process's own copy: a second `local dev` (Forge's watcher beside a `--once`) makes its own,
|
|
680
|
+
// so neither applies a folder the other is rewriting. Copies of processes gone are cleared.
|
|
681
|
+
const runs = join(project.root, '.frontera', 'local', 'resolved')
|
|
682
|
+
for (const name of existsSync(runs) ? readdirSync(runs) : []) {
|
|
683
|
+
const pid = /^run-(\d+)$/.exec(name)?.[1]
|
|
684
|
+
if (pid && Number(pid) !== process.pid && !isAlive(Number(pid))) rmSync(join(runs, name), { recursive: true, force: true })
|
|
685
|
+
}
|
|
686
|
+
const copy = join('.frontera', 'local', 'resolved', `run-${process.pid}`, 'blueprint')
|
|
567
687
|
rmSync(join(project.root, copy), { recursive: true, force: true })
|
|
568
688
|
cpSync(source, join(project.root, copy), { recursive: true })
|
|
569
689
|
for (const [rel, text] of patched) writeFileSync(join(project.root, copy, rel), text)
|
|
@@ -571,13 +691,37 @@ export function resolveBlueprint(project: LocalProject): ResolvedBlueprint {
|
|
|
571
691
|
return { ...out, dir: copy }
|
|
572
692
|
}
|
|
573
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Point each type over a Media Set's catalog at the dataset that catalog is
|
|
696
|
+
* on this runtime. A set's catalog is `media_<set>` unless that name was
|
|
697
|
+
* taken when the set was made (a sample dataset of the same name the project
|
|
698
|
+
* applied first): then the runtime named it otherwise, and a type left
|
|
699
|
+
* reading `media_<set>` would read the sample's plain text columns, which
|
|
700
|
+
* the runtime refuses for a media field. Edits the runtime's copy only.
|
|
701
|
+
*/
|
|
702
|
+
export function pointMediaTypesAt(project: LocalProject, blueprint: ResolvedBlueprint, catalogs: Readonly<Record<string, string>>): void {
|
|
703
|
+
if (!blueprint.dir || !blueprint.mediaTypes) return
|
|
704
|
+
for (const { set, file } of Object.values(blueprint.mediaTypes)) {
|
|
705
|
+
const catalog = catalogs[set]
|
|
706
|
+
if (!catalog || catalog === `media_${set}`) continue
|
|
707
|
+
const at = join(project.root, blueprint.dir, file)
|
|
708
|
+
try {
|
|
709
|
+
const doc = parseYaml(readFileSync(at, 'utf8')) as Record<string, any> | null
|
|
710
|
+
if (!doc?.backing || typeof doc.backing !== 'object') continue
|
|
711
|
+
writeFileSync(at, stringifyYaml({ ...doc, backing: { ...doc.backing, dataset: catalog } }, { lineWidth: 0 }))
|
|
712
|
+
} catch {
|
|
713
|
+
// A file that does not parse is refused by the apply, and said there.
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
574
718
|
/**
|
|
575
719
|
* The properties each object type lets a governed Action write
|
|
576
720
|
* (`backing.editable`), for the types those Actions act on. `blueprint apply`
|
|
577
721
|
* leaves backings to `bind`, so `local dev` declares them itself
|
|
578
722
|
* (`blueprint editable`) before an Action's write path is built.
|
|
579
723
|
*/
|
|
580
|
-
export function editableSets(dir: string, subjects: ReadonlySet<string>): Map<string, string[]> {
|
|
724
|
+
export function editableSets(dir: string, subjects: ReadonlySet<string> | null): Map<string, string[]> {
|
|
581
725
|
const out = new Map<string, string[]>()
|
|
582
726
|
const at = join(dir, 'object-types')
|
|
583
727
|
if (!existsSync(at)) return out
|
|
@@ -586,7 +730,7 @@ export function editableSets(dir: string, subjects: ReadonlySet<string>): Map<st
|
|
|
586
730
|
try {
|
|
587
731
|
const doc = parseYaml(readFileSync(join(at, name), 'utf8')) as { apiName?: unknown; backing?: { editable?: unknown } } | null
|
|
588
732
|
const editable = doc?.backing?.editable
|
|
589
|
-
if (typeof doc?.apiName === 'string' && subjects.has(doc.apiName) && Array.isArray(editable) && editable.length) out.set(doc.apiName, editable.map(String).sort())
|
|
733
|
+
if (typeof doc?.apiName === 'string' && (!subjects || subjects.has(doc.apiName)) && Array.isArray(editable) && editable.length) out.set(doc.apiName, editable.map(String).sort())
|
|
590
734
|
} catch {
|
|
591
735
|
// A file that does not parse is refused by the apply itself.
|
|
592
736
|
}
|
|
@@ -625,14 +769,33 @@ const KNOWLEDGE_SETTINGS = ['retrieval', 'chunker', 'chunkSize', 'chunkOverlap',
|
|
|
625
769
|
/** The files `local dev` uploads into a knowledge base: those the runtime ingests. */
|
|
626
770
|
export const KNOWLEDGE_DOCUMENT = /\.(md|markdown|txt|pdf|docx|html)$/i
|
|
627
771
|
|
|
772
|
+
/**
|
|
773
|
+
* What the runtime's embedding model is compared against, when it differs:
|
|
774
|
+
* the linked workspace's, or, in a project with no workspace, the platform's
|
|
775
|
+
* default, which the default threshold is tuned for. `keyless`: the runtime
|
|
776
|
+
* embeds with the keyless local embeddings, whose distances no model's
|
|
777
|
+
* threshold fits, whatever the workspace embeds with.
|
|
778
|
+
*/
|
|
779
|
+
export type EmbeddingMismatch = 'workspace' | 'platform-default' | 'keyless'
|
|
780
|
+
|
|
628
781
|
/**
|
|
629
782
|
* A knowledge base's settings as the runtime can hold them. A model it names
|
|
630
783
|
* (the reranker, the extractor's, OCR's) that the runtime's catalog does not
|
|
631
784
|
* have is left off here, so documents are read and searched without that
|
|
632
785
|
* step instead of failing; each says so. `models` is the catalog, or null
|
|
633
786
|
* when it could not be read (the settings then go as they are).
|
|
787
|
+
*
|
|
788
|
+
* When the runtime embeds with another model than the workspace, or, with
|
|
789
|
+
* no workspace, than the platform's default (`embeddingsDiffer`), the
|
|
790
|
+
* distances between a question and its passages are not the ones the
|
|
791
|
+
* default threshold was set for, and it finds nothing that model ranks a
|
|
792
|
+
* little further away: the base searches with a wider one here (only when
|
|
793
|
+
* the file sets none, or the default), as an adaptation. On the keyless
|
|
794
|
+
* local embeddings every base searches with `KEYLESS_KNOWLEDGE_THRESHOLD`,
|
|
795
|
+
* whatever its file sets: its threshold is for a model's distances. The
|
|
796
|
+
* file is never changed, and a platform never sees either threshold.
|
|
634
797
|
*/
|
|
635
|
-
export function knowledgeSettings(doc: Record<string, unknown>, models: ReadonlySet<string> | null, label: string): { settings: Record<string, unknown>; warnings: string[] } {
|
|
798
|
+
export function knowledgeSettings(doc: Record<string, unknown>, models: ReadonlySet<string> | null, label: string, embeddingsDiffer: EmbeddingMismatch | boolean = false): { settings: Record<string, unknown>; warnings: string[] } {
|
|
636
799
|
const settings: Record<string, unknown> = {}
|
|
637
800
|
for (const key of KNOWLEDGE_SETTINGS) if (doc[key] !== undefined && doc[key] !== null) settings[key] = doc[key]
|
|
638
801
|
// The platform keeps the threshold as a decimal, which its list sends as a string ("0.7"); the update takes numbers.
|
|
@@ -640,6 +803,15 @@ export function knowledgeSettings(doc: Record<string, unknown>, models: Readonly
|
|
|
640
803
|
if (typeof settings[key] === 'string' && Number.isFinite(Number(settings[key]))) settings[key] = Number(settings[key])
|
|
641
804
|
}
|
|
642
805
|
const warnings: string[] = []
|
|
806
|
+
if (embeddingsDiffer === 'keyless') {
|
|
807
|
+
const own = settings.similarityThreshold === undefined ? '' : ` (its file’s ${String(settings.similarityThreshold)} is for a model’s embeddings, and stays the workspace’s)`
|
|
808
|
+
settings.similarityThreshold = KEYLESS_KNOWLEDGE_THRESHOLD
|
|
809
|
+
warnings.push(` warning: Knowledge base ${label} searches with threshold ${KEYLESS_KNOWLEDGE_THRESHOLD} here, set for the keyless local embeddings${own}: retrieval is approximate, by the words a passage shares with the question; a key the provider accepts for an embedding model gives the platform’s (\`frontera local status\` says which, under Embeddings)`)
|
|
810
|
+
} else if (embeddingsDiffer && (settings.similarityThreshold === undefined || settings.similarityThreshold === DEFAULT_KNOWLEDGE_THRESHOLD)) {
|
|
811
|
+
settings.similarityThreshold = WIDENED_KNOWLEDGE_THRESHOLD
|
|
812
|
+
const from = embeddingsDiffer === 'platform-default' ? 'the platform’s default, which its threshold is tuned for' : 'the workspace’s'
|
|
813
|
+
warnings.push(` warning: Knowledge base ${label} searches with threshold ${WIDENED_KNOWLEDGE_THRESHOLD} here: embeddings differ from ${from}`)
|
|
814
|
+
}
|
|
643
815
|
const has = (model: unknown) => typeof model !== 'string' || !model.trim() || !models || models.has(model) || models.has(model.split('/').pop()!)
|
|
644
816
|
const hybrid = settings.hybridConfig as Record<string, unknown> | undefined
|
|
645
817
|
if (hybrid?.rerank === true && !has(hybrid.rerankerModel)) {
|
|
@@ -656,6 +828,35 @@ export function knowledgeSettings(doc: Record<string, unknown>, models: Readonly
|
|
|
656
828
|
return { settings, warnings }
|
|
657
829
|
}
|
|
658
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Whether the runtime embeds with another model than the workspace (as the
|
|
833
|
+
* last fetch recorded the workspace's settings), the same model through
|
|
834
|
+
* another provider counting as the same. A project linked to no workspace
|
|
835
|
+
* (`local init`) is compared against the platform's default model instead:
|
|
836
|
+
* its knowledge files carry the default threshold, which is tuned for that
|
|
837
|
+
* model. `keyless` when the runtime embeds with the keyless local
|
|
838
|
+
* embeddings, whatever the workspace's. False when either side is not known,
|
|
839
|
+
* including a linked workspace not fetched yet.
|
|
840
|
+
*/
|
|
841
|
+
export async function embeddingsDiffer(root: string, local: { serviceUrl: string; creds: LocalCredentials }): Promise<EmbeddingMismatch | false> {
|
|
842
|
+
if (!local.creds.workspaceKey) return false
|
|
843
|
+
const linked = readLink(root) !== null
|
|
844
|
+
const workspace = linked ? readSnapshot(root)?.settings?.embedding : PLATFORM_DEFAULT_EMBEDDING
|
|
845
|
+
try {
|
|
846
|
+
const here = (await syncClient(local.serviceUrl, local.creds.workspaceKey).get<OrgSettings | { data?: OrgSettings }>('/v1/sync/settings')) as OrgSettings & { data?: OrgSettings }
|
|
847
|
+
const settings = here?.data ?? here
|
|
848
|
+
if (!settings || !('embedding' in settings)) return false
|
|
849
|
+
if (!settings.embedding?.provider || !settings.embedding.model) return false
|
|
850
|
+
// Before the workspace is compared: the keyless embeddings answer as OpenAI's model, and are never it.
|
|
851
|
+
if (isLocalEmbeddings(settings.embedding)) return 'keyless'
|
|
852
|
+
if (!workspace?.provider || !workspace.model) return false
|
|
853
|
+
if (sameEmbedding(workspace, settings.embedding)) return false
|
|
854
|
+
return linked ? 'workspace' : 'platform-default'
|
|
855
|
+
} catch {
|
|
856
|
+
return false
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
659
860
|
/** The runtime's model catalog, by slug and by provider/model id; null when it could not be read. */
|
|
660
861
|
async function localModels(local: { serviceUrl: string; creds: LocalCredentials }): Promise<Set<string> | null> {
|
|
661
862
|
try {
|
|
@@ -676,6 +877,48 @@ async function localModels(local: { serviceUrl: string; creds: LocalCredentials
|
|
|
676
877
|
* search and reranking, extraction and OCR), then the folder's documents the
|
|
677
878
|
* base does not hold yet are uploaded.
|
|
678
879
|
*/
|
|
880
|
+
/**
|
|
881
|
+
* What to say of a knowledge base with no documents in its folder: an Agent
|
|
882
|
+
* searching it finds nothing. A base pulled from a workspace (in
|
|
883
|
+
* `frontera.lock`) has its documents there, and the pull brings them; one of
|
|
884
|
+
* the project's own takes Markdown or text files in its folder.
|
|
885
|
+
*/
|
|
886
|
+
export function emptyKnowledgeNote(root: string, folder: string, name: string): string {
|
|
887
|
+
const pulled = !!readLockSafe(root)?.items[`knowledge/${folder}`]
|
|
888
|
+
return pulled
|
|
889
|
+
? `Knowledge base ${name} has no documents here, so an Agent searching it finds nothing: its documents stay in the workspace until \`frontera local sync pull --documents ${folder}\` brings them`
|
|
890
|
+
: `Knowledge base ${name} has no documents, so an Agent searching it finds nothing: add Markdown or text files to knowledge/${folder}/`
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function readLockSafe(root: string): { items: Record<string, unknown> } | null {
|
|
894
|
+
try {
|
|
895
|
+
const lock = JSON.parse(readFileSync(join(root, 'frontera.lock'), 'utf8')) as { items?: Record<string, unknown> }
|
|
896
|
+
return lock?.items && typeof lock.items === 'object' ? { items: lock.items } : null
|
|
897
|
+
} catch {
|
|
898
|
+
return null
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* What a knowledge base's settings change did to its documents, in words
|
|
904
|
+
* (`rechunk` from the settings write): how many are being chunked again,
|
|
905
|
+
* why none were, and which keep their chunks (added as text). Nothing when
|
|
906
|
+
* the change asked for no chunking.
|
|
907
|
+
*/
|
|
908
|
+
export function rechunkNotes(name: string, rechunk: unknown): string[] {
|
|
909
|
+
if (!rechunk || typeof rechunk !== 'object') return []
|
|
910
|
+
const r = rechunk as { changed?: unknown; queued?: unknown; kept?: unknown; notQueued?: unknown }
|
|
911
|
+
const changed = Array.isArray(r.changed) ? r.changed.filter((c): c is string => typeof c === 'string') : []
|
|
912
|
+
if (!changed.length) return []
|
|
913
|
+
const queued = typeof r.queued === 'number' ? r.queued : 0
|
|
914
|
+
const kept = Array.isArray(r.kept) ? r.kept.filter((k): k is string => typeof k === 'string') : []
|
|
915
|
+
const lines: string[] = []
|
|
916
|
+
if (queued) lines.push(` note: Knowledge base ${name}: ${changed.join(', ')} changed, so ${queued} document${queued === 1 ? ' is' : 's are'} being chunked again (\`frontera knowledge sources ${name}\` shows each as processing, then ready)`)
|
|
917
|
+
else if (typeof r.notQueued === 'string') lines.push(` warning: Knowledge base ${name}: ${changed.join(', ')} changed, but its documents keep their old chunks: ${r.notQueued}`)
|
|
918
|
+
if (kept.length) lines.push(` warning: Knowledge base ${name}: ${kept.join(', ')} ${kept.length === 1 ? 'was' : 'were'} added as text and keep${kept.length === 1 ? 's' : ''} ${kept.length === 1 ? 'its' : 'their'} chunks; add ${kept.length === 1 ? 'it' : 'them'} again to chunk with the new settings`)
|
|
919
|
+
return lines
|
|
920
|
+
}
|
|
921
|
+
|
|
679
922
|
export async function applyKnowledge(
|
|
680
923
|
project: LocalProject,
|
|
681
924
|
target: Extract<SyncTarget, { kind: 'knowledge' }>,
|
|
@@ -703,7 +946,7 @@ export async function applyKnowledge(
|
|
|
703
946
|
const notes: string[] = []
|
|
704
947
|
const id = bases.find((b) => b.name === name)?.id
|
|
705
948
|
if (local?.creds.workspaceKey && id) {
|
|
706
|
-
const { settings, warnings } = knowledgeSettings(doc, await localModels(local), name)
|
|
949
|
+
const { settings, warnings } = knowledgeSettings(doc, await localModels(local), name, await embeddingsDiffer(project.root, local))
|
|
707
950
|
notes.push(...warnings)
|
|
708
951
|
if (Object.keys(settings).length) {
|
|
709
952
|
const failed = await fetch(new URL(`/v1/workspace-knowledge/${encodeURIComponent(id)}`, local.serviceUrl), {
|
|
@@ -711,7 +954,13 @@ export async function applyKnowledge(
|
|
|
711
954
|
headers: { authorization: `Bearer ${local.creds.workspaceKey}`, 'content-type': 'application/json' },
|
|
712
955
|
body: JSON.stringify(settings),
|
|
713
956
|
})
|
|
714
|
-
.then(async (res) =>
|
|
957
|
+
.then(async (res) => {
|
|
958
|
+
if (!res.ok) return `${res.status} ${(await res.text()).slice(0, 200)}`
|
|
959
|
+
// A chunking change chunks the documents again: said, so the base is not read as unchanged.
|
|
960
|
+
const body = (await res.json().catch(() => null)) as { rechunk?: unknown } | null
|
|
961
|
+
notes.push(...rechunkNotes(name, body?.rechunk))
|
|
962
|
+
return null
|
|
963
|
+
})
|
|
715
964
|
.catch((err: Error) => err.message)
|
|
716
965
|
if (failed) notes.push(` warning: Knowledge base ${name} keeps the runtime’s own retrieval settings: its settings were refused (${failed})`)
|
|
717
966
|
}
|
|
@@ -732,9 +981,13 @@ export async function applyKnowledge(
|
|
|
732
981
|
// The documents, the knowledge base's folders as subfolders (`Policies/Returns/terms.pdf`).
|
|
733
982
|
const { documentPath, documentPaths, folderPaths, splitDocumentPath } = await import('./local-knowledge-documents')
|
|
734
983
|
const documents = documentPaths(project.root, target.name)
|
|
735
|
-
|
|
984
|
+
// No documents: an Agent searching it finds nothing. Said with the command that brings them.
|
|
985
|
+
if (!documents.length) {
|
|
986
|
+
notes.push(` warning: ${emptyKnowledgeNote(project.root, target.name, name)}`)
|
|
987
|
+
return await done({ ok: true, output: '' })
|
|
988
|
+
}
|
|
736
989
|
const sources = await run(['knowledge', 'sources', name, '--json', ...p])
|
|
737
|
-
const listedSources = parseJson<Array<{ fileName?: string; folderId?: string | null }>>(sources.output) ?? []
|
|
990
|
+
const listedSources = parseJson<Array<{ id?: string; fileName?: string; folderId?: string | null; status?: string; updatedAt?: string }>>(sources.output) ?? []
|
|
738
991
|
const key = local?.creds.workspaceKey
|
|
739
992
|
const http = async (method: string, path: string, body?: FormData | Record<string, unknown>) => {
|
|
740
993
|
const res = await fetch(new URL(path, local!.serviceUrl), {
|
|
@@ -752,6 +1005,9 @@ export async function applyKnowledge(
|
|
|
752
1005
|
? folderPaths((((await http('GET', `/v1/workspace-knowledge/${encodeURIComponent(id)}/folders`).catch(() => null))?.folders ?? []) as Array<{ id: string; parentId?: string | null; name: string }>))
|
|
753
1006
|
: new Map<string, string>()
|
|
754
1007
|
const have = new Set(listedSources.map((s) => documentPath(s.folderId ? folders.get(s.folderId) : null, s.fileName ?? '')))
|
|
1008
|
+
// A document the runtime holds pending or processing with no ingestion behind it (a rollback took its
|
|
1009
|
+
// reading with the database it replaced) is queued to be read again: uploading it again would add a second.
|
|
1010
|
+
if (key && id) notes.push(...(await requeueStuckDocuments(project, local!, name, id, listedSources)))
|
|
755
1011
|
const fresh = documents.filter((f) => !have.has(f))
|
|
756
1012
|
if (!fresh.length) return await done({ ok: true, output: '' })
|
|
757
1013
|
const top = fresh.filter((f) => !f.includes('/'))
|
|
@@ -800,6 +1056,33 @@ export async function applyKnowledge(
|
|
|
800
1056
|
return await done({ ok: true, output: lines.join('\n') })
|
|
801
1057
|
}
|
|
802
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* The documents of one knowledge base stuck pending or processing with no
|
|
1061
|
+
* ingestion behind them (`stuckSources`: unsettled since before the last
|
|
1062
|
+
* restore, or for half an hour), queued to be read again. Returns the lines
|
|
1063
|
+
* for the build.
|
|
1064
|
+
*/
|
|
1065
|
+
async function requeueStuckDocuments(
|
|
1066
|
+
project: LocalProject,
|
|
1067
|
+
local: { serviceUrl: string; creds: LocalCredentials },
|
|
1068
|
+
name: string,
|
|
1069
|
+
baseId: string,
|
|
1070
|
+
listed: Array<{ id?: string; fileName?: string; status?: string; updatedAt?: string }>,
|
|
1071
|
+
): Promise<string[]> {
|
|
1072
|
+
const { localIngestApi, requeueSources, shellArg, stuckLine, stuckSources } = await import('./local-knowledge-ingestion')
|
|
1073
|
+
const { readBaseline } = await import('./local-db-snapshot')
|
|
1074
|
+
const api = localIngestApi(local.serviceUrl, local.creds)
|
|
1075
|
+
if (!api) return []
|
|
1076
|
+
const sources = listed.filter((s) => s.id).map((s) => ({ base: name, baseId, id: s.id!, fileName: s.fileName ?? s.id!, status: String(s.status ?? ''), ...(s.updatedAt ? { updatedAt: s.updatedAt } : {}) }))
|
|
1077
|
+
const stuck = stuckSources(sources, { restoredAt: readBaseline(project, local.creds.organizationId)?.restoredAt })
|
|
1078
|
+
if (!stuck.length) return []
|
|
1079
|
+
const { requeued, refused } = await requeueSources(api, stuck)
|
|
1080
|
+
return [
|
|
1081
|
+
...(requeued.length ? [` note: Knowledge base ${name}: queued ${requeued.map((s) => s.fileName).join(', ')} to be read again: ${requeued.length === 1 ? 'it was' : 'they were'} ${requeued.length === 1 ? requeued[0]!.status : 'pending or processing'} with no ingestion behind ${requeued.length === 1 ? 'it' : 'them'} (\`frontera knowledge sources ${shellArg(name)}\` shows each as pending, then ready)`] : []),
|
|
1082
|
+
...refused.map((r) => ` warning: ${stuckLine(r.source)} (re-queuing it was refused: ${r.reason})`),
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
|
|
803
1086
|
/**
|
|
804
1087
|
* A knowledge base's quality checks on the runtime (`local-quality-checks.ts`),
|
|
805
1088
|
* their documents by this runtime's ids. Returns the lines for the build.
|
|
@@ -1024,21 +1307,35 @@ async function setToolPolicy(local: { serviceUrl: string; creds: LocalCredential
|
|
|
1024
1307
|
}
|
|
1025
1308
|
|
|
1026
1309
|
/**
|
|
1027
|
-
* Promote a workflow's
|
|
1028
|
-
*
|
|
1310
|
+
* Promote a workflow's version to live on the local runtime: `version` (the
|
|
1311
|
+
* one `local dev` pushed its file as) when given, else the newest draft, but
|
|
1312
|
+
* only one newer than the live version. After a promotion the newest draft
|
|
1313
|
+
* can be an older push, and promoting that would put stale code live. Returns
|
|
1314
|
+
* the version promoted, or null when there was none, it was live already, or
|
|
1315
|
+
* it was refused.
|
|
1029
1316
|
*/
|
|
1030
|
-
export async function promoteWorkflow(serviceUrl: string, creds: LocalCredentials, slug: string): Promise<number | null> {
|
|
1317
|
+
export async function promoteWorkflow(serviceUrl: string, creds: LocalCredentials, slug: string, version?: number, opts: { onlyIfLive?: boolean } = {}): Promise<number | null> {
|
|
1031
1318
|
if (!creds.workspaceKey) return null
|
|
1032
1319
|
const call = (path: string, method: string) =>
|
|
1033
1320
|
fetch(new URL(path, serviceUrl), { method, headers: { authorization: `Bearer ${creds.workspaceKey}`, 'content-type': 'application/json' }, body: method === 'POST' ? '{}' : undefined })
|
|
1034
1321
|
const res = await call(`/v1/workflows/${encodeURIComponent(slug)}`, 'GET')
|
|
1035
1322
|
if (!res.ok) return null
|
|
1036
|
-
const
|
|
1323
|
+
const found = ((await res.json()) as { data?: { latestDraftNumber?: number | null; liveVersion?: number | null } }).data
|
|
1324
|
+
// `onlyIfLive`: a workflow with no live version stays a draft (its triggers stay unarmed).
|
|
1325
|
+
if (opts.onlyIfLive && !found?.liveVersion) return null
|
|
1326
|
+
const n = promotionTarget(found?.latestDraftNumber ?? null, found?.liveVersion ?? null, version)
|
|
1037
1327
|
if (!n) return null
|
|
1038
1328
|
const promoted = await call(`/v1/workflows/${encodeURIComponent(slug)}/versions/${n}/promote`, 'POST')
|
|
1039
1329
|
return promoted.ok ? n : null
|
|
1040
1330
|
}
|
|
1041
1331
|
|
|
1332
|
+
/** Which version a promotion makes live: the pushed one, else a draft newer than the live one; null when nothing should change. */
|
|
1333
|
+
export function promotionTarget(latestDraft: number | null, live: number | null, pushed?: number): number | null {
|
|
1334
|
+
if (pushed) return pushed === live ? null : pushed
|
|
1335
|
+
if (!latestDraft) return null
|
|
1336
|
+
return live && latestDraft < live ? null : latestDraft
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1042
1339
|
/** The project's object type files by API name, parsed; unreadable ones are left out. */
|
|
1043
1340
|
function projectObjectTypes(root: string): Map<string, Record<string, unknown>> {
|
|
1044
1341
|
const dir = join(root, 'blueprint', 'object-types')
|
|
@@ -1086,15 +1383,25 @@ export async function applyBindings(project: LocalProject, target: Extract<SyncT
|
|
|
1086
1383
|
// An Agent runs only a workflow's live version, and local dev pushes drafts: on this
|
|
1087
1384
|
// one-person runtime, a workflow an Agent may start is promoted so the binding holds.
|
|
1088
1385
|
const promote = async (slug: string): Promise<boolean> => {
|
|
1089
|
-
|
|
1386
|
+
// The version the workflow's file was pushed as, not whatever draft is newest.
|
|
1387
|
+
const n = await promoteWorkflow(serviceUrl, creds, slug, pushedWorkflowVersion(project, creds.organizationId, slug))
|
|
1090
1388
|
if (n) notes.push(` warning: promoted workflow ${slug} v${n} so ${target.slug} can start it`)
|
|
1091
1389
|
return n !== null
|
|
1092
1390
|
}
|
|
1093
1391
|
const failures: string[] = []
|
|
1094
1392
|
const settings = (row: Record<string, unknown>, keys: string[]) => Object.fromEntries(keys.filter((k) => row[k] !== undefined).map((k) => [k, row[k]]))
|
|
1393
|
+
// The last build: a Function of the project's that failed to build here, and a type the runtime refused.
|
|
1394
|
+
const build = loadBuild(project, creds.organizationId)
|
|
1095
1395
|
for (const row of doc.functions ?? []) {
|
|
1096
1396
|
const ref = typeof row.function === 'string' ? parseRef(row.function) : null
|
|
1097
1397
|
if (!ref || ref.kind !== 'function') continue
|
|
1398
|
+
// A Function of the project that does not build here: the Agent goes without it, and gets it once it builds.
|
|
1399
|
+
const fn = build[`function:${ref.name}/`] ?? build[`function:${ref.name}.ts`]
|
|
1400
|
+
if (fn?.status === 'failed') {
|
|
1401
|
+
const why = oneLine(fn.error ?? 'it failed to build')
|
|
1402
|
+
notes.push(` warning: ${target.slug} goes without the Function ${ref.name} here until it builds: ${why.charAt(0).toLowerCase()}${why.slice(1)}`)
|
|
1403
|
+
continue
|
|
1404
|
+
}
|
|
1098
1405
|
const failed = await put(`/v1/functions/${encodeURIComponent(ref.name)}/bindings/${encodeURIComponent(target.slug)}`, settings(row, ['enabled', 'requiresApproval', 'waitSeconds']))
|
|
1099
1406
|
if (failed) failures.push(` ${row.function}: ${failed}`)
|
|
1100
1407
|
}
|
|
@@ -1121,9 +1428,13 @@ export async function applyBindings(project: LocalProject, target: Extract<SyncT
|
|
|
1121
1428
|
creds,
|
|
1122
1429
|
leftOff: (apiName) => {
|
|
1123
1430
|
const type = types.get(apiName)
|
|
1124
|
-
|
|
1431
|
+
if (type && objectTypeFit(type, () => true).leftOff) return 'it reads the platform’s media files'
|
|
1432
|
+
const refused = build.blueprint?.objects?.[apiName]?.refused
|
|
1433
|
+
return refused ? `the local runtime refused it: ${refused}` : null
|
|
1125
1434
|
},
|
|
1126
1435
|
inProject: (apiName) => types.has(apiName),
|
|
1436
|
+
// Assumed as read because the pull couldn't read them: what the runtime already gives is kept.
|
|
1437
|
+
keepHeld: unread,
|
|
1127
1438
|
})
|
|
1128
1439
|
failures.push(...granted.failures)
|
|
1129
1440
|
notes.push(...granted.notes)
|