@erclx/aitk 3.7.0 → 3.9.0

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.
@@ -121,6 +121,13 @@ export interface SyncAdapter {
121
121
  collectRetired?(target: string): RetiredSurface[]
122
122
  /** Dropped from the walk, so neither matching nor orphaned. */
123
123
  isExcluded?(file: InstalledFile): boolean
124
+ /**
125
+ * Top-level folder under `installedRoot` that is project-authored by
126
+ * location rather than by the name inference `locateSource` runs.
127
+ * Checked before `locateSource`, so a file here is orphaned even when its
128
+ * name also matches a toolkit source, and never enters the stamp.
129
+ */
130
+ readonly projectSubdir?: string
124
131
  /** Defaults to applying. */
125
132
  readonly nonInteractive?: NonInteractivePolicy
126
133
  /** Runs on a completed sync, including one with no changes. */
@@ -168,6 +175,11 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
168
175
  if (adapter.isExcluded?.(file) === true) continue
169
176
  walked.add(toStampKey(file.rel))
170
177
 
178
+ if (isProjectAuthored(adapter, file)) {
179
+ entries.push({ state: 'orphaned', rel: file.rel })
180
+ continue
181
+ }
182
+
171
183
  const source = adapter.locateSource(file)
172
184
 
173
185
  if (source === undefined || !existsSync(source)) {
@@ -439,6 +451,16 @@ function strandedByRelocation(
439
451
  return entries
440
452
  }
441
453
 
454
+ /**
455
+ * `Bun.Glob` reports `relToRoot` with `/` separators regardless of platform,
456
+ * so the declared subfolder is compared against the walk's first segment
457
+ * rather than through a path-aware join.
458
+ */
459
+ function isProjectAuthored(adapter: SyncAdapter, file: InstalledFile): boolean {
460
+ if (adapter.projectSubdir === undefined) return false
461
+ return file.relToRoot.split('/')[0] === adapter.projectSubdir
462
+ }
463
+
442
464
  function isInside(target: string, path: string): boolean {
443
465
  const rel = relative(target, path)
444
466
  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel)
@@ -457,7 +479,8 @@ function hasUnattributedDrift(plan: SyncPlan): boolean {
457
479
  /**
458
480
  * Records what the toolkit placed, after the copies land, so a partial apply
459
481
  * that throws leaves the previous stamp rather than a claim the target does not
460
- * meet. Files with no source are project-authored and stay out.
482
+ * meet. A file with no source, or one orphaned by location, is
483
+ * project-authored and stays out.
461
484
  *
462
485
  * Reads the installed tree rather than the caller's file list, so a partial
463
486
  * install still stamps the domain's whole installed set.
@@ -474,6 +497,7 @@ export async function recordStamp(
474
497
 
475
498
  for (const file of listInstalled(adapter.installedRoot(target), target)) {
476
499
  if (adapter.isExcluded?.(file) === true) continue
500
+ if (isProjectAuthored(adapter, file)) continue
477
501
 
478
502
  const source = adapter.locateSource(file)
479
503
  if (source === undefined || !existsSync(source)) continue
@@ -21,6 +21,7 @@ Does not govern:
21
21
  - Punctuation, formatting, and word choice in a skill body: `markdown.md`
22
22
  - The transform from a branch name to a slug a skill carries in a filename: `slug.md`
23
23
  - The domain conventions a skill cites, each of which belongs to the standard that owns it
24
+ - Whether a new skill earns its place: the three-question test in the `create-skill` skill and the clause in `570-skill.md`
24
25
 
25
26
  ## Changing a skill
26
27