akm-cli 0.10.261012-beta → 0.10.261013-beta
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/CHANGELOG.md +216 -0
- package/SECURITY.md +2 -2
- package/STABILITY.md +2 -2
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/hints/cli-hints-short.md +3 -4
- package/dist/commands/command/execution-source-loader.js +10 -14
- package/dist/commands/env/env-binding.js +47 -58
- package/dist/commands/env/env-cli.js +20 -22
- package/dist/commands/env/env.js +10 -58
- package/dist/commands/env/secret-cli.js +9 -27
- package/dist/commands/env/secret.js +3 -10
- package/dist/commands/health/scheduler-binary.js +1 -1
- package/dist/commands/improve/distill.js +0 -2
- package/dist/commands/improve/eligibility.js +0 -2
- package/dist/commands/lint/env-key-rules.js +7 -6
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/proposal/validators/proposal-validators.js +1 -4
- package/dist/commands/read/show.js +4 -25
- package/dist/commands/sources/add-cli.js +8 -63
- package/dist/commands/sources/bundle-cli.js +8 -9
- package/dist/commands/sources/installed-stashes.js +117 -61
- package/dist/commands/sources/schema-repair.js +0 -2
- package/dist/commands/sources/source-clone.js +31 -7
- package/dist/commands/tasks/explain.js +1 -1
- package/dist/commands/tasks/tasks.js +38 -56
- package/dist/commands/ui/redact.js +4 -4
- package/dist/commands/ui/workspace-actions.js +1 -1
- package/dist/commands/workflow/plan.js +47 -8
- package/dist/commands/workflow-cli.js +37 -41
- package/dist/core/adapter/adapters/akm-adapter.js +3 -3
- package/dist/core/adapter/adapters/akm-lint.js +3 -4
- package/dist/core/adapter/adapters/akm-metadata.js +2 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
- package/dist/core/asset/resolve-ref.js +5 -0
- package/dist/core/bundle-rename.js +12 -4
- package/dist/core/common.js +4 -52
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +55 -18
- package/dist/core/config/schema/primitives.js +12 -6
- package/dist/core/config/schema/sources-bundles.js +2 -2
- package/dist/core/env-secret-ref.js +84 -73
- package/dist/core/errors.js +1 -1
- package/dist/core/paths.js +1 -6
- package/dist/core/redaction.js +16 -3
- package/dist/core/ref-token.js +82 -0
- package/dist/core/secret-value.js +14 -0
- package/dist/core/write-source.js +20 -4
- package/dist/indexer/indexer.js +27 -36
- package/dist/indexer/installations.js +1 -8
- package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
- package/dist/indexer/materialize-embeddings.js +1 -1
- package/dist/indexer/scan/drain-dir.js +3 -3
- package/dist/indexer/search/search-source.js +4 -2
- package/dist/indexer/walk/path-resolver.js +13 -68
- package/dist/indexer/walk/walker.js +18 -22
- package/dist/integrations/agent/engine-resolution.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +4 -5
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +1 -1
- package/dist/llm/embedders/remote.js +4 -3
- package/dist/output/renderers.js +3 -4
- package/dist/output/shapes/helpers.js +0 -2
- package/dist/output/text/command-format.js +12 -2
- package/dist/output/text/show-directives.js +16 -28
- package/dist/output/text/workflow-format.js +37 -7
- package/dist/scripts/akm-migrate-node.js +45865 -44692
- package/dist/scripts/akm-migrate.js +45804 -44630
- package/dist/setup/steps/tasks.js +1 -2
- package/dist/sources/providers/provider-utils.js +12 -0
- package/dist/sources/resolve.js +12 -16
- package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
- package/dist/sources/snapshot-fetchers/x.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +2 -2
- package/dist/storage/repositories/task-history-repository.js +2 -2
- package/dist/storage/repositories/workflow-runs-repository.js +38 -116
- package/dist/tasks/embedded.js +1 -1
- package/dist/tasks/prepare/prepare-support.js +3 -2
- package/dist/tasks/prepare/prepare.js +3 -4
- package/dist/tasks/prepare/script-capture.js +8 -22
- package/dist/tasks/run/load-task.js +46 -14
- package/dist/tasks/run/run-native-task.js +0 -8
- package/dist/tasks/run/run-workflow-task.js +2 -2
- package/dist/tasks/scheduler-sync.js +0 -1
- package/dist/tasks/source/bounded-document.js +0 -41
- package/dist/tasks/source/parse-task-source.js +0 -1
- package/dist/tasks/source/task-source-v4.js +1 -4
- package/dist/workflows/compile.js +1 -15
- package/dist/workflows/exec/child-workflow.js +1 -2
- package/dist/workflows/exec/dispatch-redaction.js +24 -6
- package/dist/workflows/exec/environment.js +11 -74
- package/dist/workflows/exec/exec-unit.js +31 -23
- package/dist/workflows/exec/frozen-judge.js +17 -3
- package/dist/workflows/exec/native-executor.js +70 -26
- package/dist/workflows/exec/run-workflow.js +31 -18
- package/dist/workflows/exec/step-work.js +4 -3
- package/dist/workflows/exec/unit-dispatch.js +1 -8
- package/dist/workflows/freeze/environment.js +14 -119
- package/dist/workflows/freeze/freeze.js +0 -1
- package/dist/workflows/freeze/step-values.js +1 -2
- package/dist/workflows/freeze/targets/child-workflow.js +3 -5
- package/dist/workflows/freeze/targets/command.js +6 -9
- package/dist/workflows/freeze/targets/shell.js +0 -3
- package/dist/workflows/parser.js +6 -25
- package/dist/workflows/plan.js +6 -2
- package/dist/workflows/renderer.js +77 -4
- package/dist/workflows/runtime/run-plan.js +37 -23
- package/dist/workflows/runtime/runs.js +41 -84
- package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
- package/dist/workflows/source-files.js +7 -19
- package/docs/architecture/0.10-process-rules.md +6 -6
- package/docs/architecture/architecture.md +6 -6
- package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
- package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
- package/docs/architecture/internals/storage-locations.md +4 -4
- package/docs/architecture/persisted-data-compat.md +3 -2
- package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
- package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
- package/docs/architecture/testing/manual-testing-checklist.md +46 -78
- package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
- package/docs/architecture/testing/testing-workflow.md +9 -11
- package/docs/architecture/workflow-engine.md +41 -27
- package/docs/guides/author-bundles.md +14 -17
- package/docs/guides/author-workflows.md +43 -9
- package/docs/guides/bundles.md +16 -11
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
- package/docs/guides/run-workflows.md +25 -13
- package/docs/guides/scheduling.md +1 -1
- package/docs/maintainers/README.md +2 -3
- package/docs/maintainers/release-checklist.md +73 -126
- package/docs/migration/v0.9-to-v0.10.md +5 -1
- package/docs/reference/cli.md +124 -112
- package/docs/reference/configuration.md +16 -9
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/env-and-secrets.md +66 -15
- package/docs/reference/registry.md +6 -11
- package/docs/reference/tasks.md +11 -3
- package/docs/reference/ui-api.md +3 -3
- package/docs/reference/workflow-schema.md +109 -36
- package/package.json +1 -1
- package/schemas/akm-task.json +2 -4
- package/dist/commands/sources/dangerous-env-audit.js +0 -134
- package/dist/core/activation-policy.js +0 -67
- package/dist/execution/directory-identity.js +0 -78
- package/dist/workflows/authoring/scope-key.js +0 -79
- package/dist/workflows/source-semantics.js +0 -110
package/dist/core/paths.js
CHANGED
|
@@ -311,12 +311,7 @@ export function getStateDir(env = process.env, platform = process.platform) {
|
|
|
311
311
|
* its `stash_dir` column — so two stashes on one machine never collide.
|
|
312
312
|
*
|
|
313
313
|
* Reuses {@link shortHash} (bundle-id.ts) rather than growing a second
|
|
314
|
-
* sha256-truncated-hex helper.
|
|
315
|
-
* `getCurrentWorkflowScopeKey`'s `dir:v1:<sha256>` in
|
|
316
|
-
* `src/workflows/authoring/scope-key.ts`, is a state.db column value for a
|
|
317
|
-
* different subsystem — full untruncated hex plus a `dir:v1:` prefix, which
|
|
318
|
-
* is neither filesystem-safe as a directory segment on Windows (`:`) nor
|
|
319
|
-
* short, so it is not a fit here.) `shortHash` trims to 8 hex chars (32
|
|
314
|
+
* sha256-truncated-hex helper. `shortHash` trims to 8 hex chars (32
|
|
320
315
|
* bits) — fine at its original call site because a collision only ever
|
|
321
316
|
* shortens a batch-unique slug suffix that a retry loop immediately
|
|
322
317
|
* re-disambiguates. Here there is no such retry loop, but a machine
|
package/dist/core/redaction.js
CHANGED
|
@@ -499,10 +499,23 @@ export function redactErrorBody(input) {
|
|
|
499
499
|
}
|
|
500
500
|
return out;
|
|
501
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* The spellings a JSON encoder gives a value inside a string literal: the
|
|
504
|
+
* `JSON.stringify` form, and the ASCII-only form with non-ASCII as `\uXXXX`
|
|
505
|
+
* (what Python's `json.dumps` writes). Empty for a value that needs no escaping.
|
|
506
|
+
*/
|
|
507
|
+
function jsonEscapedSpellings(value) {
|
|
508
|
+
const json = JSON.stringify(value).slice(1, -1);
|
|
509
|
+
// ASCII-only encoders (Python's ensure_ascii) also escape DEL; some (Java's) write uppercase hex.
|
|
510
|
+
const ascii = json.replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
511
|
+
const upper = ascii.replace(/\\u([0-9a-f]{4})/g, (_match, hex) => `\\u${hex.toUpperCase()}`);
|
|
512
|
+
return [...new Set([json, ascii, upper])].filter((spelling) => spelling !== value);
|
|
513
|
+
}
|
|
502
514
|
export function redactSensitiveText(text, sensitiveValues) {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
515
|
+
const exact = [...sensitiveValues].filter((value) => value.length > 0);
|
|
516
|
+
// A child that prints JSON writes a secret's escaped spelling (`\"`, `\\`, `\n`, `\uXXXX`),
|
|
517
|
+
// which the exact value never matches. Each escaped spelling is just another needle.
|
|
518
|
+
const values = [...new Set([...exact, ...exact.flatMap(jsonEscapedSpellings)])].sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
506
519
|
if (values.length === 0)
|
|
507
520
|
return text;
|
|
508
521
|
const coverageDelta = new Int32Array(text.length + 1);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The one substitution token akm hands to a process or a provider: `${ref}`,
|
|
6
|
+
* where `ref` is an akm ref (`[bundle//]conceptId`). A `secrets/…` ref stands
|
|
7
|
+
* for the secret's value; any other asset ref stands for the asset's absolute
|
|
8
|
+
* path. A `${…}` body that is not an akm ref (`${HOME}`, `${PATH//:/ }`,
|
|
9
|
+
* `${v//-/_}`, `${x/a/b}`: not led by a native type and not qualified by an
|
|
10
|
+
* installed bundle) is plain text and is left alone.
|
|
11
|
+
*
|
|
12
|
+
* Kept free of the commands layer so config credentials and env values share
|
|
13
|
+
* this one function.
|
|
14
|
+
*/
|
|
15
|
+
import { parseBundleRef } from "./asset/asset-ref.js";
|
|
16
|
+
import { typeNameFromConceptId } from "./asset/resolve-ref.js";
|
|
17
|
+
export const REF_TOKEN_RE = /\$\{([^}\s]+)\}/g;
|
|
18
|
+
/**
|
|
19
|
+
* Does a `${…}` body parse as an akm ref (no export fragment) that either
|
|
20
|
+
* leads with a native asset type (bundle-qualified or not) or is bundle-qualified
|
|
21
|
+
* with a bundle `isBundleInstalled` accepts? Anything else stays text, so shell
|
|
22
|
+
* syntax (`${v//-/_}`, `${x/a/b}`) is never read as a ref. A qualified opaque body
|
|
23
|
+
* follows the core ref grammar and the owner walk, whatever adapter owns it, but
|
|
24
|
+
* only for an installed bundle; with no predicate no bundle is installed.
|
|
25
|
+
*/
|
|
26
|
+
function isRefBody(body, isBundleInstalled) {
|
|
27
|
+
try {
|
|
28
|
+
const ref = parseBundleRef(body);
|
|
29
|
+
if (ref.fragment !== undefined)
|
|
30
|
+
return false;
|
|
31
|
+
if (typeNameFromConceptId(ref.conceptId) !== undefined)
|
|
32
|
+
return true;
|
|
33
|
+
return ref.bundle !== undefined && isBundleInstalled?.(ref.bundle) === true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Is the whole string exactly one `${ref}` token? */
|
|
40
|
+
export function isRefToken(value) {
|
|
41
|
+
const body = /^\$\{([^}\s]+)\}$/.exec(value)?.[1];
|
|
42
|
+
return body !== undefined && isRefBody(body);
|
|
43
|
+
}
|
|
44
|
+
/** Is the whole string exactly one `${ref}` token whose ref names a `secrets/` asset? */
|
|
45
|
+
export function isSecretRefToken(value) {
|
|
46
|
+
const body = /^\$\{([^}\s]+)\}$/.exec(value)?.[1];
|
|
47
|
+
return (body !== undefined && isRefBody(body) && typeNameFromConceptId(parseBundleRef(body).conceptId)?.type === "secret");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Replace every `${ref}` token in each value. Keys are never touched, and
|
|
51
|
+
* tokens embedded in larger strings are all substituted. `isBundleInstalled`
|
|
52
|
+
* lets a bundle-qualified body that is not led by a native type be a ref when
|
|
53
|
+
* its bundle is installed. An unresolved ref is
|
|
54
|
+
* collected in `missing` and its token left in place. Resolved secret values
|
|
55
|
+
* must never be logged or printed by callers.
|
|
56
|
+
*/
|
|
57
|
+
export function substituteRefTokens(values, resolve, isBundleInstalled) {
|
|
58
|
+
const missing = [];
|
|
59
|
+
const secretValues = new Set();
|
|
60
|
+
const cache = new Map();
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, value] of Object.entries(values)) {
|
|
63
|
+
out[key] = value.replace(REF_TOKEN_RE, (token, body) => {
|
|
64
|
+
if (!isRefBody(body, isBundleInstalled))
|
|
65
|
+
return token;
|
|
66
|
+
if (!cache.has(body))
|
|
67
|
+
cache.set(body, resolve(body));
|
|
68
|
+
const resolved = cache.get(body);
|
|
69
|
+
if (resolved === undefined) {
|
|
70
|
+
if (!missing.includes(body))
|
|
71
|
+
missing.push(body);
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
if (resolved.kind === "path")
|
|
75
|
+
return resolved.path;
|
|
76
|
+
if (resolved.value)
|
|
77
|
+
secretValues.add(resolved.value);
|
|
78
|
+
return resolved.value;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { values: out, missing, secretValues: [...secretValues] };
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
/**
|
|
6
|
+
* Read a secret's value: the file content as UTF-8 with surrounding whitespace
|
|
7
|
+
* trimmed. The one read every consumer uses (`secret run`, env `${ref}`
|
|
8
|
+
* tokens, config credentials, website fetchers), so a hand-written file's
|
|
9
|
+
* trailing newline never lands in an HTTP header. Callers MUST NOT write the
|
|
10
|
+
* returned value to stdout or any log.
|
|
11
|
+
*/
|
|
12
|
+
export function readValue(secretPath) {
|
|
13
|
+
return fs.readFileSync(secretPath, "utf8").trim();
|
|
14
|
+
}
|
|
@@ -49,26 +49,42 @@ export function resolveWritable(entry) {
|
|
|
49
49
|
return entry.writable;
|
|
50
50
|
return entry.type === "filesystem";
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a resolved source is write-activated. Only the primary stash and
|
|
54
|
+
* sources explicitly marked `writable: true` are writable; registry-cached
|
|
55
|
+
* (installed, read-only) sources are never written in place because
|
|
56
|
+
* `akm update` overwrites them.
|
|
57
|
+
*/
|
|
58
|
+
export function isSourceWriteActivated(source) {
|
|
59
|
+
return source.writable === true;
|
|
60
|
+
}
|
|
52
61
|
/** The two kinds writes are defined for; scheduler state binds only to these. */
|
|
53
62
|
export function isWriteCapableSourceKind(kind) {
|
|
54
63
|
return kind === "filesystem" || kind === "git";
|
|
55
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Throw when `name` is a configured bundle that is disabled. Callers run it
|
|
67
|
+
* after a name failed to resolve against the active set, so a disabled bundle
|
|
68
|
+
* is reported as disabled rather than as not configured.
|
|
69
|
+
*/
|
|
70
|
+
export function assertBundleNotDisabled(akmConfig, name) {
|
|
71
|
+
if (resolveConfiguredSources(akmConfig).some((source) => source.name === name)) {
|
|
72
|
+
throw new UsageError(`Bundle "${name}" is disabled.`, "INVALID_FLAG_VALUE");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
56
75
|
/**
|
|
57
76
|
* Resolve the destination for a write: explicit `--bundle`, then
|
|
58
77
|
* `defaultWriteTarget`, then the working stash (`defaultBundle`). There is no
|
|
59
78
|
* fallback to the first writable source.
|
|
60
79
|
*/
|
|
61
80
|
export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
62
|
-
const allConfiguredSources = resolveConfiguredSources(akmConfig);
|
|
63
81
|
const configuredSources = resolveActiveConfiguredSources(akmConfig);
|
|
64
82
|
const requireWritable = options.requireWritable !== false;
|
|
65
83
|
const flag = options.flag ?? "--bundle";
|
|
66
84
|
if (explicitTarget) {
|
|
67
85
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
68
86
|
if (!match) {
|
|
69
|
-
|
|
70
|
-
throw new UsageError(`Bundle "${explicitTarget}" is disabled.`, "INVALID_FLAG_VALUE");
|
|
71
|
-
}
|
|
87
|
+
assertBundleNotDisabled(akmConfig, explicitTarget);
|
|
72
88
|
throw new UsageError(`${flag} must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
73
89
|
}
|
|
74
90
|
if (requireWritable && !resolveWritable({ type: match.type, writable: match.writable })) {
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -25,7 +25,7 @@ import { INDEX_DB_VACUUMED_EVENT, readFreelistInfo, vacuumIfReclaimable } from "
|
|
|
25
25
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
26
26
|
import { reclassifyIndexDbContention } from "./index-db-contention.js";
|
|
27
27
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
28
|
-
import { indexedPathMatchesOwner,
|
|
28
|
+
import { indexedPathMatchesOwner, resolveConceptOwner } from "./lookup/adapter-concept-owner.js";
|
|
29
29
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
30
30
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
31
31
|
import { isWorkflowSkipWarning, withFileSize } from "./passes/metadata.js";
|
|
@@ -669,9 +669,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
669
669
|
}
|
|
670
670
|
const walked = walkStashFlatWithStatus(currentStashDir, {
|
|
671
671
|
includeAllDirectories: component.adapter === "okf",
|
|
672
|
-
...(component.adapter === "akm" || component.adapter === "akm-workflow"
|
|
673
|
-
? { workflowSymlinkAdapter: component.adapter }
|
|
674
|
-
: {}),
|
|
675
672
|
});
|
|
676
673
|
const dirGroups = groupFileContextsByDir(walked.files);
|
|
677
674
|
const adapter = adapterForId(component.adapter);
|
|
@@ -1132,10 +1129,8 @@ borrowed = false) {
|
|
|
1132
1129
|
const sources = await resolveLookupSources();
|
|
1133
1130
|
if (sources.length === 0)
|
|
1134
1131
|
return { entry: null };
|
|
1135
|
-
const bundleBySourcePath = new Map(sourceComponents(sources).map((component, index) => [path.resolve(sources[index].path), component.id]));
|
|
1136
1132
|
const qualified = Boolean(ref.bundle);
|
|
1137
|
-
|
|
1138
|
-
if (candidateSources.length === 0)
|
|
1133
|
+
if ((ref.bundle ? resolveSourcesForOrigin(ref.bundle, sources) : sources).length === 0)
|
|
1139
1134
|
return { entry: null };
|
|
1140
1135
|
let db;
|
|
1141
1136
|
let indexError;
|
|
@@ -1151,42 +1146,38 @@ borrowed = false) {
|
|
|
1151
1146
|
...(indexError === undefined ? {} : { indexError }),
|
|
1152
1147
|
});
|
|
1153
1148
|
try {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
catch (error) {
|
|
1172
|
-
if (!(error instanceof WorkflowSourceIdentityError))
|
|
1173
|
-
throw error;
|
|
1174
|
-
warn(`${error.message} Falling back to the physical owner.`);
|
|
1175
|
-
return resolved(null, owner);
|
|
1149
|
+
const match = resolveConceptOwner(ref, sources);
|
|
1150
|
+
if (!match)
|
|
1151
|
+
return resolved(null);
|
|
1152
|
+
const { owner, source, bundleId, adapterId } = match;
|
|
1153
|
+
const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, owner.conceptId);
|
|
1154
|
+
const id = db ? findEntryIdByRef(db, inputRef, bundleId) : undefined;
|
|
1155
|
+
if (id !== undefined && db) {
|
|
1156
|
+
const entry = readLookupEntry(db, id, ref.conceptId, source.path);
|
|
1157
|
+
if (entry) {
|
|
1158
|
+
if (owner.workflowSource) {
|
|
1159
|
+
try {
|
|
1160
|
+
assertIndexedWorkflowSourceIdentity(inputRef, entry.filePath, owner.workflowSource);
|
|
1161
|
+
if (entry.adapterId !== adapterId) {
|
|
1162
|
+
throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
|
|
1176
1163
|
}
|
|
1177
1164
|
}
|
|
1178
|
-
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
if (!(error instanceof WorkflowSourceIdentityError))
|
|
1167
|
+
throw error;
|
|
1168
|
+
warn(`${error.message} Falling back to the physical owner.`);
|
|
1179
1169
|
return resolved(null, owner);
|
|
1180
1170
|
}
|
|
1181
|
-
return resolved(entry, owner);
|
|
1182
1171
|
}
|
|
1172
|
+
else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
|
|
1173
|
+
return resolved(null, owner);
|
|
1174
|
+
}
|
|
1175
|
+
return resolved(entry, owner);
|
|
1183
1176
|
}
|
|
1184
|
-
// A physical owner with a missing/incomplete index row still owns this
|
|
1185
|
-
// unqualified concept. Stop here so a later source cannot retarget it.
|
|
1186
|
-
if (owner)
|
|
1187
|
-
return resolved(null, owner);
|
|
1188
1177
|
}
|
|
1189
|
-
|
|
1178
|
+
// A physical owner with a missing/incomplete index row still owns this
|
|
1179
|
+
// concept: no later source can retarget it.
|
|
1180
|
+
return resolved(null, owner);
|
|
1190
1181
|
}
|
|
1191
1182
|
finally {
|
|
1192
1183
|
if (db && !borrowed)
|
|
@@ -19,12 +19,6 @@
|
|
|
19
19
|
* spec §1.3). Slugs are made unique WITHIN a derivation batch
|
|
20
20
|
* by appending a short path-hash suffix on collision, so two
|
|
21
21
|
* sources sharing a basename never mint the same bundle id.
|
|
22
|
-
* - `trusted` = `source.writable === true`. 0.9.0 ships no first-class trust
|
|
23
|
-
* record (spec §1.3 — "no new trust machinery"); the writable
|
|
24
|
-
* primary/config stashes are the user's own and count as
|
|
25
|
-
* trusted, read-only registry caches do not. Nothing consumes
|
|
26
|
-
* `trusted` in the live path yet (installation grants nothing,
|
|
27
|
-
* History D8) — this is the Tier-A placeholder mapping.
|
|
28
22
|
* - components = ONE component for the current single-root akm layout
|
|
29
23
|
* (spec §1.2 rule 5): `{ id, root: source.path, adapter }`. The
|
|
30
24
|
* akm layout's type dirs (`knowledge/`, `skills/`, …) are NOT
|
|
@@ -56,10 +50,10 @@
|
|
|
56
50
|
* fallback is **`akm`** (see {@link FALLBACK_ADAPTER_ID}), preserving workspace
|
|
57
51
|
* root behavior. Explicit configured ownership always wins over probing.
|
|
58
52
|
*/
|
|
59
|
-
import { isSourceWriteActivated } from "../core/activation-policy.js";
|
|
60
53
|
import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
61
54
|
import { stashDirFor } from "../core/asset/asset-placement.js";
|
|
62
55
|
import { deriveBundleId, deriveBundleIds, slugForPath } from "../core/bundle-id.js";
|
|
56
|
+
import { isSourceWriteActivated } from "../core/write-source.js";
|
|
63
57
|
export { deriveBundleId, deriveBundleIds, slugForPath };
|
|
64
58
|
/**
|
|
65
59
|
* The no-probe-match fallback adapter id.
|
|
@@ -98,7 +92,6 @@ export function deriveInstallations(sources) {
|
|
|
98
92
|
const adapter = source.adapterId ?? detectAdapterId(source.path, FALLBACK_ADAPTER_ID);
|
|
99
93
|
installations.push({
|
|
100
94
|
id,
|
|
101
|
-
trusted: writable,
|
|
102
95
|
components: [
|
|
103
96
|
{
|
|
104
97
|
// Single-component akm layout: the component id == the bundle id so
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
6
7
|
import { adapterForId } from "../../core/adapter/registry.js";
|
|
7
|
-
import { compareCodePoints, hasErrnoCode
|
|
8
|
+
import { compareCodePoints, hasErrnoCode } from "../../core/common.js";
|
|
8
9
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
9
10
|
import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
|
|
11
|
+
import { sensitiveMarkerPath } from "../../core/sensitive-marker-path.js";
|
|
10
12
|
import { warnOnce } from "../../core/warn.js";
|
|
11
13
|
import { resolveUniqueWorkflowSource, workflowNameForConceptId, } from "../../workflows/source-files.js";
|
|
14
|
+
import { deriveInstallations } from "../installations.js";
|
|
12
15
|
import { buildFileContext } from "../walk/file-context.js";
|
|
13
16
|
const CONTENT_READ_REQUIRED = Symbol("adapter ownership probe requires content");
|
|
14
17
|
export class AdapterConceptOwnershipError extends UsageError {
|
|
@@ -40,22 +43,18 @@ function lexicallyWithin(candidate, root) {
|
|
|
40
43
|
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
41
44
|
}
|
|
42
45
|
/** Add case-only suffix spellings beside an adapter-authored read candidate. */
|
|
43
|
-
function candidateSpellings(candidate, sourcePath
|
|
46
|
+
function candidateSpellings(candidate, sourcePath) {
|
|
44
47
|
const authored = path.resolve(candidate.path);
|
|
45
48
|
if (!lexicallyWithin(authored, sourcePath))
|
|
46
49
|
return [{ ...candidate, path: authored }];
|
|
47
50
|
const parent = path.dirname(authored);
|
|
48
|
-
let realParent;
|
|
49
51
|
let entries;
|
|
50
52
|
try {
|
|
51
|
-
realParent = fs.realpathSync(parent);
|
|
52
53
|
entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
53
54
|
}
|
|
54
55
|
catch {
|
|
55
56
|
return [{ ...candidate, path: authored }];
|
|
56
57
|
}
|
|
57
|
-
if (!isWithin(realParent, realRoot))
|
|
58
|
-
return [{ ...candidate, path: authored }];
|
|
59
58
|
const extension = path.extname(authored);
|
|
60
59
|
if (!extension)
|
|
61
60
|
return [{ ...candidate, path: authored }];
|
|
@@ -67,7 +66,7 @@ function candidateSpellings(candidate, sourcePath, realRoot) {
|
|
|
67
66
|
})
|
|
68
67
|
.map((entry) => ({ ...candidate, path: path.join(parent, entry.name) }));
|
|
69
68
|
}
|
|
70
|
-
function inspectCandidate(sourcePath,
|
|
69
|
+
function inspectCandidate(sourcePath, adapterId, candidate) {
|
|
71
70
|
const authoredPath = path.resolve(candidate.path);
|
|
72
71
|
if (!lexicallyWithin(authoredPath, sourcePath))
|
|
73
72
|
return undefined;
|
|
@@ -87,8 +86,6 @@ function inspectCandidate(sourcePath, realRoot, adapterId, candidate) {
|
|
|
87
86
|
catch {
|
|
88
87
|
return undefined;
|
|
89
88
|
}
|
|
90
|
-
if (!isWithin(realPath, realRoot))
|
|
91
|
-
return undefined;
|
|
92
89
|
try {
|
|
93
90
|
if (!fs.statSync(realPath).isFile())
|
|
94
91
|
return undefined;
|
|
@@ -118,6 +115,14 @@ function claimsWithoutContent(adapter, component, owner) {
|
|
|
118
115
|
throw error;
|
|
119
116
|
}
|
|
120
117
|
}
|
|
118
|
+
/** True for an env/secret file that a sibling `.sensitive` marker hides from the index. */
|
|
119
|
+
function isMarkedSensitive(owner) {
|
|
120
|
+
const head = owner.conceptId.split("/")[0];
|
|
121
|
+
if (head !== "env" && head !== "secrets")
|
|
122
|
+
return false;
|
|
123
|
+
const marker = sensitiveMarkerPath(owner.path, head === "env" ? "env" : "secret");
|
|
124
|
+
return marker !== owner.path && fs.existsSync(marker);
|
|
125
|
+
}
|
|
121
126
|
/**
|
|
122
127
|
* Resolve one adapter/component's exact physical concept owner without reading
|
|
123
128
|
* authored bytes. Native workflow arbitration is reused verbatim; every other
|
|
@@ -129,9 +134,8 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
|
|
|
129
134
|
const normalized = normalizedConceptId(conceptId);
|
|
130
135
|
if (!adapter || !normalized)
|
|
131
136
|
return undefined;
|
|
132
|
-
let realRoot;
|
|
133
137
|
try {
|
|
134
|
-
|
|
138
|
+
fs.realpathSync(sourcePath);
|
|
135
139
|
}
|
|
136
140
|
catch (error) {
|
|
137
141
|
// Genuinely absent — "no owner" is correct. Any other failure (e.g.
|
|
@@ -168,13 +172,13 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
|
|
|
168
172
|
// — see each adapter's own doc comment for its candidate classes), and
|
|
169
173
|
// `candidateSpellings` adds case-only siblings via one `readdirSync` of
|
|
170
174
|
// each candidate's own parent directory.
|
|
171
|
-
const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath
|
|
175
|
+
const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath));
|
|
172
176
|
const inspected = [
|
|
173
177
|
...new Map(spellings.map((candidate) => [`${path.resolve(candidate.path)}\0${candidate.conceptId}`, candidate])).values(),
|
|
174
178
|
]
|
|
175
179
|
.sort((left, right) => compareCodePoints(left.path, right.path))
|
|
176
180
|
.flatMap((candidate) => {
|
|
177
|
-
const owner = inspectCandidate(sourcePath,
|
|
181
|
+
const owner = inspectCandidate(sourcePath, adapterId, candidate);
|
|
178
182
|
return owner ? [owner] : [];
|
|
179
183
|
});
|
|
180
184
|
for (const candidate of inspected) {
|
|
@@ -183,8 +187,10 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
|
|
|
183
187
|
const identity = `${path.resolve(candidate.path)}\0${candidate.conceptId}`;
|
|
184
188
|
if (ownersByIdentity.has(identity))
|
|
185
189
|
continue;
|
|
186
|
-
if (claimsWithoutContent(adapter, component, candidate)
|
|
190
|
+
if (claimsWithoutContent(adapter, component, candidate) ||
|
|
191
|
+
(options?.includeSensitive && isMarkedSensitive(candidate))) {
|
|
187
192
|
ownersByIdentity.set(identity, candidate);
|
|
193
|
+
}
|
|
188
194
|
}
|
|
189
195
|
const owners = [...ownersByIdentity.values()]
|
|
190
196
|
.filter((owner) => owner.conceptId === resolutionConceptId)
|
|
@@ -202,6 +208,26 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
|
|
|
202
208
|
}
|
|
203
209
|
return owners[0];
|
|
204
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The one "where is asset X right now?" walk. `sources` is the ordered source
|
|
213
|
+
* list (`resolveSourceEntries`: default bundle first, then installation
|
|
214
|
+
* priority). The first source with a physical owner for the ref ends the
|
|
215
|
+
* walk; a qualified ref only considers its own bundle. Synchronous and
|
|
216
|
+
* index-free: it never opens a database, so a caller may use it at dispatch.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveConceptOwner(ref, sources, options) {
|
|
219
|
+
const installations = deriveInstallations([...sources]);
|
|
220
|
+
for (const [index, source] of sources.entries()) {
|
|
221
|
+
const bundleId = installations[index]?.id;
|
|
222
|
+
if (!bundleId || (ref.bundle !== undefined && bundleId !== ref.bundle))
|
|
223
|
+
continue;
|
|
224
|
+
const adapterId = source.adapterId ?? detectAdapterId(source.path);
|
|
225
|
+
const owner = resolveAdapterConceptOwner(source.path, adapterId, ref.conceptId, options);
|
|
226
|
+
if (owner)
|
|
227
|
+
return { owner, source, bundleId, adapterId };
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
205
231
|
export function indexedPathMatchesOwner(indexedPath, owner) {
|
|
206
232
|
if (path.resolve(indexedPath) !== path.resolve(owner.path))
|
|
207
233
|
return false;
|
|
@@ -81,7 +81,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
81
81
|
return { success: false, message: "Semantic search is disabled." };
|
|
82
82
|
}
|
|
83
83
|
// #953 field gap: the actionable outcome is a self-diagnosing run, not a
|
|
84
|
-
// fix (every RemoteEmbedder path already resolves
|
|
84
|
+
// fix (every RemoteEmbedder path already resolves `${ref}` credentials through one
|
|
85
85
|
// boundary — a keyless request can only mean embedding.apiKey was absent
|
|
86
86
|
// from the config THIS run loaded). One default-level line, before the
|
|
87
87
|
// first provider request of the phase, naming the endpoint/model/credential
|
|
@@ -98,7 +98,7 @@ export function drainDirDocuments(adapter, component, fileContexts) {
|
|
|
98
98
|
const entry = indexDocumentToStashEntry(doc);
|
|
99
99
|
// Workflow docs: drop-with-warning if broken; otherwise cache a lossless
|
|
100
100
|
// runtime projection when the current executor can represent the source.
|
|
101
|
-
const dropWarning = handleWorkflowDoc(doc, file
|
|
101
|
+
const dropWarning = handleWorkflowDoc(doc, file);
|
|
102
102
|
if (dropWarning !== null) {
|
|
103
103
|
warnings.push(dropWarning);
|
|
104
104
|
if (workflowName !== undefined)
|
|
@@ -138,13 +138,13 @@ export function recognizeStashEntries(stashRoot, files) {
|
|
|
138
138
|
* `Skipped workflow …` drop warning when it is broken, or return `null` when
|
|
139
139
|
* it compiles. Non-workflow docs return `null` immediately.
|
|
140
140
|
*/
|
|
141
|
-
function handleWorkflowDoc(doc, file
|
|
141
|
+
function handleWorkflowDoc(doc, file) {
|
|
142
142
|
if (doc.type !== "workflow" ||
|
|
143
143
|
(doc.adapterId !== "akm" && doc.adapterId !== "akm-workflow") ||
|
|
144
144
|
(docRenderer(doc) !== WORKFLOW_MD_RENDERER && doc.adapterId !== "akm-workflow")) {
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
|
-
const result = compileWorkflowSource(file.content(), { path: file.relPath
|
|
147
|
+
const result = compileWorkflowSource(file.content(), { path: file.relPath });
|
|
148
148
|
if (!result.ok)
|
|
149
149
|
return workflowDropWarning(file, result.errors);
|
|
150
150
|
return null;
|
|
@@ -43,7 +43,9 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
43
43
|
}
|
|
44
44
|
// Explicit and environment overrides stay first. Without either override,
|
|
45
45
|
// the configured default enters through the validated loop below.
|
|
46
|
-
const sources = implicitStashDir
|
|
46
|
+
const sources = implicitStashDir
|
|
47
|
+
? [{ path: implicitStashDir, writable: true, isDefault: true, type: "filesystem" }]
|
|
48
|
+
: [];
|
|
47
49
|
const sourceIdentity = (dir) => {
|
|
48
50
|
const resolved = path.resolve(dir);
|
|
49
51
|
try {
|
|
@@ -65,7 +67,7 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
65
67
|
// identity-less and a user-named primary source ("name": "my-stash")
|
|
66
68
|
// would validate but match zero entries when filtering.
|
|
67
69
|
const existing = sources.find((source) => sourceIdentity(source.path) === identity);
|
|
68
|
-
if (existing && existing.
|
|
70
|
+
if (existing && existing.registryId === undefined) {
|
|
69
71
|
if (registryId)
|
|
70
72
|
existing.registryId = registryId;
|
|
71
73
|
if (isDefault)
|
|
@@ -1,55 +1,15 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
7
|
-
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
8
|
-
import { isWithin } from "../../core/common.js";
|
|
9
|
-
import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
|
|
4
|
+
import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
10
5
|
import { lookup } from "../indexer.js";
|
|
11
6
|
import { deriveInstallations } from "../installations.js";
|
|
7
|
+
import { resolveConceptOwner } from "../lookup/adapter-concept-owner.js";
|
|
12
8
|
import { resolveSourceEntries } from "../search/search-source.js";
|
|
13
9
|
function normalizeRef(ref) {
|
|
14
10
|
// String inputs use the current `[bundle//]conceptId` grammar.
|
|
15
11
|
return typeof ref === "string" ? parseRefInput(ref) : ref;
|
|
16
12
|
}
|
|
17
|
-
function buildDiskCandidates(sourcePath, ref, preserveDirectNameFallback) {
|
|
18
|
-
const placementStashDir = stashDirFor(ref.type);
|
|
19
|
-
// Opaque adapter conceptIds are index-resolved. Core cannot infer their
|
|
20
|
-
// adapter-owned serialization or path extension before indexing.
|
|
21
|
-
if (placementStashDir === undefined)
|
|
22
|
-
return [];
|
|
23
|
-
const typeDir = path.join(sourcePath, placementStashDir);
|
|
24
|
-
const candidates = [
|
|
25
|
-
assetPathForName(ref.type, typeDir, ref.name),
|
|
26
|
-
path.join(sourcePath, ref.type, `${ref.name}.md`),
|
|
27
|
-
path.join(sourcePath, ref.type, ref.name),
|
|
28
|
-
];
|
|
29
|
-
if (preserveDirectNameFallback) {
|
|
30
|
-
candidates.push(path.join(sourcePath, `${ref.name}.md`), path.join(sourcePath, ref.name));
|
|
31
|
-
}
|
|
32
|
-
return candidates;
|
|
33
|
-
}
|
|
34
|
-
function resolveDirectoryEntry(filePath, directoryIndexNames) {
|
|
35
|
-
let stat;
|
|
36
|
-
try {
|
|
37
|
-
stat = fs.statSync(filePath);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
if (stat.isFile())
|
|
43
|
-
return filePath;
|
|
44
|
-
if (!stat.isDirectory())
|
|
45
|
-
return null;
|
|
46
|
-
for (const indexName of directoryIndexNames) {
|
|
47
|
-
const candidate = path.join(filePath, indexName);
|
|
48
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
49
|
-
return candidate;
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
13
|
async function resolveViaIndex(ref) {
|
|
54
14
|
try {
|
|
55
15
|
const entry = await lookup(ref);
|
|
@@ -60,32 +20,17 @@ async function resolveViaIndex(ref) {
|
|
|
60
20
|
}
|
|
61
21
|
}
|
|
62
22
|
function resolveViaDisk(ref, options) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const candidates = buildDiskCandidates(source.path, ref, preserveDirectNameFallback);
|
|
75
|
-
for (const candidate of candidates) {
|
|
76
|
-
if (!fs.existsSync(candidate))
|
|
77
|
-
continue;
|
|
78
|
-
const resolved = resolveDirectoryEntry(candidate, directoryIndexNames);
|
|
79
|
-
if (!resolved)
|
|
80
|
-
continue;
|
|
81
|
-
const resolvedRoot = fs.realpathSync(source.path);
|
|
82
|
-
const realTarget = fs.realpathSync(resolved);
|
|
83
|
-
if (!isWithin(realTarget, resolvedRoot))
|
|
84
|
-
continue;
|
|
85
|
-
return realTarget;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
23
|
+
const allSources = resolveSourceEntries(options.stashDir);
|
|
24
|
+
const installations = deriveInstallations(allSources);
|
|
25
|
+
const sources = options.writableBundleIds
|
|
26
|
+
? allSources.filter((_, index) => {
|
|
27
|
+
const bundleId = installations[index]?.id;
|
|
28
|
+
return bundleId !== undefined && options.writableBundleIds?.has(bundleId);
|
|
29
|
+
})
|
|
30
|
+
: allSources;
|
|
31
|
+
const bundle = options.honorOrigin !== false ? ref.origin : undefined;
|
|
32
|
+
return (resolveConceptOwner({ bundle, conceptId: conceptIdFromTypeName(ref.type, ref.name) }, sources)?.owner.realPath ??
|
|
33
|
+
null);
|
|
89
34
|
}
|
|
90
35
|
export async function resolveAssetPath(ref, options = {}) {
|
|
91
36
|
const parsed = normalizeRef(ref);
|