akm-cli 0.9.3 → 0.9.5
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 +233 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +81 -3
- package/dist/commands/tasks/tasks.js +117 -2
- package/dist/core/adapter/adapters/akm-adapter.js +23 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +18 -5
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12678 -12601
- package/dist/scripts/akm-migrate.js +12678 -12601
- package/dist/storage/repositories/proposals-repository.js +65 -7
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +45 -0
- package/dist/tasks/scheduler-sync.js +77 -41
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +512 -0
- package/dist/tasks/source/task-to-v4.js +457 -0
- package/docs/reference/cli.md +33 -6
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +4 -4
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
15
15
|
import { rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
16
16
|
import { appendEvent } from "../../core/events.js";
|
|
17
|
-
import { isTransientStashPath } from "../../core/paths.js";
|
|
18
17
|
import { resolveReadSources } from "../../indexer/read-preflight.js";
|
|
19
18
|
import { searchLocal } from "../../indexer/search/db-search.js";
|
|
20
19
|
import { getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
21
20
|
import { getEntryIdByFilePath, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
22
|
-
import { bumpUtilityScoresBatch } from "../../storage/repositories/index-utility-repository.js";
|
|
23
|
-
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
24
21
|
// Eagerly import source providers to trigger self-registration before the
|
|
25
22
|
// indexer or path-resolution code runs.
|
|
26
23
|
import "../../sources/providers/index.js";
|
|
@@ -188,7 +185,7 @@ function usageSearchMode(mode) {
|
|
|
188
185
|
function maybeLogSearchEvent(input, query, response, mode) {
|
|
189
186
|
if (input.skipLogging)
|
|
190
187
|
return;
|
|
191
|
-
logSearchEvent(query, response, mode, input.eventSource, input.
|
|
188
|
+
logSearchEvent(query, response, mode, input.eventSource, input.attributionProjection);
|
|
192
189
|
}
|
|
193
190
|
/**
|
|
194
191
|
* Resolve entry IDs by file_path lookup (exact match, not LIKE).
|
|
@@ -229,7 +226,7 @@ function resolveEntryIds(db, hits) {
|
|
|
229
226
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
230
227
|
* have no local entry_id to reference.
|
|
231
228
|
*/
|
|
232
|
-
function logSearchEvent(query, response, mode = "keyword", eventSource = "user",
|
|
229
|
+
function logSearchEvent(query, response, mode = "keyword", eventSource = "user", attributionProjection = "full") {
|
|
233
230
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
234
231
|
// detect akm search invocations without relying on stdout scraping.
|
|
235
232
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
@@ -276,25 +273,18 @@ function logSearchEvent(query, response, mode = "keyword", eventSource = "user",
|
|
|
276
273
|
source: eventSource,
|
|
277
274
|
});
|
|
278
275
|
}, TELEMETRY_BUSY_TIMEOUT_MS);
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// Non-fatal — fall back to global-only bumps on any error.
|
|
295
|
-
}
|
|
296
|
-
bumpUtilityScoresBatch(db, resolvedIds, 1.0, 0.1, scopeKey);
|
|
297
|
-
}
|
|
276
|
+
// No live utility_scores/utility_scores_scoped write here (#862): a
|
|
277
|
+
// search result is an impression, not a signal that the asset was
|
|
278
|
+
// useful. Rewarding every returned hit created a feedback loop where
|
|
279
|
+
// merely appearing in results inflated future ranking — assets
|
|
280
|
+
// surfaced because they'd surfaced before, not because a user acted
|
|
281
|
+
// on them. Retrieval counts are still recorded above via
|
|
282
|
+
// insertUsageEvent (search_count) and rolled into utility_scores by
|
|
283
|
+
// the offline `recomputeUtilityScores` pass (`akm index`), which uses
|
|
284
|
+
// the show/search *select rate* — a ratio that requires an actual
|
|
285
|
+
// `show`/select event, not raw impressions. Explicit signal comes
|
|
286
|
+
// from `akm feedback` (applyFeedbackToUtilityScore) and from
|
|
287
|
+
// selection (recordShowUsage / the `select` event derived from it).
|
|
298
288
|
}, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
|
|
299
289
|
}
|
|
300
290
|
catch (err) {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
import { defineCommand } from "citty";
|
|
28
28
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
29
29
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
30
|
-
import { defineGroupCommand, defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
30
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
32
|
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
33
33
|
import { akmTaskExplain } from "./explain.js";
|
|
34
|
-
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksRun, akmTasksSync } from "./tasks.js";
|
|
34
|
+
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
|
|
35
35
|
/** Shared `--bundle <bundle>` arg wired onto every task subcommand. */
|
|
36
36
|
const bundleArg = {
|
|
37
37
|
bundle: {
|
|
@@ -303,6 +303,18 @@ const tasksHistoryCommand = defineJsonCommand({
|
|
|
303
303
|
output("task-history", result);
|
|
304
304
|
},
|
|
305
305
|
});
|
|
306
|
+
/**
|
|
307
|
+
* #849: `task sync --dry-run`'s exit-code contract, "non-zero when the plan
|
|
308
|
+
* contains removals" — factored out as a pure function (rather than left
|
|
309
|
+
* inline in the command's `run()`) so it's directly unit-testable without
|
|
310
|
+
* driving the whole CLI through a real scheduler backend. #867: also
|
|
311
|
+
* non-zero when any source failed to parse/prepare — sync degrades (still
|
|
312
|
+
* reconciles the tasks/workflows that DID parse) rather than rejecting the
|
|
313
|
+
* whole set, but a dropped source must still surface as a failing exit.
|
|
314
|
+
*/
|
|
315
|
+
export function taskSyncDryRunExitCode(preview) {
|
|
316
|
+
return preview.hasRemovals || (preview.failures?.length ?? 0) > 0 ? EXIT_CODES.GENERAL : undefined;
|
|
317
|
+
}
|
|
306
318
|
const tasksSyncCommand = defineJsonCommand({
|
|
307
319
|
meta: {
|
|
308
320
|
name: "sync",
|
|
@@ -315,11 +327,33 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
315
327
|
description: "Replace installed bindings with the current invocation",
|
|
316
328
|
default: false,
|
|
317
329
|
},
|
|
330
|
+
"dry-run": {
|
|
331
|
+
type: "boolean",
|
|
332
|
+
description: "Compute and print the full reconcile plan (adds/updates/removes, with owning bundle on every " +
|
|
333
|
+
"removal) without touching the OS scheduler. Zero durable writes. Exits non-zero when the plan " +
|
|
334
|
+
"contains removals.",
|
|
335
|
+
default: false,
|
|
336
|
+
},
|
|
318
337
|
},
|
|
319
338
|
async run({ args }) {
|
|
320
339
|
rejectRetiredTaskTargetFlag();
|
|
321
|
-
const
|
|
340
|
+
const rebind = args.rebind === true;
|
|
341
|
+
if (args["dry-run"] === true) {
|
|
342
|
+
const preview = await akmTasksSyncPlan({}, args.bundle, { rebind });
|
|
343
|
+
output("task-sync-dry-run", preview);
|
|
344
|
+
const exitCode = taskSyncDryRunExitCode(preview);
|
|
345
|
+
if (exitCode !== undefined)
|
|
346
|
+
process.exitCode = exitCode;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const result = await akmTasksSync({}, args.bundle, { rebind });
|
|
322
350
|
output("task-sync", result);
|
|
351
|
+
// #867: sync degrades — sources that failed to parse/prepare are
|
|
352
|
+
// excluded from reconciliation and reported in `result.failed` rather
|
|
353
|
+
// than poisoning the whole sync, but their presence must still fail
|
|
354
|
+
// the command's exit code so the breakage stays visible.
|
|
355
|
+
if (result.failed.length > 0)
|
|
356
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
323
357
|
},
|
|
324
358
|
});
|
|
325
359
|
// ── `akm task explain` — read-only introspection (P2b Lane B, spec
|
|
@@ -364,6 +398,49 @@ const tasksDoctorCommand = defineJsonCommand({
|
|
|
364
398
|
output("task-doctor", result);
|
|
365
399
|
},
|
|
366
400
|
});
|
|
401
|
+
/**
|
|
402
|
+
* #851: `akm task prune`'s exit-code contract mirrors `task sync --dry-run`'s
|
|
403
|
+
* (`taskSyncDryRunExitCode` above) — non-zero whenever the preview lists
|
|
404
|
+
* removals the invocation didn't (or couldn't, without `--yes`) execute, so
|
|
405
|
+
* `akm task prune` is usable as a CI/health check the same way `sync
|
|
406
|
+
* --dry-run` is.
|
|
407
|
+
*/
|
|
408
|
+
export function taskPruneExitCode(result) {
|
|
409
|
+
return result.dryRun && result.preview.hasRemovals ? EXIT_CODES.GENERAL : undefined;
|
|
410
|
+
}
|
|
411
|
+
const tasksPruneCommand = defineJsonCommand({
|
|
412
|
+
meta: {
|
|
413
|
+
name: "prune",
|
|
414
|
+
description: "Remove installed scheduler entries `sync` can never reclaim because their own descriptor no longer " +
|
|
415
|
+
"resolves to a live bundle (corrupt/missing --scheduler-context, or the owning bundle directory is " +
|
|
416
|
+
"gone). Defaults to a dry-run preview — zero scheduler writes. Requires --yes to remove anything; " +
|
|
417
|
+
"--id narrows removal to specific binding ids (comma-separated).",
|
|
418
|
+
},
|
|
419
|
+
args: {
|
|
420
|
+
yes: {
|
|
421
|
+
type: "boolean",
|
|
422
|
+
description: "Execute removal of every currently-computed orphan (the plan is still printed first)",
|
|
423
|
+
default: false,
|
|
424
|
+
},
|
|
425
|
+
id: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Comma-separated scheduler binding id(s) to limit pruning to (must already be orphan candidates)",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
async run({ args }) {
|
|
431
|
+
const id = args.id
|
|
432
|
+
? args.id
|
|
433
|
+
.split(",")
|
|
434
|
+
.map((value) => value.trim())
|
|
435
|
+
.filter(Boolean)
|
|
436
|
+
: undefined;
|
|
437
|
+
const result = await akmTasksPrune({}, { yes: args.yes === true, id });
|
|
438
|
+
output("task-prune", result);
|
|
439
|
+
const exitCode = taskPruneExitCode(result);
|
|
440
|
+
if (exitCode !== undefined)
|
|
441
|
+
process.exitCode = exitCode;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
367
444
|
export const taskCommand = defineGroupCommand({
|
|
368
445
|
meta: {
|
|
369
446
|
name: "task",
|
|
@@ -375,6 +452,7 @@ export const taskCommand = defineGroupCommand({
|
|
|
375
452
|
explain: tasksExplainCommand,
|
|
376
453
|
history: tasksHistoryCommand,
|
|
377
454
|
sync: tasksSyncCommand,
|
|
455
|
+
prune: tasksPruneCommand,
|
|
378
456
|
doctor: tasksDoctorCommand,
|
|
379
457
|
},
|
|
380
458
|
// Bare `akm task` reports scheduler diagnostics. Inspection of individual
|
|
@@ -35,7 +35,8 @@ import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
|
35
35
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
36
36
|
import { assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../../tasks/scheduler-binding.js";
|
|
37
37
|
import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
|
|
38
|
-
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
38
|
+
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, buildSchedulerRemoveOperation, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
39
|
+
import { renderSchedulerPlanPreview, renderSchedulerSyncPlanPreview, } from "../../tasks/scheduler-sync-preview.js";
|
|
39
40
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
40
41
|
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
41
42
|
import { TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
|
|
@@ -326,7 +327,16 @@ export async function akmTasksHistory(input) {
|
|
|
326
327
|
* scans all bundles. Activation happens only through explicit `add --bundle`
|
|
327
328
|
* (or `sync --bundle` on a bundle whose task files are already present).
|
|
328
329
|
*/
|
|
329
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Compute (but never apply) a scheduler sync plan: everything through
|
|
332
|
+
* `finalizeSchedulerSyncPlan`'s final call, stopping strictly before
|
|
333
|
+
* `applySchedulerSyncPlan`. Shared by `akmTasksSync` (applies the plan) and
|
|
334
|
+
* `akmTasksSyncPlan` (#849 `--dry-run`, never applies it) so the two paths
|
|
335
|
+
* can never drift on what "the plan" means. `prepared?.publish`, the one
|
|
336
|
+
* deferred write-producing closure in this pipeline, is returned but never
|
|
337
|
+
* invoked here — only `applySchedulerSyncPlan` may call it.
|
|
338
|
+
*/
|
|
339
|
+
async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
330
340
|
const resolved = resolveTaskReadBundle(undefined, bundleTarget);
|
|
331
341
|
const config = loadConfig();
|
|
332
342
|
const stashDir = resolved.source.path;
|
|
@@ -393,6 +403,10 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
393
403
|
}
|
|
394
404
|
: {}),
|
|
395
405
|
}, preparedSources);
|
|
406
|
+
return { sched, plan, prepared, warnings };
|
|
407
|
+
}
|
|
408
|
+
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
409
|
+
const { sched, plan, prepared, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
396
410
|
await applySchedulerSyncPlan(sched, plan, prepared?.publish && plan.operations.some((operation) => operation.kind !== "remove")
|
|
397
411
|
? prepared.publish
|
|
398
412
|
: undefined);
|
|
@@ -403,9 +417,110 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
403
417
|
unchanged: [...plan.unchanged],
|
|
404
418
|
skipped: [],
|
|
405
419
|
backend: sched.name,
|
|
420
|
+
failed: plan.failures.map((failure) => ({ ...failure })),
|
|
406
421
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
407
422
|
};
|
|
408
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* `akm task sync --dry-run` (#849): compute the exact same plan
|
|
426
|
+
* `akmTasksSync` would apply, then return a non-mutating preview instead of
|
|
427
|
+
* calling `applySchedulerSyncPlan`. `buildSchedulerSyncPlan` is shared with
|
|
428
|
+
* the real sync path specifically so this can never see a different plan
|
|
429
|
+
* than the one a real sync would apply — and specifically so this function
|
|
430
|
+
* never even holds a reference to a callable `publish` closure past this
|
|
431
|
+
* point: `prepared.publish`, if any, is dropped on the floor here, never
|
|
432
|
+
* invoked. Zero durable writes, mirroring `akm workflow plan`.
|
|
433
|
+
*/
|
|
434
|
+
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
435
|
+
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
436
|
+
return renderSchedulerSyncPlanPreview(sched.name, plan);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Classify one installed scheduler binding as a prune candidate (#851), using
|
|
440
|
+
* the same two signals `doctor`'s `inspectInstalledBinding` already computes
|
|
441
|
+
* — deliberately narrower than that function's full `status` set. Only an
|
|
442
|
+
* entry whose ownership can NEVER be resolved (`invalid-context`) or whose
|
|
443
|
+
* resolved owner no longer exists on disk (`dead-bundle-path`) is a
|
|
444
|
+
* candidate; `missing-path` (e.g. the akm binary itself moved) is a
|
|
445
|
+
* different failure mode and is intentionally NOT folded in here, per the
|
|
446
|
+
* scoping in #851 — an entry that still resolves to a live bundle is never a
|
|
447
|
+
* candidate, full stop.
|
|
448
|
+
*/
|
|
449
|
+
function classifyPruneCandidate(entry) {
|
|
450
|
+
let ownerBundlePath;
|
|
451
|
+
try {
|
|
452
|
+
ownerBundlePath = validateSchedulerContextDescriptor(entry.contextPath).environment.AKM_BUNDLE_DIR;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return "invalid-context";
|
|
456
|
+
}
|
|
457
|
+
if (ownerBundlePath !== undefined && !fs.existsSync(ownerBundlePath))
|
|
458
|
+
return "dead-bundle-path";
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
async function buildTaskPrunePlan(deps = {}, options = {}) {
|
|
462
|
+
const sched = deps.backend ?? selectBackend();
|
|
463
|
+
if (!sched.inspectBindings) {
|
|
464
|
+
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for prune.`, "INVALID_CONFIG_FILE");
|
|
465
|
+
}
|
|
466
|
+
const inspection = await sched.inspectBindings({});
|
|
467
|
+
const candidates = new Map();
|
|
468
|
+
for (const entry of inspection.installed) {
|
|
469
|
+
const reason = classifyPruneCandidate(entry);
|
|
470
|
+
if (reason)
|
|
471
|
+
candidates.set(entry.id, reason);
|
|
472
|
+
}
|
|
473
|
+
const requestedIds = options.id?.filter((id) => id.length > 0) ?? [];
|
|
474
|
+
for (const id of requestedIds) {
|
|
475
|
+
if (!candidates.has(id)) {
|
|
476
|
+
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
477
|
+
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const idFilter = requestedIds.length > 0 ? new Set(requestedIds) : undefined;
|
|
481
|
+
const resolved = resolveTaskReadBundle(undefined, undefined);
|
|
482
|
+
const bundleContext = {
|
|
483
|
+
adapterId: resolved.source.adapterId ?? detectAdapterId(resolved.source.path),
|
|
484
|
+
bundleName: resolved.source.name,
|
|
485
|
+
};
|
|
486
|
+
const operations = [];
|
|
487
|
+
for (const entry of inspection.installed) {
|
|
488
|
+
const reason = candidates.get(entry.id);
|
|
489
|
+
if (!reason)
|
|
490
|
+
continue;
|
|
491
|
+
if (idFilter && !idFilter.has(entry.id))
|
|
492
|
+
continue;
|
|
493
|
+
const operation = buildSchedulerRemoveOperation(entry.id, entry, inspection.artifacts, bundleContext);
|
|
494
|
+
operations.push(Object.freeze({ ...operation, reason }));
|
|
495
|
+
}
|
|
496
|
+
return { sched, operations: Object.freeze(operations) };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* `akm task prune` (#851): remove installed scheduler bindings `sync` can
|
|
500
|
+
* never reclaim because their own `--scheduler-context` descriptor doesn't
|
|
501
|
+
* resolve to a live bundle. Defaults to dry-run — no `--yes` and no `--id`
|
|
502
|
+
* means zero backend calls that could mutate anything, matching
|
|
503
|
+
* `akmTasksSyncPlan`'s zero-write guarantee. `--id` (one or more) narrows
|
|
504
|
+
* execution to exactly those bindings; `--yes` alone executes every
|
|
505
|
+
* currently-computed candidate. Both still return the full preview so the
|
|
506
|
+
* plan is never silent about what it did.
|
|
507
|
+
*/
|
|
508
|
+
export async function akmTasksPrune(deps = {}, options = {}) {
|
|
509
|
+
const { sched, operations } = await buildTaskPrunePlan(deps, options);
|
|
510
|
+
const preview = renderSchedulerPlanPreview(sched.name, operations);
|
|
511
|
+
if (!options.yes) {
|
|
512
|
+
return { backend: sched.name, dryRun: true, preview, removed: [] };
|
|
513
|
+
}
|
|
514
|
+
await applySchedulerTransaction(sched, operations, {
|
|
515
|
+
initialExpectations: operations.map((operation) => operation.expected),
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
backend: sched.name,
|
|
519
|
+
dryRun: false,
|
|
520
|
+
preview,
|
|
521
|
+
removed: operations.map((operation) => operation.id),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
409
524
|
export async function akmTasksDoctor(deps = {}) {
|
|
410
525
|
const warnings = [];
|
|
411
526
|
let invocation = {
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
import fs from "node:fs";
|
|
83
83
|
import path from "node:path";
|
|
84
84
|
import { applyPostContributorFields, applyPreContributorFields, extractPackageMetadata, } from "../../../indexer/passes/metadata.js";
|
|
85
|
-
import { assetPathForName, deriveCanonicalAssetNameFromStashRoot, placementTypes, stashDirFor, stashDirNames, } from "../../asset/asset-placement.js";
|
|
85
|
+
import { assetPathCandidatesForName, assetPathForName, deriveCanonicalAssetNameFromStashRoot, placementTypes, stashDirFor, stashDirNames, } from "../../asset/asset-placement.js";
|
|
86
86
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
87
87
|
import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from "../execution-source.js";
|
|
88
|
-
import { recognizeMatch
|
|
88
|
+
import { recognizeMatch } from "../recognize-match.js";
|
|
89
89
|
import { perTypeValidateChecks, skillDirectoryDiagnostics, workflowYamlSourceDiagnostics } from "./akm-lint.js";
|
|
90
90
|
import { applyFoldedMetadata, foldRecognizedMetadata } from "./akm-metadata.js";
|
|
91
91
|
import { hashContent, runBaseValidateChecks } from "./shared.js";
|
|
@@ -223,6 +223,8 @@ function indexDocumentFromEntry(entry, base, rendererName) {
|
|
|
223
223
|
doc.tags = entry.tags;
|
|
224
224
|
if (entry.content !== undefined)
|
|
225
225
|
doc.content = entry.content;
|
|
226
|
+
if (entry.contentTruncated !== undefined)
|
|
227
|
+
doc.contentTruncated = entry.contentTruncated;
|
|
226
228
|
if (entry.aliases !== undefined)
|
|
227
229
|
doc.aliases = entry.aliases;
|
|
228
230
|
if (entry.searchHints !== undefined)
|
|
@@ -252,14 +254,6 @@ function conceptIdForRecognizedType(root, filePath, type) {
|
|
|
252
254
|
const stashDir = stashDirFor(type);
|
|
253
255
|
return stashDir !== undefined ? `${stashDir}/${canonicalName}` : canonicalName;
|
|
254
256
|
}
|
|
255
|
-
function recognizePathCandidates(c, file) {
|
|
256
|
-
if (isReservedFileName(file.fileName) || akmStashAbstains(c.root, file.absPath))
|
|
257
|
-
return [];
|
|
258
|
-
return recognizePathCandidateMatches(file).flatMap((match) => {
|
|
259
|
-
const conceptId = conceptIdForRecognizedType(c.root, file.absPath, match.type);
|
|
260
|
-
return conceptId === undefined ? [] : [conceptId];
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
257
|
function recognize(c, file) {
|
|
264
258
|
// D-R6 (spec §5.1): `index.md` / `log.md` are OKF reserved structural files at
|
|
265
259
|
// every depth — never items. Excluded BEFORE classification so a directory
|
|
@@ -481,9 +475,19 @@ export const akmAdapter = {
|
|
|
481
475
|
".kts",
|
|
482
476
|
],
|
|
483
477
|
recognize,
|
|
484
|
-
recognizePathCandidates,
|
|
485
478
|
renderExecutionSource,
|
|
486
479
|
validate,
|
|
480
|
+
/**
|
|
481
|
+
* Closed-form owner candidates (#857): every physical path spelling that
|
|
482
|
+
* could claim `conceptId` without walking the bundle. `deriveCanonicalAssetNameFromStashRoot`
|
|
483
|
+
* (recognize's own conceptId derivation) is a pure function of (type,
|
|
484
|
+
* filePath); this is its inverse, enumerated for the two placements that
|
|
485
|
+
* function can produce for one conceptId — CANONICAL (authored under the
|
|
486
|
+
* type's own stash subdir) and the LOOSE FALLBACK (authored anywhere else
|
|
487
|
+
* in the bundle, so the canonical name is the file's full path relative to
|
|
488
|
+
* the bundle root instead of the stash subdir). `assetPathCandidatesForName`
|
|
489
|
+
* additionally expands `env`'s `.env`/`<name>.env` duality on each.
|
|
490
|
+
*/
|
|
487
491
|
readCandidates(c, conceptId) {
|
|
488
492
|
const posix = conceptId.replace(/\\/g, "/");
|
|
489
493
|
const slash = posix.indexOf("/");
|
|
@@ -492,9 +496,14 @@ export const akmAdapter = {
|
|
|
492
496
|
const head = posix.slice(0, slash);
|
|
493
497
|
const rest = posix.slice(slash + 1);
|
|
494
498
|
const type = stashDirToType(head);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
499
|
+
if (type === undefined || rest.length === 0)
|
|
500
|
+
return [];
|
|
501
|
+
const canonical = assetPathCandidatesForName(type, path.join(c.root, head), rest);
|
|
502
|
+
const loose = assetPathCandidatesForName(type, c.root, rest);
|
|
503
|
+
return [...new Set([...canonical, ...loose])].map((candidatePath) => ({
|
|
504
|
+
path: candidatePath,
|
|
505
|
+
conceptId: posix,
|
|
506
|
+
}));
|
|
498
507
|
},
|
|
499
508
|
/**
|
|
500
509
|
* Type-driven placement (§5.1), reproducing `path-resolver.ts#buildDiskCandidates`:
|
|
@@ -57,6 +57,7 @@ import { taskSourceErrorDetail } from "../../../tasks/source-v3.js";
|
|
|
57
57
|
import { compileWorkflowPlan } from "../../../workflows/ir/compile.js";
|
|
58
58
|
import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
|
|
59
59
|
import { conceptIdForStashFile } from "../../asset/resolve-ref.js";
|
|
60
|
+
import { isAkmRegistryCachePath } from "../../common.js";
|
|
60
61
|
/** Recommended `category` values for facts — `commands/lint/fact-linter.ts:9`. */
|
|
61
62
|
const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
|
|
62
63
|
/** Placeholder markers a workflow stub carries — `commands/lint/workflow-linter.ts:10`. */
|
|
@@ -336,7 +337,7 @@ function lineOf(err) {
|
|
|
336
337
|
* like the established Markdown frontend without inventing another parser.
|
|
337
338
|
*/
|
|
338
339
|
export function workflowYamlSourceDiagnostics(relPath, raw, parsePath, workspaceRoot) {
|
|
339
|
-
if (
|
|
340
|
+
if (isAkmRegistryCachePath(parsePath))
|
|
340
341
|
return { errors: [], warnings: [] };
|
|
341
342
|
const compiled = compileWorkflowSource(raw, { path: parsePath, workspaceRoot });
|
|
342
343
|
if (compiled.ok)
|
|
@@ -369,7 +370,7 @@ export function workflowYamlSourceDiagnostics(relPath, raw, parsePath, workspace
|
|
|
369
370
|
*/
|
|
370
371
|
export function workflowFrontendDiagnostics(relPath, raw, parsePath) {
|
|
371
372
|
const none = { errors: [], warnings: [] };
|
|
372
|
-
if (
|
|
373
|
+
if (isAkmRegistryCachePath(parsePath))
|
|
373
374
|
return none;
|
|
374
375
|
const errors = [];
|
|
375
376
|
const warnings = [];
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
* ── validate (spec §6 task validation column) ──
|
|
18
18
|
*
|
|
19
19
|
* Validation enters the canonical task source parser (`parseTaskSource`,
|
|
20
|
-
* task source v4
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* task source v4 native as of P4 — a `version: 3` or `version: 2` document
|
|
21
|
+
* is auto-read through the in-memory migration shim in
|
|
22
|
+
* `parse-task-source.ts`; only a version the shim's deterministic planners
|
|
23
|
+
* cannot convert, or any other unsupported number, fails closed with
|
|
24
|
+
* `TASK_SCHEMA_VERSION_UNSUPPORTED`). That parser owns the closed key sets,
|
|
25
|
+
* the executable-selector XOR, hostile YAML policy, the `akm/command`
|
|
26
|
+
* builtin, bounds, and physical `working-directory` containment. The
|
|
27
|
+
* adapter only translates a parser failure into the format-family
|
|
28
|
+
* diagnostic shape.
|
|
26
29
|
*
|
|
27
30
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
28
31
|
* `tests/fixtures/bundles/akm-task/` + goldens
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import path from "node:path";
|
|
32
|
-
import { assetPathForName, typeForStashDir } from "../../asset/asset-placement.js";
|
|
32
|
+
import { assetPathCandidatesForName, assetPathForName, typeForStashDir } from "../../asset/asset-placement.js";
|
|
33
33
|
import { dangerousEnvKeyDiagnostics } from "./akm-lint.js";
|
|
34
34
|
import { hashContent } from "./shared.js";
|
|
35
35
|
/** A dotenv bundle is single-component; its one component is `main`. */
|
|
@@ -103,12 +103,6 @@ function conceptIdForPath(type, relativePath) {
|
|
|
103
103
|
const stripped = posix.replace(/\.env$/i, "");
|
|
104
104
|
return stripped.endsWith("/") ? `${stripped}default` : stripped;
|
|
105
105
|
}
|
|
106
|
-
function recognizePathCandidates(_c, file) {
|
|
107
|
-
const type = classify(file.relPath);
|
|
108
|
-
if (type === null || hasSensitiveMarker(file.absPath, type))
|
|
109
|
-
return [];
|
|
110
|
-
return [conceptIdForPath(type, file.relPath)];
|
|
111
|
-
}
|
|
112
106
|
function recognize(c, file) {
|
|
113
107
|
const type = classify(file.relPath);
|
|
114
108
|
if (type === null)
|
|
@@ -172,8 +166,15 @@ export const dotenvAdapter = {
|
|
|
172
166
|
version: "0.9.0",
|
|
173
167
|
extensions: [".env"],
|
|
174
168
|
recognize,
|
|
175
|
-
recognizePathCandidates,
|
|
176
169
|
validate,
|
|
170
|
+
/**
|
|
171
|
+
* Closed-form owner candidates (#857): `env`/`secret` are always authored
|
|
172
|
+
* directly under their own stash subdir (`classify` requires it — no
|
|
173
|
+
* off-canonical loose placement exists for this adapter), so there is no
|
|
174
|
+
* loose-fallback class to enumerate here, unlike `akm-adapter`.
|
|
175
|
+
* `assetPathCandidatesForName` expands `env`'s `.env`/`<name>.env` duality;
|
|
176
|
+
* the sensitive-marker sibling spellings are then layered on each.
|
|
177
|
+
*/
|
|
177
178
|
readCandidates(c, conceptId) {
|
|
178
179
|
const posix = toPosix(conceptId);
|
|
179
180
|
const slash = posix.indexOf("/");
|
|
@@ -184,10 +185,11 @@ export const dotenvAdapter = {
|
|
|
184
185
|
const type = typeForStashDir(head);
|
|
185
186
|
if ((type !== "env" && type !== "secret") || rest.length === 0)
|
|
186
187
|
return [];
|
|
187
|
-
const
|
|
188
|
-
|
|
188
|
+
const primaries = assetPathCandidatesForName(type, path.join(c.root, head), rest);
|
|
189
|
+
const expanded = primaries.flatMap((primary) => type === "env"
|
|
189
190
|
? [primary, primary.replace(/\.env$/i, ".sensitive")]
|
|
190
|
-
: [primary, `${primary}.sensitive`, `${primary}.lock`])
|
|
191
|
+
: [primary, `${primary}.sensitive`, `${primary}.lock`]);
|
|
192
|
+
return expanded.map((candidatePath) => ({ path: candidatePath, conceptId: posix }));
|
|
191
193
|
},
|
|
192
194
|
/**
|
|
193
195
|
* env places to `env/<name>.env`, secret to `secrets/<name>` (identity join,
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { directoryMatcher, extensionMatcher, parentDirHintMatcher, smartMdMatcher
|
|
4
|
+
import { directoryMatcher, extensionMatcher, parentDirHintMatcher, smartMdMatcher } from "../../indexer/walk/matchers.js";
|
|
5
5
|
/**
|
|
6
6
|
* The four builtin matchers, in registration order. The array index IS the
|
|
7
7
|
* registration index `runMatchers` uses for tie-breaking. (The `wiki` matcher
|
|
@@ -46,22 +46,3 @@ export function recognizeMatch(file) {
|
|
|
46
46
|
}
|
|
47
47
|
return winningMatch(hits);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Every AKM matcher winner possible from path fields alone. The three
|
|
51
|
-
* path-only matchers run exactly as production does; each possible result of
|
|
52
|
-
* the shared smart-Markdown fact table is then arbitrated at its real index.
|
|
53
|
-
*/
|
|
54
|
-
export function recognizePathCandidateMatches(file) {
|
|
55
|
-
const fileContext = file;
|
|
56
|
-
const pathHits = [extensionMatcher, directoryMatcher, parentDirHintMatcher].flatMap((matcher, index) => {
|
|
57
|
-
const result = matcher(fileContext);
|
|
58
|
-
return result ? [{ result, index }] : [];
|
|
59
|
-
});
|
|
60
|
-
const smartCandidates = smartMdPathCandidates(file);
|
|
61
|
-
const variants = smartCandidates.length > 0 ? smartCandidates : [undefined];
|
|
62
|
-
const winners = variants.flatMap((smart) => {
|
|
63
|
-
const winner = winningMatch(smart ? [...pathHits, { result: smart, index: 3 }] : [...pathHits]);
|
|
64
|
-
return winner ? [winner] : [];
|
|
65
|
-
});
|
|
66
|
-
return [...new Map(winners.map((winner) => [`${winner.type}\0${winner.renderer}`, winner])).values()];
|
|
67
|
-
}
|
|
@@ -151,10 +151,10 @@ const BUILTIN_PLACEMENT_SPECS = {
|
|
|
151
151
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".yml",
|
|
152
152
|
toCanonicalName: (typeRoot, filePath) => {
|
|
153
153
|
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
154
|
-
return rel.endsWith(".yml") ? rel.slice(0, -4) : rel;
|
|
154
|
+
return rel.toLowerCase().endsWith(".yml") ? rel.slice(0, -4) : rel;
|
|
155
155
|
},
|
|
156
156
|
toAssetPath: (typeRoot, name) => {
|
|
157
|
-
const withExt = name.endsWith(".yml") ? name : `${name}.yml`;
|
|
157
|
+
const withExt = name.toLowerCase().endsWith(".yml") ? name : `${name}.yml`;
|
|
158
158
|
return path.join(typeRoot, withExt);
|
|
159
159
|
},
|
|
160
160
|
},
|
|
@@ -241,3 +241,22 @@ export function assetPathForName(assetType, typeRoot, name) {
|
|
|
241
241
|
throw new Error(`Unknown asset type: "${assetType}"`);
|
|
242
242
|
return spec.toAssetPath(typeRoot, name);
|
|
243
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Every physical path spelling that could own `name`, closed-form (#857).
|
|
246
|
+
* `toAssetPath` is a function, so it can only pick ONE spelling — but `env`'s
|
|
247
|
+
* "default" alias is genuinely dual-owned: both `<dir>/.env` and
|
|
248
|
+
* `<dir>/default.env` derive the same canonical name (`toCanonicalName`
|
|
249
|
+
* above), so a physical-owner lookup must consider both without reading
|
|
250
|
+
* either file. Every other placement type has exactly one inverse spelling.
|
|
251
|
+
*/
|
|
252
|
+
export function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
253
|
+
const primary = assetPathForName(assetType, typeRoot, name);
|
|
254
|
+
if (assetType !== "env")
|
|
255
|
+
return [primary];
|
|
256
|
+
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|
|
257
|
+
if (base === undefined)
|
|
258
|
+
return [primary];
|
|
259
|
+
const dotForm = path.join(typeRoot, base, ".env");
|
|
260
|
+
const namedForm = path.join(typeRoot, base, "default.env");
|
|
261
|
+
return [...new Set([primary, dotForm, namedForm])];
|
|
262
|
+
}
|
package/dist/core/common.js
CHANGED
|
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { ConfigError } from "./errors.js";
|
|
8
|
-
import { getConfigPath, getDefaultStashDir } from "./paths.js";
|
|
8
|
+
import { getConfigPath, getDefaultStashDir, getRegistryCacheDir, getRegistryIndexCacheDir } from "./paths.js";
|
|
9
9
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
// Moved to the platform leaf so paths.ts can use it without a common↔paths
|
|
11
11
|
// cycle (chunk-8 WI-8.6, DoD 11); re-exported here for the existing surface.
|
|
@@ -382,6 +382,26 @@ export function isContainedRelativePath(value) {
|
|
|
382
382
|
export function isWithin(candidate, root) {
|
|
383
383
|
return isContainedResolvedPath(safeRealpath(candidate), safeRealpath(root));
|
|
384
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* True when `filePath` sits inside akm's OWN resolved registry-cache
|
|
387
|
+
* directories (`<cache>/registry`, `<cache>/registry-index` —
|
|
388
|
+
* {@link getRegistryCacheDir}/{@link getRegistryIndexCacheDir}), the
|
|
389
|
+
* read-only installed-source/registry-index copies that lint (and `--fix`)
|
|
390
|
+
* must never touch.
|
|
391
|
+
*
|
|
392
|
+
* This is the single source of truth for that exclusion — it replaces what
|
|
393
|
+
* used to be three independent unanchored substring checks
|
|
394
|
+
* (`posixPath.includes("/.cache/") || posixPath.includes("/registry/")`).
|
|
395
|
+
* That check matched ANY path merely containing the literal text `.cache` or
|
|
396
|
+
* `registry` anywhere in it — a normal XDG `~/.cache/...` user bundle, or a
|
|
397
|
+
* CI workspace checked out under a `.cache`-named directory, tripped it and
|
|
398
|
+
* silently got zero lint findings. Using {@link isWithin} (realpath +
|
|
399
|
+
* containment, not a string search) fixes that while still excluding the
|
|
400
|
+
* real cache content the check was meant to skip.
|
|
401
|
+
*/
|
|
402
|
+
export function isAkmRegistryCachePath(filePath) {
|
|
403
|
+
return isWithin(filePath, getRegistryCacheDir()) || isWithin(filePath, getRegistryIndexCacheDir());
|
|
404
|
+
}
|
|
385
405
|
/**
|
|
386
406
|
* {@link isWithin} for callers that must not block the event loop (e.g. the
|
|
387
407
|
* workflow exec dispatch path, which runs once per fan-out unit). Same
|