@orkestrel/scaffold 0.0.55 → 0.0.56

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.
@@ -4,6 +4,7 @@ import { Dependency } from '@orkestrel/scaffold';
4
4
  import { EmitterErrorHandler } from '@orkestrel/emitter';
5
5
  import { EmitterHooks } from '@orkestrel/emitter';
6
6
  import { EmitterInterface } from '@orkestrel/emitter';
7
+ import { Group } from '@orkestrel/scaffold';
7
8
  import { Guard } from '@orkestrel/contract';
8
9
  import { HostFile } from '@orkestrel/scaffold';
9
10
  import { ManifestRegionSet } from '@orkestrel/scaffold';
@@ -130,8 +131,11 @@ export declare const DRIVE_PATTERN: RegExp;
130
131
  * went missing, names an undeclared path, or leaves a host-owned path absent
131
132
  * answers `undefined`. Deferred paths are presence-only and retain the installed
132
133
  * floor bytes that their catalog or mirror surface owns; repair never writes
133
- * those floor bytes. One `Host` can therefore carry live host bytes beside floor
134
- * bytes without mixing baselines within a surface.
134
+ * those floor bytes. A canon path is read the same way for a different reason:
135
+ * the canon is staged for reading rather than for a target, so every canon
136
+ * destination keeps its floor bytes here, claimed or not, and a fill that carries
137
+ * no row for one is complete rather than spoiled. One `Host` can therefore carry
138
+ * live host bytes beside floor bytes without mixing baselines within a surface.
135
139
  *
136
140
  * The emitted entries keep the release's own order and its storage and
137
141
  * executable declarations, and carry digests recomputed over the bytes the fill
@@ -143,8 +147,12 @@ export declare const DRIVE_PATTERN: RegExp;
143
147
  * ```ts
144
148
  * import { filesToHost } from '@orkestrel/scaffold/server'
145
149
  *
146
- * filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
147
- * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
150
+ * // A floor declaring the host-owned `scripts/codex.sh` path and the canon
151
+ * // `AGENTS.md` destination. The script's live bytes are taken; the canon
152
+ * // destination keeps the floor's.
153
+ * filesToHost([{ path: 'scripts/codex.sh', lookup: 'found', hex: '23212f62696e2f73680a' }], floor)
154
+ * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' },
155
+ * // bytes: { 'scripts/codex.sh': '23212f62696e2f73680a', 'AGENTS.md': floor.bytes['AGENTS.md'] } }
148
156
  * ```
149
157
  */
150
158
  export declare function filesToHost(files: readonly HostFile[], floor: Host): Host | undefined;
@@ -624,6 +632,39 @@ export declare function isVacant(target: string): boolean;
624
632
  */
625
633
  export declare const isWorktree: Guard<Worktree>;
626
634
 
635
+ /**
636
+ * Lists the canon paths a target holds, filtered to a plan's groups.
637
+ *
638
+ * @param target - The target directory to inspect.
639
+ * @param groups - The artifact groups the plan covers; a held path whose group
640
+ * is outside them is not listed.
641
+ * @returns Every held canon path as a `/`-separated target-relative path, a
642
+ * directory member expanded to the files beneath it, and `[]` when the target
643
+ * holds none or cannot be resolved.
644
+ * @throws `ScaffoldError('TARGET', …)` when a held canon directory cannot be
645
+ * inventoried; {@link listFiles} states each refusal.
646
+ *
647
+ * @remarks
648
+ * A release stages the canon for reading rather than for a target, so a copy
649
+ * sitting in one is an artifact of a release that vendored it, and reading it
650
+ * here is what lets the deletion verb take it.
651
+ *
652
+ * A directory member is read by file, which is what pairs the paths a plan
653
+ * claims inside the canon with their artifacts and leaves only the rest foreign,
654
+ * with no case for a planned file. The plan's own selection gates the reading,
655
+ * the same rule that decides a vendored path's group, so a scoped audit reports
656
+ * nothing outside its groups. A member that cannot resolve inside the target is
657
+ * not held by it.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * import { listCanonPaths } from '@orkestrel/scaffold/server'
662
+ *
663
+ * listCanonPaths('vacant', ['orchestration']) // []
664
+ * ```
665
+ */
666
+ export declare function listCanonPaths(target: string, groups: readonly Group[]): readonly string[];
667
+
627
668
  /**
628
669
  * List a directory's descendant directories as sorted root-relative paths.
629
670
  *
@@ -988,7 +1029,8 @@ export declare class Materializer implements MaterializerInterface {
988
1029
  *
989
1030
  * @param plan - The compiled plan to compare.
990
1031
  * @param target - The directory to inspect.
991
- * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
1032
+ * @returns One finding per hydrated planned path, plus the foreign files
1033
+ * beneath owned host roots and inside the instruction canon.
992
1034
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
993
1035
  * exact shape, `TARGET` when the host or target cannot be read within its
994
1036
  * bounds, and `DESTROYED` after teardown.
@@ -996,8 +1038,16 @@ export declare class Materializer implements MaterializerInterface {
996
1038
  * @remarks
997
1039
  * Host directories expand before the target is read, so this method and
998
1040
  * {@link repair} compare the same paths with the same ownership. Foreign
999
- * candidates are files beneath those expanded roots only; a root file never
1000
- * becomes a deletion candidate merely because its group is selected.
1041
+ * candidates are files beneath those expanded roots and files the target holds
1042
+ * at a `CANON_PATHS` member, each in a group the plan selects; a file outside
1043
+ * both populations never becomes a deletion candidate merely because its group
1044
+ * is selected.
1045
+ *
1046
+ * The canon is staged for reading rather than for a target, so a copy of one of
1047
+ * its paths sitting in a target is a superseded artifact and reports foreign. A
1048
+ * path the plan claims inside the canon — the root instruction pointers and the
1049
+ * catalog agent file — pairs with its artifact and is compared like any other
1050
+ * planned path.
1001
1051
  */
1002
1052
  audit(plan: Plan, target: string): Audit;
1003
1053
  /**
@@ -1124,9 +1174,20 @@ export declare class Materializer implements MaterializerInterface {
1124
1174
  * recovery mechanism, so a path it cannot restore is not one this verb takes.
1125
1175
  * A tree carrying uncommitted work is refused whole for the same reason.
1126
1176
  *
1177
+ * One candidate list carries both foreign populations {@link audit} reports, so
1178
+ * a superseded instruction copy the target holds inside the canon is deleted in
1179
+ * the same transaction as a stray beneath an owned root. Membership decides it,
1180
+ * never byte identity: a copy a release behind no longer matches the bytes the
1181
+ * canon now stages, and matching bytes is exactly how such a copy would be
1182
+ * spared. An untracked leftover is left where it sits and stays a finding, which
1183
+ * is the seam a maintainer keeps a git-ignored file in.
1184
+ *
1127
1185
  * The whole call refuses when the preview disagrees with the re-derivation on
1128
1186
  * any foreign finding, including one the deletion itself would skip, because a
1129
1187
  * preview stale anywhere is stale evidence.
1188
+ *
1189
+ * Every directory the deletions emptied is taken after the commit, so a swept
1190
+ * target does not keep the shape of the set it no longer holds.
1130
1191
  */
1131
1192
  remove(plan: Plan, audit: Audit, worktree: Worktree, target: string): MaterializeResult;
1132
1193
  /**
@@ -1384,6 +1445,38 @@ export declare class Materializer implements MaterializerInterface {
1384
1445
  */
1385
1446
  export declare function pathToStorage(path: string): string;
1386
1447
 
1448
+ /**
1449
+ * Removes every directory one set of deletions emptied.
1450
+ *
1451
+ * @param target - The directory the deleted paths are relative to.
1452
+ * @param removed - The `/`-separated target-relative paths the deletion took.
1453
+ * @returns Every removed directory as a target-relative path, in the order they
1454
+ * were taken, and `[]` when the target is absent or nothing it holds was
1455
+ * emptied.
1456
+ *
1457
+ * @remarks
1458
+ * A deletion that takes the last file out of a directory leaves the directory
1459
+ * standing, and git records no directory, so a swept target keeps the shape of a
1460
+ * set it no longer holds while every reading of it reports clean. Only an
1461
+ * ancestor of a path this deletion took is a candidate, so nothing the deletion
1462
+ * did not reach is inspected, and the target itself is never a candidate.
1463
+ *
1464
+ * Candidates are taken deepest first, which is what lets a whole chain go: the
1465
+ * directory holding nothing but the emptied directory is empty in turn by the
1466
+ * time it is read. Each one is resolved through the containment law and left
1467
+ * standing when it escapes the target, is not a physical directory, still holds
1468
+ * an entry, or refuses removal, and a directory left standing is absent from the
1469
+ * answer.
1470
+ *
1471
+ * @example
1472
+ * ```ts
1473
+ * import { pruneEmptiedDirectories } from '@orkestrel/scaffold/server'
1474
+ *
1475
+ * pruneEmptiedDirectories('vacant', ['notes/entry.md']) // []
1476
+ * ```
1477
+ */
1478
+ export declare function pruneEmptiedDirectories(target: string, removed: readonly string[]): readonly string[];
1479
+
1387
1480
  /**
1388
1481
  * Capture one directory's physical identity.
1389
1482
  *
@@ -1779,9 +1872,15 @@ export declare class Materializer implements MaterializerInterface {
1779
1872
  * once. A directory is the same case — declaring an absent directory as an empty
1780
1873
  * root would create an empty directory in every generated workspace.
1781
1874
  *
1782
- * The vendoring deny-list applies to what the walk discovers beneath a vendored
1875
+ * The walk covers `HOST_PATHS` and `CANON_PATHS` together, because a release
1876
+ * ships both: a target receives the first set, and reads the second one out of
1877
+ * the installed package. Everything downstream — the missing-path refusal, the
1878
+ * storage collision guard, the sort, the digests, and the root inventory — reads
1879
+ * the union, so a canon path is staged under exactly the law a vendored one is.
1880
+ *
1881
+ * The vendoring deny-list applies to what the walk discovers beneath a staged
1783
1882
  * directory, where a maintainer's local credential can legitimately sit, and
1784
- * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1883
+ * such a path is skipped. A path either list names itself is curated data
1785
1884
  * rather than discovery, so it is staged or the stage is refused.
1786
1885
  *
1787
1886
  * @example
@@ -4,6 +4,7 @@ import { Dependency } from '@orkestrel/scaffold';
4
4
  import { EmitterErrorHandler } from '@orkestrel/emitter';
5
5
  import { EmitterHooks } from '@orkestrel/emitter';
6
6
  import { EmitterInterface } from '@orkestrel/emitter';
7
+ import { Group } from '@orkestrel/scaffold';
7
8
  import { Guard } from '@orkestrel/contract';
8
9
  import { HostFile } from '@orkestrel/scaffold';
9
10
  import { ManifestRegionSet } from '@orkestrel/scaffold';
@@ -130,8 +131,11 @@ export declare const DRIVE_PATTERN: RegExp;
130
131
  * went missing, names an undeclared path, or leaves a host-owned path absent
131
132
  * answers `undefined`. Deferred paths are presence-only and retain the installed
132
133
  * floor bytes that their catalog or mirror surface owns; repair never writes
133
- * those floor bytes. One `Host` can therefore carry live host bytes beside floor
134
- * bytes without mixing baselines within a surface.
134
+ * those floor bytes. A canon path is read the same way for a different reason:
135
+ * the canon is staged for reading rather than for a target, so every canon
136
+ * destination keeps its floor bytes here, claimed or not, and a fill that carries
137
+ * no row for one is complete rather than spoiled. One `Host` can therefore carry
138
+ * live host bytes beside floor bytes without mixing baselines within a surface.
135
139
  *
136
140
  * The emitted entries keep the release's own order and its storage and
137
141
  * executable declarations, and carry digests recomputed over the bytes the fill
@@ -143,8 +147,12 @@ export declare const DRIVE_PATTERN: RegExp;
143
147
  * ```ts
144
148
  * import { filesToHost } from '@orkestrel/scaffold/server'
145
149
  *
146
- * filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
147
- * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
150
+ * // A floor declaring the host-owned `scripts/codex.sh` path and the canon
151
+ * // `AGENTS.md` destination. The script's live bytes are taken; the canon
152
+ * // destination keeps the floor's.
153
+ * filesToHost([{ path: 'scripts/codex.sh', lookup: 'found', hex: '23212f62696e2f73680a' }], floor)
154
+ * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' },
155
+ * // bytes: { 'scripts/codex.sh': '23212f62696e2f73680a', 'AGENTS.md': floor.bytes['AGENTS.md'] } }
148
156
  * ```
149
157
  */
150
158
  export declare function filesToHost(files: readonly HostFile[], floor: Host): Host | undefined;
@@ -624,6 +632,39 @@ export declare function isVacant(target: string): boolean;
624
632
  */
625
633
  export declare const isWorktree: Guard<Worktree>;
626
634
 
635
+ /**
636
+ * Lists the canon paths a target holds, filtered to a plan's groups.
637
+ *
638
+ * @param target - The target directory to inspect.
639
+ * @param groups - The artifact groups the plan covers; a held path whose group
640
+ * is outside them is not listed.
641
+ * @returns Every held canon path as a `/`-separated target-relative path, a
642
+ * directory member expanded to the files beneath it, and `[]` when the target
643
+ * holds none or cannot be resolved.
644
+ * @throws `ScaffoldError('TARGET', …)` when a held canon directory cannot be
645
+ * inventoried; {@link listFiles} states each refusal.
646
+ *
647
+ * @remarks
648
+ * A release stages the canon for reading rather than for a target, so a copy
649
+ * sitting in one is an artifact of a release that vendored it, and reading it
650
+ * here is what lets the deletion verb take it.
651
+ *
652
+ * A directory member is read by file, which is what pairs the paths a plan
653
+ * claims inside the canon with their artifacts and leaves only the rest foreign,
654
+ * with no case for a planned file. The plan's own selection gates the reading,
655
+ * the same rule that decides a vendored path's group, so a scoped audit reports
656
+ * nothing outside its groups. A member that cannot resolve inside the target is
657
+ * not held by it.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * import { listCanonPaths } from '@orkestrel/scaffold/server'
662
+ *
663
+ * listCanonPaths('vacant', ['orchestration']) // []
664
+ * ```
665
+ */
666
+ export declare function listCanonPaths(target: string, groups: readonly Group[]): readonly string[];
667
+
627
668
  /**
628
669
  * List a directory's descendant directories as sorted root-relative paths.
629
670
  *
@@ -988,7 +1029,8 @@ export declare class Materializer implements MaterializerInterface {
988
1029
  *
989
1030
  * @param plan - The compiled plan to compare.
990
1031
  * @param target - The directory to inspect.
991
- * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
1032
+ * @returns One finding per hydrated planned path, plus the foreign files
1033
+ * beneath owned host roots and inside the instruction canon.
992
1034
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
993
1035
  * exact shape, `TARGET` when the host or target cannot be read within its
994
1036
  * bounds, and `DESTROYED` after teardown.
@@ -996,8 +1038,16 @@ export declare class Materializer implements MaterializerInterface {
996
1038
  * @remarks
997
1039
  * Host directories expand before the target is read, so this method and
998
1040
  * {@link repair} compare the same paths with the same ownership. Foreign
999
- * candidates are files beneath those expanded roots only; a root file never
1000
- * becomes a deletion candidate merely because its group is selected.
1041
+ * candidates are files beneath those expanded roots and files the target holds
1042
+ * at a `CANON_PATHS` member, each in a group the plan selects; a file outside
1043
+ * both populations never becomes a deletion candidate merely because its group
1044
+ * is selected.
1045
+ *
1046
+ * The canon is staged for reading rather than for a target, so a copy of one of
1047
+ * its paths sitting in a target is a superseded artifact and reports foreign. A
1048
+ * path the plan claims inside the canon — the root instruction pointers and the
1049
+ * catalog agent file — pairs with its artifact and is compared like any other
1050
+ * planned path.
1001
1051
  */
1002
1052
  audit(plan: Plan, target: string): Audit;
1003
1053
  /**
@@ -1124,9 +1174,20 @@ export declare class Materializer implements MaterializerInterface {
1124
1174
  * recovery mechanism, so a path it cannot restore is not one this verb takes.
1125
1175
  * A tree carrying uncommitted work is refused whole for the same reason.
1126
1176
  *
1177
+ * One candidate list carries both foreign populations {@link audit} reports, so
1178
+ * a superseded instruction copy the target holds inside the canon is deleted in
1179
+ * the same transaction as a stray beneath an owned root. Membership decides it,
1180
+ * never byte identity: a copy a release behind no longer matches the bytes the
1181
+ * canon now stages, and matching bytes is exactly how such a copy would be
1182
+ * spared. An untracked leftover is left where it sits and stays a finding, which
1183
+ * is the seam a maintainer keeps a git-ignored file in.
1184
+ *
1127
1185
  * The whole call refuses when the preview disagrees with the re-derivation on
1128
1186
  * any foreign finding, including one the deletion itself would skip, because a
1129
1187
  * preview stale anywhere is stale evidence.
1188
+ *
1189
+ * Every directory the deletions emptied is taken after the commit, so a swept
1190
+ * target does not keep the shape of the set it no longer holds.
1130
1191
  */
1131
1192
  remove(plan: Plan, audit: Audit, worktree: Worktree, target: string): MaterializeResult;
1132
1193
  /**
@@ -1384,6 +1445,38 @@ export declare class Materializer implements MaterializerInterface {
1384
1445
  */
1385
1446
  export declare function pathToStorage(path: string): string;
1386
1447
 
1448
+ /**
1449
+ * Removes every directory one set of deletions emptied.
1450
+ *
1451
+ * @param target - The directory the deleted paths are relative to.
1452
+ * @param removed - The `/`-separated target-relative paths the deletion took.
1453
+ * @returns Every removed directory as a target-relative path, in the order they
1454
+ * were taken, and `[]` when the target is absent or nothing it holds was
1455
+ * emptied.
1456
+ *
1457
+ * @remarks
1458
+ * A deletion that takes the last file out of a directory leaves the directory
1459
+ * standing, and git records no directory, so a swept target keeps the shape of a
1460
+ * set it no longer holds while every reading of it reports clean. Only an
1461
+ * ancestor of a path this deletion took is a candidate, so nothing the deletion
1462
+ * did not reach is inspected, and the target itself is never a candidate.
1463
+ *
1464
+ * Candidates are taken deepest first, which is what lets a whole chain go: the
1465
+ * directory holding nothing but the emptied directory is empty in turn by the
1466
+ * time it is read. Each one is resolved through the containment law and left
1467
+ * standing when it escapes the target, is not a physical directory, still holds
1468
+ * an entry, or refuses removal, and a directory left standing is absent from the
1469
+ * answer.
1470
+ *
1471
+ * @example
1472
+ * ```ts
1473
+ * import { pruneEmptiedDirectories } from '@orkestrel/scaffold/server'
1474
+ *
1475
+ * pruneEmptiedDirectories('vacant', ['notes/entry.md']) // []
1476
+ * ```
1477
+ */
1478
+ export declare function pruneEmptiedDirectories(target: string, removed: readonly string[]): readonly string[];
1479
+
1387
1480
  /**
1388
1481
  * Capture one directory's physical identity.
1389
1482
  *
@@ -1779,9 +1872,15 @@ export declare class Materializer implements MaterializerInterface {
1779
1872
  * once. A directory is the same case — declaring an absent directory as an empty
1780
1873
  * root would create an empty directory in every generated workspace.
1781
1874
  *
1782
- * The vendoring deny-list applies to what the walk discovers beneath a vendored
1875
+ * The walk covers `HOST_PATHS` and `CANON_PATHS` together, because a release
1876
+ * ships both: a target receives the first set, and reads the second one out of
1877
+ * the installed package. Everything downstream — the missing-path refusal, the
1878
+ * storage collision guard, the sort, the digests, and the root inventory — reads
1879
+ * the union, so a canon path is staged under exactly the law a vendored one is.
1880
+ *
1881
+ * The vendoring deny-list applies to what the walk discovers beneath a staged
1783
1882
  * directory, where a maintainer's local credential can legitimately sit, and
1784
- * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1883
+ * such a path is skipped. A path either list names itself is curated data
1785
1884
  * rather than discovery, so it is staged or the stage is refused.
1786
1885
  *
1787
1886
  * @example
@@ -1,5 +1,5 @@
1
- import { andOf, arrayOf, attempt, boundsOf, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf, unionOf } from "@orkestrel/contract";
2
- import { CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_INVENTORY_PATH, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, ScaffoldError, WORKSPACE_OWNED_PATHS, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, contentToHex, extractRangeMajor, extractVersion, inferGroup, isAudit, isCatalogEntry, isCollection, isDeferredPath, isDependency, isDependencyName, isHex, isManifestScript, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, replaceManifestRanges, replaceManifestScripts } from "../core/index.js";
1
+ import { andOf, arrayOf, attempt, boundsOf, compareValues, holds, isArray, isBoolean, isError, isFunction, isInteger, isRecord, isString, parseJSON, parseJSONAs, parseStringField, recordOf, stringOf, unionOf } from "@orkestrel/contract";
2
+ import { CANON_PATHS, CATALOG_AGENT_PATH, CONTROL_CHARACTER_PATTERN, EXECUTABLE_PATHS, HOST_INVENTORY_PATH, HOST_PATHS, MAX_ARTIFACT_BYTES, MAX_COLLECTION_ITEMS, MAX_DEPENDENCY_NAME_LENGTH, MAX_MANIFEST_BYTES, MAX_PATH_LENGTH, MAX_RANGE_LENGTH, MAX_REGISTRY_BYTES, MAX_TOTAL_ARTIFACT_BYTES, MAX_TOTAL_REGISTRY_BYTES, ScaffoldError, WORKSPACE_OWNED_PATHS, bytesToHex, catalogToLayers, cloneValue, compareVersions, computeBytes, contentToHex, extractRangeMajor, extractVersion, inferGroup, isAudit, isCanonPath, isCatalogEntry, isCollection, isDeferredPath, isDependency, isDependencyName, isHex, isManifestScript, isMirror, isPath, isPlan, isSnapshot, matchesDriftReachability, matchesRange, nameToGuide, planToFindings, replaceManifestRanges, replaceManifestScripts } from "../core/index.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { chmodSync, closeSync, constants, copyFileSync, fstatSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, opendirSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
@@ -1210,6 +1210,97 @@ function listDirectories(root) {
1210
1210
  return directories.sort();
1211
1211
  }
1212
1212
  /**
1213
+ * Lists the canon paths a target holds, filtered to a plan's groups.
1214
+ *
1215
+ * @param target - The target directory to inspect.
1216
+ * @param groups - The artifact groups the plan covers; a held path whose group
1217
+ * is outside them is not listed.
1218
+ * @returns Every held canon path as a `/`-separated target-relative path, a
1219
+ * directory member expanded to the files beneath it, and `[]` when the target
1220
+ * holds none or cannot be resolved.
1221
+ * @throws `ScaffoldError('TARGET', …)` when a held canon directory cannot be
1222
+ * inventoried; {@link listFiles} states each refusal.
1223
+ *
1224
+ * @remarks
1225
+ * A release stages the canon for reading rather than for a target, so a copy
1226
+ * sitting in one is an artifact of a release that vendored it, and reading it
1227
+ * here is what lets the deletion verb take it.
1228
+ *
1229
+ * A directory member is read by file, which is what pairs the paths a plan
1230
+ * claims inside the canon with their artifacts and leaves only the rest foreign,
1231
+ * with no case for a planned file. The plan's own selection gates the reading,
1232
+ * the same rule that decides a vendored path's group, so a scoped audit reports
1233
+ * nothing outside its groups. A member that cannot resolve inside the target is
1234
+ * not held by it.
1235
+ *
1236
+ * @example
1237
+ * ```ts
1238
+ * import { listCanonPaths } from '@orkestrel/scaffold/server'
1239
+ *
1240
+ * listCanonPaths('vacant', ['orchestration']) // []
1241
+ * ```
1242
+ */
1243
+ function listCanonPaths(target, groups) {
1244
+ const held = [];
1245
+ for (const member of CANON_PATHS) {
1246
+ const full = resolveContainedPath(target, member);
1247
+ if (full === void 0) continue;
1248
+ if (isPhysicalFile(full)) held.push(member);
1249
+ else if (isPhysicalDirectory(full)) for (const name of listFiles(full)) held.push(`${member}/${name}`);
1250
+ }
1251
+ return held.filter((path) => groups.includes(inferGroup(path)));
1252
+ }
1253
+ /**
1254
+ * Removes every directory one set of deletions emptied.
1255
+ *
1256
+ * @param target - The directory the deleted paths are relative to.
1257
+ * @param removed - The `/`-separated target-relative paths the deletion took.
1258
+ * @returns Every removed directory as a target-relative path, in the order they
1259
+ * were taken, and `[]` when the target is absent or nothing it holds was
1260
+ * emptied.
1261
+ *
1262
+ * @remarks
1263
+ * A deletion that takes the last file out of a directory leaves the directory
1264
+ * standing, and git records no directory, so a swept target keeps the shape of a
1265
+ * set it no longer holds while every reading of it reports clean. Only an
1266
+ * ancestor of a path this deletion took is a candidate, so nothing the deletion
1267
+ * did not reach is inspected, and the target itself is never a candidate.
1268
+ *
1269
+ * Candidates are taken deepest first, which is what lets a whole chain go: the
1270
+ * directory holding nothing but the emptied directory is empty in turn by the
1271
+ * time it is read. Each one is resolved through the containment law and left
1272
+ * standing when it escapes the target, is not a physical directory, still holds
1273
+ * an entry, or refuses removal, and a directory left standing is absent from the
1274
+ * answer.
1275
+ *
1276
+ * @example
1277
+ * ```ts
1278
+ * import { pruneEmptiedDirectories } from '@orkestrel/scaffold/server'
1279
+ *
1280
+ * pruneEmptiedDirectories('vacant', ['notes/entry.md']) // []
1281
+ * ```
1282
+ */
1283
+ function pruneEmptiedDirectories(target, removed) {
1284
+ const candidates = /* @__PURE__ */ new Set();
1285
+ for (const path of removed) {
1286
+ let parent = dirname(path);
1287
+ while (parent !== "." && parent !== dirname(parent) && !candidates.has(parent)) {
1288
+ candidates.add(parent);
1289
+ parent = dirname(parent);
1290
+ }
1291
+ }
1292
+ const ordered = [...candidates].sort((left, right) => compareValues(right.split("/").length, left.split("/").length) || compareValues(left, right));
1293
+ const pruned = [];
1294
+ for (const candidate of ordered) {
1295
+ const directory = resolveContainedPath(target, candidate);
1296
+ if (directory === void 0 || !isPhysicalDirectory(directory)) continue;
1297
+ const entries = attempt(() => readdirSync(directory));
1298
+ if (!entries.success || entries.value.length > 0) continue;
1299
+ if (attempt(() => rmdirSync(directory)).success) pruned.push(candidate);
1300
+ }
1301
+ return pruned;
1302
+ }
1303
+ /**
1213
1304
  * Read one contained file as its exact bytes in lowercase hexadecimal.
1214
1305
  *
1215
1306
  * @param root - The containing host directory.
@@ -1518,8 +1609,11 @@ function readManifestEntry(destination, source) {
1518
1609
  * went missing, names an undeclared path, or leaves a host-owned path absent
1519
1610
  * answers `undefined`. Deferred paths are presence-only and retain the installed
1520
1611
  * floor bytes that their catalog or mirror surface owns; repair never writes
1521
- * those floor bytes. One `Host` can therefore carry live host bytes beside floor
1522
- * bytes without mixing baselines within a surface.
1612
+ * those floor bytes. A canon path is read the same way for a different reason:
1613
+ * the canon is staged for reading rather than for a target, so every canon
1614
+ * destination keeps its floor bytes here, claimed or not, and a fill that carries
1615
+ * no row for one is complete rather than spoiled. One `Host` can therefore carry
1616
+ * live host bytes beside floor bytes without mixing baselines within a surface.
1523
1617
  *
1524
1618
  * The emitted entries keep the release's own order and its storage and
1525
1619
  * executable declarations, and carry digests recomputed over the bytes the fill
@@ -1531,8 +1625,12 @@ function readManifestEntry(destination, source) {
1531
1625
  * ```ts
1532
1626
  * import { filesToHost } from '@orkestrel/scaffold/server'
1533
1627
  *
1534
- * filesToHost([{ path: 'AGENTS.md', lookup: 'found', hex: '23204167656e74730a' }], floor)
1535
- * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' }, bytes: { 'AGENTS.md': '…' } }
1628
+ * // A floor declaring the host-owned `scripts/codex.sh` path and the canon
1629
+ * // `AGENTS.md` destination. The script's live bytes are taken; the canon
1630
+ * // destination keeps the floor's.
1631
+ * filesToHost([{ path: 'scripts/codex.sh', lookup: 'found', hex: '23212f62696e2f73680a' }], floor)
1632
+ * // { manifest: { entries: [ … ], roots: [ … ], digest: '…' },
1633
+ * // bytes: { 'scripts/codex.sh': '23212f62696e2f73680a', 'AGENTS.md': floor.bytes['AGENTS.md'] } }
1536
1634
  * ```
1537
1635
  */
1538
1636
  function filesToHost(files, floor) {
@@ -1540,12 +1638,12 @@ function filesToHost(files, floor) {
1540
1638
  const held = /* @__PURE__ */ new Map();
1541
1639
  for (const file of files) {
1542
1640
  if (file.lookup !== "found" || !declared.has(file.path)) return void 0;
1543
- if (!isDeferredPath(file.path)) held.set(file.path, file.hex);
1641
+ if (!isDeferredPath(file.path) && !isCanonPath(file.path)) held.set(file.path, file.hex);
1544
1642
  }
1545
1643
  const entries = [];
1546
1644
  const bytes = {};
1547
1645
  for (const entry of floor.manifest.entries) {
1548
- const hex = isDeferredPath(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
1646
+ const hex = isDeferredPath(entry.destination) || isCanonPath(entry.destination) ? floor.bytes[entry.destination] : held.get(entry.destination);
1549
1647
  if (hex === void 0) return void 0;
1550
1648
  entries.push({
1551
1649
  storage: entry.storage,
@@ -1676,9 +1774,15 @@ function stageBytes(host, root, destinations) {
1676
1774
  * once. A directory is the same case — declaring an absent directory as an empty
1677
1775
  * root would create an empty directory in every generated workspace.
1678
1776
  *
1679
- * The vendoring deny-list applies to what the walk discovers beneath a vendored
1777
+ * The walk covers `HOST_PATHS` and `CANON_PATHS` together, because a release
1778
+ * ships both: a target receives the first set, and reads the second one out of
1779
+ * the installed package. Everything downstream — the missing-path refusal, the
1780
+ * storage collision guard, the sort, the digests, and the root inventory — reads
1781
+ * the union, so a canon path is staged under exactly the law a vendored one is.
1782
+ *
1783
+ * The vendoring deny-list applies to what the walk discovers beneath a staged
1680
1784
  * directory, where a maintainer's local credential can legitimately sit, and
1681
- * such a path is skipped. A path `HOST_PATHS` names itself is curated data
1785
+ * such a path is skipped. A path either list names itself is curated data
1682
1786
  * rather than discovery, so it is staged or the stage is refused.
1683
1787
  *
1684
1788
  * @example
@@ -1697,7 +1801,7 @@ function stageHost(checkout, host) {
1697
1801
  const vendored = [];
1698
1802
  const roots = [];
1699
1803
  const missing = [];
1700
- for (const path of HOST_PATHS) {
1804
+ for (const path of [...HOST_PATHS, ...CANON_PATHS]) {
1701
1805
  const full = resolveContainedPath(source, path);
1702
1806
  if (full === void 0) throw new ScaffoldError("INVALID", `Vendored path leaves its checkout at ${path}`, {
1703
1807
  checkout: source,
@@ -2651,7 +2755,8 @@ var Materializer = class Materializer {
2651
2755
  *
2652
2756
  * @param plan - The compiled plan to compare.
2653
2757
  * @param target - The directory to inspect.
2654
- * @returns One finding per hydrated planned path, plus foreign files beneath owned host roots.
2758
+ * @returns One finding per hydrated planned path, plus the foreign files
2759
+ * beneath owned host roots and inside the instruction canon.
2655
2760
  * @throws {@link ScaffoldError} coded `INVALID` when an argument is not the
2656
2761
  * exact shape, `TARGET` when the host or target cannot be read within its
2657
2762
  * bounds, and `DESTROYED` after teardown.
@@ -2659,8 +2764,16 @@ var Materializer = class Materializer {
2659
2764
  * @remarks
2660
2765
  * Host directories expand before the target is read, so this method and
2661
2766
  * {@link repair} compare the same paths with the same ownership. Foreign
2662
- * candidates are files beneath those expanded roots only; a root file never
2663
- * becomes a deletion candidate merely because its group is selected.
2767
+ * candidates are files beneath those expanded roots and files the target holds
2768
+ * at a `CANON_PATHS` member, each in a group the plan selects; a file outside
2769
+ * both populations never becomes a deletion candidate merely because its group
2770
+ * is selected.
2771
+ *
2772
+ * The canon is staged for reading rather than for a target, so a copy of one of
2773
+ * its paths sitting in a target is a superseded artifact and reports foreign. A
2774
+ * path the plan claims inside the canon — the root instruction pointers and the
2775
+ * catalog agent file — pairs with its artifact and is compared like any other
2776
+ * planned path.
2664
2777
  */
2665
2778
  audit(plan, target) {
2666
2779
  this.#assertAlive();
@@ -2859,9 +2972,20 @@ var Materializer = class Materializer {
2859
2972
  * recovery mechanism, so a path it cannot restore is not one this verb takes.
2860
2973
  * A tree carrying uncommitted work is refused whole for the same reason.
2861
2974
  *
2975
+ * One candidate list carries both foreign populations {@link audit} reports, so
2976
+ * a superseded instruction copy the target holds inside the canon is deleted in
2977
+ * the same transaction as a stray beneath an owned root. Membership decides it,
2978
+ * never byte identity: a copy a release behind no longer matches the bytes the
2979
+ * canon now stages, and matching bytes is exactly how such a copy would be
2980
+ * spared. An untracked leftover is left where it sits and stays a finding, which
2981
+ * is the seam a maintainer keeps a git-ignored file in.
2982
+ *
2862
2983
  * The whole call refuses when the preview disagrees with the re-derivation on
2863
2984
  * any foreign finding, including one the deletion itself would skip, because a
2864
2985
  * preview stale anywhere is stale evidence.
2986
+ *
2987
+ * Every directory the deletions emptied is taken after the commit, so a swept
2988
+ * target does not keep the shape of the set it no longer holds.
2865
2989
  */
2866
2990
  remove(plan, audit, worktree, target) {
2867
2991
  this.#assertAlive();
@@ -2979,6 +3103,7 @@ var Materializer = class Materializer {
2979
3103
  });
2980
3104
  for (const name of listFiles(directory)) paths.add(`${root}/${name}`);
2981
3105
  }
3106
+ for (const path of listCanonPaths(target, plan.groups)) paths.add(path);
2982
3107
  return {
2983
3108
  findings: planToFindings(hydrated, readSnapshot(target, [...paths])),
2984
3109
  questions: []
@@ -3284,6 +3409,7 @@ var Materializer = class Materializer {
3284
3409
  });
3285
3410
  const removed = this.#close(transaction, staged, target);
3286
3411
  for (const path of removed) this.#emitter.emit("remove", path);
3412
+ pruneEmptiedDirectories(target, removed);
3287
3413
  return this.#finish({
3288
3414
  target,
3289
3415
  written: [],
@@ -4136,6 +4262,6 @@ var Upstream = class Upstream {
4136
4262
  }
4137
4263
  };
4138
4264
  //#endregion
4139
- export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, filesToHost, hexToDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHost, isHostManifest, isInventory, isManifestEntry, isManifestRegionSet, isMaterializerHooks, isMaterializerOptions, isMirrors, isPaths, isPhysicalDirectory, isPhysicalFile, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, isWorktree, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, readAnchor, readExpectation, readFileHex, readFileText, readHostFloor, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageBytes, stageHost, stageInventory };
4265
+ export { BRANCH_PATTERN, DIGEST_PATTERN, DRIVE_PATTERN, INVALID_SEGMENT_CHARACTER_PATTERN, MANIFEST_NAME, MAX_BRANCH_LENGTH, MAX_ENDPOINT_LENGTH, MAX_INVENTORY_PATHS, MAX_PATH_DEPTH, MAX_PATH_SEGMENT_BYTES, MAX_UPSTREAM_CONCURRENCY, MAX_UPSTREAM_RETRIES, MAX_UPSTREAM_TIMEOUT, Materializer, RESERVED_SEGMENT_PATTERN, Upstream, WriteTransaction, computeDigest, computeFileDigest, computeManifestDigest, filesToHost, hexToDigest, isBranch, isCatalogEntries, isDependencies, isDependencyNames, isDigest, isEndpoint, isExactCaseFile, isFilesystemPath, isHost, isHostManifest, isInventory, isManifestEntry, isManifestRegionSet, isMaterializerHooks, isMaterializerOptions, isMirrors, isPaths, isPhysicalDirectory, isPhysicalFile, isTimeout, isUpstreamHooks, isUpstreamOptions, isVacant, isWorktree, listCanonPaths, listDirectories, listFiles, matchesAnchor, matchesExecutablePath, matchesExpectation, matchesGitPath, matchesMissingPath, matchesPrecondition, matchesProtectedPath, matchesSensitivePath, pathToStorage, pruneEmptiedDirectories, readAnchor, readExpectation, readFileHex, readFileText, readHostFloor, readHostManifest, readManifestEntry, readSnapshot, resolveContainedPath, resolveRealPath, stageBytes, stageHost, stageInventory };
4140
4266
 
4141
4267
  //# sourceMappingURL=index.js.map