@teambit/ci 1.0.524 → 1.0.526

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.
@@ -1,5 +1,5 @@
1
1
  ;
2
- import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.git_ci@1.0.524/dist/ci.docs.mdx';
2
+ import * as overview_0 from '/home/circleci/Library/Caches/Bit/capsules/8891be5ad/teambit.git_ci@1.0.526/dist/ci.docs.mdx';
3
3
 
4
4
  export const compositions = [];
5
5
  export const overview = [overview_0];
@@ -95,17 +95,37 @@ async function importHeadsOf(workspace, ids) {
95
95
  * deliberately distinct: only `read` licenses a destructive heal.
96
96
  */
97
97
 
98
+ /** Phrased for the skip line an operator reads, since that is the only consumer. */
99
+
98
100
  async function mainFileOnScopeHead(workspace, id) {
99
101
  const legacyScope = workspace.scope.legacyScope;
100
102
  try {
101
- const modelComponent = await legacyScope.getModelComponentIfExist(id);
102
- const head = modelComponent?.getHeadRegardlessOfLaneAsTagOrHash();
103
- if (!modelComponent || !head) return {
104
- status: 'absent'
103
+ // Ask for the component, NOT for the version `.bitmap` happens to record. `sources.get` is
104
+ // version-sensitive: when the id carries a version whose Version object is not on the
105
+ // filesystem, it returns `undefined` — indistinguishable here from "the scope has no such
106
+ // component", which sends this function down the `absent` path and makes the heal decline.
107
+ //
108
+ // That is the common case rather than an edge case. The entry being healed is stale by
109
+ // definition, and its recorded version is frequently one this repository never installed: an
110
+ // orphaned tag, or a snap that main has since moved past. `bit install` populates the scope
111
+ // from the `.bitmap` of the branch it ran on, and the main-scope reconciler then checks out a
112
+ // DIFFERENT branch (the sync branch) whose `.bitmap` names that stale version. So the lookup
113
+ // failed precisely when the heal was needed, and the run stayed red reporting a component the
114
+ // scope has as "not on the scope".
115
+ const modelComponent = await legacyScope.getModelComponentIfExist(id.changeVersion(undefined));
116
+ if (!modelComponent) return {
117
+ status: 'absent',
118
+ reason: 'not on the scope'
119
+ };
120
+ const head = modelComponent.getHeadRegardlessOfLaneAsTagOrHash();
121
+ if (!head) return {
122
+ status: 'absent',
123
+ reason: 'no head on the scope'
105
124
  };
106
125
  const version = await modelComponent.loadVersion(head, legacyScope.objects);
107
126
  if (!version?.mainFile) return {
108
- status: 'absent'
127
+ status: 'absent',
128
+ reason: 'the head records no main file'
109
129
  };
110
130
  return {
111
131
  status: 'read',
@@ -159,7 +179,7 @@ async function healMissingMainFiles(workspace, logger) {
159
179
  });
160
180
  } else {
161
181
  // Unknown, never assumed: leave the entry and let the checkout fail with its own message.
162
- skipped.push(`${id} (${onScope.status === 'absent' ? 'not on the scope' : `unreadable: ${onScope.reason}`})`);
182
+ skipped.push(`${id} (${onScope.status === 'absent' ? onScope.reason : `unreadable: ${onScope.reason}`})`);
163
183
  }
164
184
  }
165
185
  if (healed.length) await bitMap.write();
@@ -1 +1 @@
1
- {"version":3,"names":["_fsExtra","data","_interopRequireDefault","require","_path","_cli","_componentId","_laneSyncExecutor","e","__esModule","default","importHeadsOf","workspace","ids","scope","legacyScope","scopeImporter","importWithoutDeps","ComponentIdList","fromArray","toVersionLatest","cache","ignoreMissingHead","ok","error","message","String","mainFileOnScopeHead","id","modelComponent","getModelComponentIfExist","head","getHeadRegardlessOfLaneAsTagOrHash","status","version","loadVersion","objects","mainFile","reason","healMissingMainFiles","logger","workspacePath","path","bitMap","consumer","onDisk","rootDir","file","fs","existsSync","join","stale","components","filter","componentMap","Boolean","length","headFetch","map","healed","skipped","toStringWithoutVersion","onScope","markAsChanged","push","retargetedTo","removeComponent","write","consoleWarning","capEntries","retargeted","heal","untracked","console","formatWarningSummary"],"sources":["heal-missing-main-files.ts"],"sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport { formatWarningSummary } from '@teambit/cli';\nimport type { Logger } from '@teambit/logger';\nimport type { Workspace } from '@teambit/workspace';\nimport type { ComponentID } from '@teambit/component-id';\nimport { ComponentIdList } from '@teambit/component-id';\nimport { capEntries } from './lane-sync-executor';\n\n/**\n * A `.bitmap` entry whose recorded `mainFile` is no longer on disk cannot be loaded at all — the\n * component loader throws `MainFileRemoved`, and that one entry fails the whole `bit checkout head`\n * the main-scope reconciler runs, so every scheduled run stays red until a human intervenes.\n *\n * The shape is legitimate: a new version moves the component's main file (e.g. an env stops emitting\n * `dist/`, so main goes from `dist/index.d.ts` back to source), the old path is deleted in git, and\n * this repository's `.bitmap` still names it.\n *\n * Two heals, cheapest first:\n * 1. RETARGET — the scope's current version names a main file that IS in this repository, so only\n * the `.bitmap` pointer is stale. Rewrite it and the component loads; nothing moves on disk.\n * 2. UNTRACK — the scope's current version was read, and the main file it names is not here\n * either. Drop the entry; main sync's `includeNewFromScope` re-imports the component from the\n * scope's main. This one writes the component to a fresh directory, so it shows up as a move in\n * the sync PR.\n *\n * An entry is only ever untracked on a POSITIVE read of the scope's head. A component the scope does\n * not have, or whose objects could not be read (a failed import, a corrupt object), is left exactly\n * as it is and reported: a transient remote failure must not delete a `.bitmap` entry, and the run\n * failing loudly on it is the honest outcome.\n *\n * Deliberately ci-local: making the checkout itself tolerate an unloadable component would change\n * `bit checkout` for every caller passing a \"theirs\" resolution.\n */\nexport type MainFileHeal = { id: string; retargetedTo?: string };\n\n/**\n * Fetch the remote head of the stale components. Without it the local scope still holds the version\n * this repository already had — whose main file is the missing one — and every entry would look\n * unrepairable. A failure is not fatal on its own (local objects may still answer), but it must\n * never be mistaken for \"the scope says this is gone\", and its cause is the first thing an operator\n * needs when a scheduled run starts leaving entries untouched — so it is carried, not swallowed.\n */\nasync function importHeadsOf(\n workspace: Workspace,\n ids: ComponentID[]\n): Promise<{ ok: true } | { ok: false; error: string }> {\n try {\n await workspace.scope.legacyScope.scopeImporter.importWithoutDeps(\n ComponentIdList.fromArray(ids).toVersionLatest(),\n { cache: false, ignoreMissingHead: true }\n );\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: e?.message || String(e) };\n }\n}\n\n/**\n * What the scope's current (head) version says the component's main file is. The three outcomes are\n * deliberately distinct: only `read` licenses a destructive heal.\n */\ntype ScopeMainFile =\n | { status: 'read'; mainFile: string }\n /** the scope has no such component, or no head for it — nothing to compare against */\n | { status: 'absent' }\n /** objects could not be read (failed import, corrupt object) — the answer is unknown */\n | { status: 'unreadable'; reason: string };\n\nasync function mainFileOnScopeHead(workspace: Workspace, id: ComponentID): Promise<ScopeMainFile> {\n const legacyScope = workspace.scope.legacyScope;\n try {\n const modelComponent = await legacyScope.getModelComponentIfExist(id);\n const head = modelComponent?.getHeadRegardlessOfLaneAsTagOrHash();\n if (!modelComponent || !head) return { status: 'absent' };\n const version = await modelComponent.loadVersion(head, legacyScope.objects);\n if (!version?.mainFile) return { status: 'absent' };\n return { status: 'read', mainFile: version.mainFile };\n } catch (e: any) {\n return { status: 'unreadable', reason: e?.message || String(e) };\n }\n}\n\n/**\n * Heal every `.bitmap` entry whose main file is missing, and persist. Returns what was healed so the\n * caller can report it; the `.bitmap` change rides the sync commit like any other drift.\n */\nexport async function healMissingMainFiles(workspace: Workspace, logger: Logger): Promise<MainFileHeal[]> {\n const workspacePath = workspace.path;\n const { bitMap } = workspace.consumer;\n const onDisk = (rootDir: string, file: string) => fs.existsSync(path.join(workspacePath, rootDir, file));\n\n const stale = bitMap.components.filter((componentMap) => {\n const { rootDir, mainFile } = componentMap;\n return Boolean(rootDir) && Boolean(mainFile) && !onDisk(rootDir as string, mainFile);\n });\n if (!stale.length) return [];\n\n const headFetch = await importHeadsOf(\n workspace,\n stale.map((componentMap) => componentMap.id)\n );\n\n const healed: MainFileHeal[] = [];\n const skipped: string[] = [];\n for (const componentMap of stale) {\n const id = componentMap.id.toStringWithoutVersion();\n const onScope = await mainFileOnScopeHead(workspace, componentMap.id);\n if (onScope.status === 'read' && onDisk(componentMap.rootDir as string, onScope.mainFile)) {\n componentMap.mainFile = onScope.mainFile;\n bitMap.markAsChanged();\n healed.push({ id, retargetedTo: onScope.mainFile });\n } else if (onScope.status === 'read') {\n // Positively read, and the file it names is not here either: the whole component is gone from\n // this tree, so the entry cannot be repaired in place.\n bitMap.removeComponent(componentMap.id);\n healed.push({ id });\n } else {\n // Unknown, never assumed: leave the entry and let the checkout fail with its own message.\n skipped.push(`${id} (${onScope.status === 'absent' ? 'not on the scope' : `unreadable: ${onScope.reason}`})`);\n }\n }\n if (healed.length) await bitMap.write();\n\n if (skipped.length) {\n logger.consoleWarning(\n `Left ${skipped.length} .bitmap entr(ies) with a missing main file untouched — the scope could not ` +\n `confirm what to repair them to: ${capEntries(skipped).join(', ')}` +\n (headFetch.ok ? '' : `. Fetching their heads failed first: ${headFetch.error}`)\n );\n }\n\n const retargeted = healed.filter((heal) => heal.retargetedTo);\n const untracked = healed.filter((heal) => !heal.retargetedTo);\n if (retargeted.length) {\n logger.console(\n formatWarningSummary(\n `Repointed ${retargeted.length} .bitmap entr(ies) at the main file the scope's current version ` +\n `records: ${capEntries(retargeted.map((heal) => `${heal.id} -> ${heal.retargetedTo}`)).join(', ')}`\n )\n );\n }\n if (untracked.length) {\n logger.console(\n formatWarningSummary(\n `Untracked ${untracked.length} component(s) whose recorded main file is gone from this repository, ` +\n `so the scope's current version can be written in their place: ` +\n `${capEntries(untracked.map((heal) => heal.id)).join(', ')}`\n )\n );\n }\n return healed;\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_fsExtra","data","_interopRequireDefault","require","_path","_cli","_componentId","_laneSyncExecutor","e","__esModule","default","importHeadsOf","workspace","ids","scope","legacyScope","scopeImporter","importWithoutDeps","ComponentIdList","fromArray","toVersionLatest","cache","ignoreMissingHead","ok","error","message","String","mainFileOnScopeHead","id","modelComponent","getModelComponentIfExist","changeVersion","undefined","status","reason","head","getHeadRegardlessOfLaneAsTagOrHash","version","loadVersion","objects","mainFile","healMissingMainFiles","logger","workspacePath","path","bitMap","consumer","onDisk","rootDir","file","fs","existsSync","join","stale","components","filter","componentMap","Boolean","length","headFetch","map","healed","skipped","toStringWithoutVersion","onScope","markAsChanged","push","retargetedTo","removeComponent","write","consoleWarning","capEntries","retargeted","heal","untracked","console","formatWarningSummary"],"sources":["heal-missing-main-files.ts"],"sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport { formatWarningSummary } from '@teambit/cli';\nimport type { Logger } from '@teambit/logger';\nimport type { Workspace } from '@teambit/workspace';\nimport type { ComponentID } from '@teambit/component-id';\nimport { ComponentIdList } from '@teambit/component-id';\nimport { capEntries } from './lane-sync-executor';\n\n/**\n * A `.bitmap` entry whose recorded `mainFile` is no longer on disk cannot be loaded at all — the\n * component loader throws `MainFileRemoved`, and that one entry fails the whole `bit checkout head`\n * the main-scope reconciler runs, so every scheduled run stays red until a human intervenes.\n *\n * The shape is legitimate: a new version moves the component's main file (e.g. an env stops emitting\n * `dist/`, so main goes from `dist/index.d.ts` back to source), the old path is deleted in git, and\n * this repository's `.bitmap` still names it.\n *\n * Two heals, cheapest first:\n * 1. RETARGET — the scope's current version names a main file that IS in this repository, so only\n * the `.bitmap` pointer is stale. Rewrite it and the component loads; nothing moves on disk.\n * 2. UNTRACK — the scope's current version was read, and the main file it names is not here\n * either. Drop the entry; main sync's `includeNewFromScope` re-imports the component from the\n * scope's main. This one writes the component to a fresh directory, so it shows up as a move in\n * the sync PR.\n *\n * An entry is only ever untracked on a POSITIVE read of the scope's head. A component the scope does\n * not have, or whose objects could not be read (a failed import, a corrupt object), is left exactly\n * as it is and reported: a transient remote failure must not delete a `.bitmap` entry, and the run\n * failing loudly on it is the honest outcome.\n *\n * Deliberately ci-local: making the checkout itself tolerate an unloadable component would change\n * `bit checkout` for every caller passing a \"theirs\" resolution.\n */\nexport type MainFileHeal = { id: string; retargetedTo?: string };\n\n/**\n * Fetch the remote head of the stale components. Without it the local scope still holds the version\n * this repository already had — whose main file is the missing one — and every entry would look\n * unrepairable. A failure is not fatal on its own (local objects may still answer), but it must\n * never be mistaken for \"the scope says this is gone\", and its cause is the first thing an operator\n * needs when a scheduled run starts leaving entries untouched — so it is carried, not swallowed.\n */\nasync function importHeadsOf(\n workspace: Workspace,\n ids: ComponentID[]\n): Promise<{ ok: true } | { ok: false; error: string }> {\n try {\n await workspace.scope.legacyScope.scopeImporter.importWithoutDeps(\n ComponentIdList.fromArray(ids).toVersionLatest(),\n { cache: false, ignoreMissingHead: true }\n );\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: e?.message || String(e) };\n }\n}\n\n/**\n * What the scope's current (head) version says the component's main file is. The three outcomes are\n * deliberately distinct: only `read` licenses a destructive heal.\n */\ntype ScopeMainFile =\n | { status: 'read'; mainFile: string }\n /**\n * nothing on the scope to compare against. Carries which of the three it was, because they send\n * an operator to three different places and a single blanket wording sent this bug's\n * investigation to the wrong one.\n */\n | { status: 'absent'; reason: AbsentReason }\n /** objects could not be read (failed import, corrupt object) — the answer is unknown */\n | { status: 'unreadable'; reason: string };\n\n/** Phrased for the skip line an operator reads, since that is the only consumer. */\ntype AbsentReason = 'not on the scope' | 'no head on the scope' | 'the head records no main file';\n\nasync function mainFileOnScopeHead(workspace: Workspace, id: ComponentID): Promise<ScopeMainFile> {\n const legacyScope = workspace.scope.legacyScope;\n try {\n // Ask for the component, NOT for the version `.bitmap` happens to record. `sources.get` is\n // version-sensitive: when the id carries a version whose Version object is not on the\n // filesystem, it returns `undefined` — indistinguishable here from \"the scope has no such\n // component\", which sends this function down the `absent` path and makes the heal decline.\n //\n // That is the common case rather than an edge case. The entry being healed is stale by\n // definition, and its recorded version is frequently one this repository never installed: an\n // orphaned tag, or a snap that main has since moved past. `bit install` populates the scope\n // from the `.bitmap` of the branch it ran on, and the main-scope reconciler then checks out a\n // DIFFERENT branch (the sync branch) whose `.bitmap` names that stale version. So the lookup\n // failed precisely when the heal was needed, and the run stayed red reporting a component the\n // scope has as \"not on the scope\".\n const modelComponent = await legacyScope.getModelComponentIfExist(id.changeVersion(undefined));\n if (!modelComponent) return { status: 'absent', reason: 'not on the scope' };\n const head = modelComponent.getHeadRegardlessOfLaneAsTagOrHash();\n if (!head) return { status: 'absent', reason: 'no head on the scope' };\n const version = await modelComponent.loadVersion(head, legacyScope.objects);\n if (!version?.mainFile) return { status: 'absent', reason: 'the head records no main file' };\n return { status: 'read', mainFile: version.mainFile };\n } catch (e: any) {\n return { status: 'unreadable', reason: e?.message || String(e) };\n }\n}\n\n/**\n * Heal every `.bitmap` entry whose main file is missing, and persist. Returns what was healed so the\n * caller can report it; the `.bitmap` change rides the sync commit like any other drift.\n */\nexport async function healMissingMainFiles(workspace: Workspace, logger: Logger): Promise<MainFileHeal[]> {\n const workspacePath = workspace.path;\n const { bitMap } = workspace.consumer;\n const onDisk = (rootDir: string, file: string) => fs.existsSync(path.join(workspacePath, rootDir, file));\n\n const stale = bitMap.components.filter((componentMap) => {\n const { rootDir, mainFile } = componentMap;\n return Boolean(rootDir) && Boolean(mainFile) && !onDisk(rootDir as string, mainFile);\n });\n if (!stale.length) return [];\n\n const headFetch = await importHeadsOf(\n workspace,\n stale.map((componentMap) => componentMap.id)\n );\n\n const healed: MainFileHeal[] = [];\n const skipped: string[] = [];\n for (const componentMap of stale) {\n const id = componentMap.id.toStringWithoutVersion();\n const onScope = await mainFileOnScopeHead(workspace, componentMap.id);\n if (onScope.status === 'read' && onDisk(componentMap.rootDir as string, onScope.mainFile)) {\n componentMap.mainFile = onScope.mainFile;\n bitMap.markAsChanged();\n healed.push({ id, retargetedTo: onScope.mainFile });\n } else if (onScope.status === 'read') {\n // Positively read, and the file it names is not here either: the whole component is gone from\n // this tree, so the entry cannot be repaired in place.\n bitMap.removeComponent(componentMap.id);\n healed.push({ id });\n } else {\n // Unknown, never assumed: leave the entry and let the checkout fail with its own message.\n skipped.push(`${id} (${onScope.status === 'absent' ? onScope.reason : `unreadable: ${onScope.reason}`})`);\n }\n }\n if (healed.length) await bitMap.write();\n\n if (skipped.length) {\n logger.consoleWarning(\n `Left ${skipped.length} .bitmap entr(ies) with a missing main file untouched — the scope could not ` +\n `confirm what to repair them to: ${capEntries(skipped).join(', ')}` +\n (headFetch.ok ? '' : `. Fetching their heads failed first: ${headFetch.error}`)\n );\n }\n\n const retargeted = healed.filter((heal) => heal.retargetedTo);\n const untracked = healed.filter((heal) => !heal.retargetedTo);\n if (retargeted.length) {\n logger.console(\n formatWarningSummary(\n `Repointed ${retargeted.length} .bitmap entr(ies) at the main file the scope's current version ` +\n `records: ${capEntries(retargeted.map((heal) => `${heal.id} -> ${heal.retargetedTo}`)).join(', ')}`\n )\n );\n }\n if (untracked.length) {\n logger.console(\n formatWarningSummary(\n `Untracked ${untracked.length} component(s) whose recorded main file is gone from this repository, ` +\n `so the scope's current version can be written in their place: ` +\n `${capEntries(untracked.map((heal) => heal.id)).join(', ')}`\n )\n );\n }\n return healed;\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+
3
+ function _chai() {
4
+ const data = require("chai");
5
+ _chai = function () {
6
+ return data;
7
+ };
8
+ return data;
9
+ }
10
+ function _fsExtra() {
11
+ const data = _interopRequireDefault(require("fs-extra"));
12
+ _fsExtra = function () {
13
+ return data;
14
+ };
15
+ return data;
16
+ }
17
+ function _os() {
18
+ const data = _interopRequireDefault(require("os"));
19
+ _os = function () {
20
+ return data;
21
+ };
22
+ return data;
23
+ }
24
+ function _path() {
25
+ const data = _interopRequireDefault(require("path"));
26
+ _path = function () {
27
+ return data;
28
+ };
29
+ return data;
30
+ }
31
+ function _componentId() {
32
+ const data = require("@teambit/component-id");
33
+ _componentId = function () {
34
+ return data;
35
+ };
36
+ return data;
37
+ }
38
+ function _healMissingMainFiles() {
39
+ const data = require("./heal-missing-main-files");
40
+ _healMissingMainFiles = function () {
41
+ return data;
42
+ };
43
+ return data;
44
+ }
45
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
46
+ const ID_STR = 'teambit.api-reference/renderers/schema-node-member-summary';
47
+ /** A version this repository never installed: an orphaned tag whose Version object is not on the fs. */
48
+ const STALE_VERSION = '0.0.0-75238957ce4bc43dee7758156dde5bc3a343e70d';
49
+ const SCOPE_HEAD = '0.0.85';
50
+ const ROOT_DIR = 'components/renderers/schema-node-member-summary';
51
+
52
+ /**
53
+ * What the scope answers for the component. `absent` means `getModelComponentIfExist` resolves to
54
+ * undefined; `headless` and `mainFileless` exercise the other two paths that also yield an `absent`
55
+ * verdict, which must each report their own reason.
56
+ */
57
+
58
+ /**
59
+ * Real heal code over stubbed deps. `getModelComponentIfExist` reproduces what made this fail:
60
+ * `sources.get` is version-sensitive, so an id carrying a version whose Version object is absent
61
+ * from the filesystem resolves to undefined, while the same id without a version resolves to the
62
+ * component. `lookedUp` records every id the heal asked the scope for.
63
+ */
64
+ function stubWorkspace(wsPath, scope, mainFileInBitmap = 'dist/index.d.ts') {
65
+ const id = _componentId().ComponentID.fromString(`${ID_STR}@${STALE_VERSION}`);
66
+ const componentMap = {
67
+ id,
68
+ mainFile: mainFileInBitmap,
69
+ rootDir: ROOT_DIR
70
+ };
71
+ const lookedUp = [];
72
+ const removed = [];
73
+ const modelComponent = {
74
+ getHeadRegardlessOfLaneAsTagOrHash: () => scope === 'headless' ? undefined : SCOPE_HEAD,
75
+ loadVersion: async () => scope === 'mainFileless' ? {} : {
76
+ mainFile: 'index.ts'
77
+ }
78
+ };
79
+ const workspace = {
80
+ path: wsPath,
81
+ consumer: {
82
+ bitMap: {
83
+ components: [componentMap],
84
+ markAsChanged: () => {},
85
+ removeComponent: compId => removed.push(compId.toString()),
86
+ write: async () => {}
87
+ }
88
+ },
89
+ scope: {
90
+ legacyScope: {
91
+ objects: {},
92
+ scopeImporter: {
93
+ importWithoutDeps: async () => undefined
94
+ },
95
+ getModelComponentIfExist: async asked => {
96
+ lookedUp.push(asked.toString());
97
+ // The crux: a versioned lookup resolves to undefined even when the component is there.
98
+ if (asked.hasVersion()) return undefined;
99
+ return scope === 'absent' ? undefined : modelComponent;
100
+ }
101
+ }
102
+ }
103
+ };
104
+ const warnings = [];
105
+ const logger = {
106
+ console: () => {},
107
+ consoleWarning: msg => warnings.push(msg)
108
+ };
109
+ return {
110
+ workspace,
111
+ logger,
112
+ componentMap,
113
+ lookedUp,
114
+ warnings,
115
+ removed
116
+ };
117
+ }
118
+ describe('healMissingMainFiles', () => {
119
+ let wsPath;
120
+ beforeEach(() => {
121
+ wsPath = _fsExtra().default.mkdtempSync(_path().default.join(_os().default.tmpdir(), 'heal-main-file-'));
122
+ // The `.bitmap` entry points at dist/index.d.ts; only the source file is on disk.
123
+ _fsExtra().default.outputFileSync(_path().default.join(wsPath, ROOT_DIR, 'index.ts'), 'export {};\n');
124
+ });
125
+ afterEach(() => {
126
+ _fsExtra().default.rmSync(wsPath, {
127
+ recursive: true,
128
+ force: true
129
+ });
130
+ });
131
+ describe('when the .bitmap entry records a version the scope cannot resolve', () => {
132
+ it('retargets the entry at the main file the scope head records', async () => {
133
+ const {
134
+ workspace,
135
+ logger,
136
+ componentMap
137
+ } = stubWorkspace(wsPath, 'has-head');
138
+ const healed = await (0, _healMissingMainFiles().healMissingMainFiles)(workspace, logger);
139
+ (0, _chai().expect)(healed).to.have.lengthOf(1);
140
+ (0, _chai().expect)(healed[0].retargetedTo).to.equal('index.ts');
141
+ (0, _chai().expect)(componentMap.mainFile).to.equal('index.ts');
142
+ });
143
+
144
+ // The regression guard. Looking the component up WITH the stale version made every scheduled
145
+ // main sync decline for a component the scope had, and no amount of retrying could clear it.
146
+ it('never asks the scope for the stale version, only for the component', async () => {
147
+ const {
148
+ workspace,
149
+ logger,
150
+ lookedUp
151
+ } = stubWorkspace(wsPath, 'has-head');
152
+ await (0, _healMissingMainFiles().healMissingMainFiles)(workspace, logger);
153
+ (0, _chai().expect)(lookedUp).to.have.lengthOf(1);
154
+ (0, _chai().expect)(lookedUp[0]).to.equal(ID_STR);
155
+ (0, _chai().expect)(lookedUp[0]).to.not.contain(STALE_VERSION);
156
+ });
157
+ });
158
+
159
+ // All three leave the entry alone, so the reported reason is the only thing telling an operator
160
+ // which one happened. A blanket wording here is what made this bug look like a scope problem.
161
+ describe('when the scope cannot say what to repair to', () => {
162
+ const cases = [{
163
+ scope: 'absent',
164
+ reason: 'not on the scope'
165
+ }, {
166
+ scope: 'headless',
167
+ reason: 'no head on the scope'
168
+ }, {
169
+ scope: 'mainFileless',
170
+ reason: 'the head records no main file'
171
+ }];
172
+ cases.forEach(({
173
+ scope,
174
+ reason
175
+ }) => {
176
+ it(`reports "${reason}" and touches nothing`, async () => {
177
+ const {
178
+ workspace,
179
+ logger,
180
+ componentMap,
181
+ warnings,
182
+ removed
183
+ } = stubWorkspace(wsPath, scope);
184
+ const healed = await (0, _healMissingMainFiles().healMissingMainFiles)(workspace, logger);
185
+ (0, _chai().expect)(healed).to.have.lengthOf(0);
186
+ (0, _chai().expect)(removed).to.have.lengthOf(0);
187
+ (0, _chai().expect)(componentMap.mainFile).to.equal('dist/index.d.ts');
188
+ (0, _chai().expect)(warnings.join('\n')).to.contain(`${ID_STR} (${reason})`);
189
+ });
190
+ });
191
+ });
192
+ it('does nothing when every recorded main file is on disk', async () => {
193
+ const {
194
+ workspace,
195
+ logger,
196
+ lookedUp
197
+ } = stubWorkspace(wsPath, 'has-head', 'index.ts');
198
+ const healed = await (0, _healMissingMainFiles().healMissingMainFiles)(workspace, logger);
199
+ (0, _chai().expect)(healed).to.have.lengthOf(0);
200
+ (0, _chai().expect)(lookedUp).to.have.lengthOf(0);
201
+ });
202
+ });
203
+
204
+ //# sourceMappingURL=heal-missing-main-files.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_chai","data","require","_fsExtra","_interopRequireDefault","_os","_path","_componentId","_healMissingMainFiles","e","__esModule","default","ID_STR","STALE_VERSION","SCOPE_HEAD","ROOT_DIR","stubWorkspace","wsPath","scope","mainFileInBitmap","id","ComponentID","fromString","componentMap","mainFile","rootDir","lookedUp","removed","modelComponent","getHeadRegardlessOfLaneAsTagOrHash","undefined","loadVersion","workspace","path","consumer","bitMap","components","markAsChanged","removeComponent","compId","push","toString","write","legacyScope","objects","scopeImporter","importWithoutDeps","getModelComponentIfExist","asked","hasVersion","warnings","logger","console","consoleWarning","msg","describe","beforeEach","fs","mkdtempSync","join","os","tmpdir","outputFileSync","afterEach","rmSync","recursive","force","it","healed","healMissingMainFiles","expect","to","have","lengthOf","retargetedTo","equal","not","contain","cases","reason","forEach"],"sources":["heal-missing-main-files.spec.ts"],"sourcesContent":["import { expect } from 'chai';\nimport fs from 'fs-extra';\nimport os from 'os';\nimport path from 'path';\nimport { ComponentID } from '@teambit/component-id';\nimport { healMissingMainFiles } from './heal-missing-main-files';\n\nconst ID_STR = 'teambit.api-reference/renderers/schema-node-member-summary';\n/** A version this repository never installed: an orphaned tag whose Version object is not on the fs. */\nconst STALE_VERSION = '0.0.0-75238957ce4bc43dee7758156dde5bc3a343e70d';\nconst SCOPE_HEAD = '0.0.85';\nconst ROOT_DIR = 'components/renderers/schema-node-member-summary';\n\n/**\n * What the scope answers for the component. `absent` means `getModelComponentIfExist` resolves to\n * undefined; `headless` and `mainFileless` exercise the other two paths that also yield an `absent`\n * verdict, which must each report their own reason.\n */\ntype ScopeState = 'has-head' | 'absent' | 'headless' | 'mainFileless';\n\n/**\n * Real heal code over stubbed deps. `getModelComponentIfExist` reproduces what made this fail:\n * `sources.get` is version-sensitive, so an id carrying a version whose Version object is absent\n * from the filesystem resolves to undefined, while the same id without a version resolves to the\n * component. `lookedUp` records every id the heal asked the scope for.\n */\nfunction stubWorkspace(wsPath: string, scope: ScopeState, mainFileInBitmap = 'dist/index.d.ts') {\n const id = ComponentID.fromString(`${ID_STR}@${STALE_VERSION}`);\n const componentMap: any = { id, mainFile: mainFileInBitmap, rootDir: ROOT_DIR };\n const lookedUp: string[] = [];\n const removed: string[] = [];\n\n const modelComponent = {\n getHeadRegardlessOfLaneAsTagOrHash: () => (scope === 'headless' ? undefined : SCOPE_HEAD),\n loadVersion: async () => (scope === 'mainFileless' ? {} : { mainFile: 'index.ts' }),\n };\n\n const workspace: any = {\n path: wsPath,\n consumer: {\n bitMap: {\n components: [componentMap],\n markAsChanged: () => {},\n removeComponent: (compId: ComponentID) => removed.push(compId.toString()),\n write: async () => {},\n },\n },\n scope: {\n legacyScope: {\n objects: {},\n scopeImporter: { importWithoutDeps: async () => undefined },\n getModelComponentIfExist: async (asked: ComponentID) => {\n lookedUp.push(asked.toString());\n // The crux: a versioned lookup resolves to undefined even when the component is there.\n if (asked.hasVersion()) return undefined;\n return scope === 'absent' ? undefined : modelComponent;\n },\n },\n },\n };\n\n const warnings: string[] = [];\n const logger: any = { console: () => {}, consoleWarning: (msg: string) => warnings.push(msg) };\n return { workspace, logger, componentMap, lookedUp, warnings, removed };\n}\n\ndescribe('healMissingMainFiles', () => {\n let wsPath: string;\n\n beforeEach(() => {\n wsPath = fs.mkdtempSync(path.join(os.tmpdir(), 'heal-main-file-'));\n // The `.bitmap` entry points at dist/index.d.ts; only the source file is on disk.\n fs.outputFileSync(path.join(wsPath, ROOT_DIR, 'index.ts'), 'export {};\\n');\n });\n\n afterEach(() => {\n fs.rmSync(wsPath, { recursive: true, force: true });\n });\n\n describe('when the .bitmap entry records a version the scope cannot resolve', () => {\n it('retargets the entry at the main file the scope head records', async () => {\n const { workspace, logger, componentMap } = stubWorkspace(wsPath, 'has-head');\n\n const healed = await healMissingMainFiles(workspace, logger);\n\n expect(healed).to.have.lengthOf(1);\n expect(healed[0].retargetedTo).to.equal('index.ts');\n expect(componentMap.mainFile).to.equal('index.ts');\n });\n\n // The regression guard. Looking the component up WITH the stale version made every scheduled\n // main sync decline for a component the scope had, and no amount of retrying could clear it.\n it('never asks the scope for the stale version, only for the component', async () => {\n const { workspace, logger, lookedUp } = stubWorkspace(wsPath, 'has-head');\n\n await healMissingMainFiles(workspace, logger);\n\n expect(lookedUp).to.have.lengthOf(1);\n expect(lookedUp[0]).to.equal(ID_STR);\n expect(lookedUp[0]).to.not.contain(STALE_VERSION);\n });\n });\n\n // All three leave the entry alone, so the reported reason is the only thing telling an operator\n // which one happened. A blanket wording here is what made this bug look like a scope problem.\n describe('when the scope cannot say what to repair to', () => {\n const cases: Array<{ scope: ScopeState; reason: string }> = [\n { scope: 'absent', reason: 'not on the scope' },\n { scope: 'headless', reason: 'no head on the scope' },\n { scope: 'mainFileless', reason: 'the head records no main file' },\n ];\n\n cases.forEach(({ scope, reason }) => {\n it(`reports \"${reason}\" and touches nothing`, async () => {\n const { workspace, logger, componentMap, warnings, removed } = stubWorkspace(wsPath, scope);\n\n const healed = await healMissingMainFiles(workspace, logger);\n\n expect(healed).to.have.lengthOf(0);\n expect(removed).to.have.lengthOf(0);\n expect(componentMap.mainFile).to.equal('dist/index.d.ts');\n expect(warnings.join('\\n')).to.contain(`${ID_STR} (${reason})`);\n });\n });\n });\n\n it('does nothing when every recorded main file is on disk', async () => {\n const { workspace, logger, lookedUp } = stubWorkspace(wsPath, 'has-head', 'index.ts');\n\n const healed = await healMissingMainFiles(workspace, logger);\n\n expect(healed).to.have.lengthOf(0);\n expect(lookedUp).to.have.lengthOf(0);\n });\n});\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teambit/ci",
3
- "version": "1.0.524",
3
+ "version": "1.0.526",
4
4
  "homepage": "https://bit.cloud/teambit/git/ci",
5
5
  "main": "dist/index.js",
6
6
  "componentId": {
7
7
  "scope": "teambit.git",
8
8
  "name": "ci",
9
- "version": "1.0.524"
9
+ "version": "1.0.526"
10
10
  },
11
11
  "dependencies": {
12
12
  "chalk": "4.1.2",
@@ -16,32 +16,32 @@
16
16
  "simple-git": "^3.28.0",
17
17
  "fs-extra": "10.0.0",
18
18
  "@teambit/harmony": "0.4.12",
19
- "@teambit/cli": "0.0.1375",
20
- "@teambit/logger": "0.0.1468",
21
- "@teambit/workspace": "1.0.1128",
22
- "@teambit/builder": "1.0.1128",
19
+ "@teambit/cli": "0.0.1376",
20
+ "@teambit/logger": "0.0.1469",
21
+ "@teambit/workspace": "1.0.1129",
22
+ "@teambit/builder": "1.0.1129",
23
+ "@teambit/checkout": "1.0.1131",
23
24
  "@teambit/component-id": "1.2.4",
24
- "@teambit/component.modules.merge-helper": "0.0.91",
25
- "@teambit/component.snap-distance": "0.0.149",
26
- "@teambit/config-merger": "0.0.995",
27
- "@teambit/dependency-resolver": "1.0.1128",
28
- "@teambit/export": "1.0.1128",
25
+ "@teambit/component.modules.merge-helper": "0.0.92",
26
+ "@teambit/component.snap-distance": "0.0.150",
27
+ "@teambit/config-merger": "0.0.996",
28
+ "@teambit/dependency-resolver": "1.0.1129",
29
+ "@teambit/export": "1.0.1129",
29
30
  "@teambit/harmony.modules.concurrency": "0.0.42",
30
- "@teambit/importer": "1.0.1128",
31
+ "@teambit/importer": "1.0.1129",
31
32
  "@teambit/lane-id": "0.0.312",
32
- "@teambit/legacy.consumer-component": "0.0.149",
33
- "@teambit/objects": "0.0.635",
34
- "@teambit/snapping": "1.0.1128",
33
+ "@teambit/lanes": "1.0.1151",
34
+ "@teambit/legacy.consumer-component": "0.0.150",
35
+ "@teambit/objects": "0.0.636",
36
+ "@teambit/snapping": "1.0.1129",
37
+ "@teambit/status": "1.0.1155",
35
38
  "@teambit/toolbox.promise.map-pool": "0.0.19",
36
39
  "@teambit/toolbox.string.random": "0.0.18",
37
40
  "@teambit/bit-error": "0.0.404",
38
41
  "@teambit/legacy.constants": "0.0.41",
39
- "@teambit/legacy.scope": "0.0.148",
40
- "@teambit/legacy.bit-map": "0.0.205",
41
- "@teambit/toolbox.crypto.sha1": "0.0.20",
42
- "@teambit/checkout": "1.0.1130",
43
- "@teambit/lanes": "1.0.1150",
44
- "@teambit/status": "1.0.1154"
42
+ "@teambit/legacy.scope": "0.0.149",
43
+ "@teambit/legacy.bit-map": "0.0.206",
44
+ "@teambit/toolbox.crypto.sha1": "0.0.20"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/lodash": "4.14.165",
@@ -0,0 +1,135 @@
1
+ import { expect } from 'chai';
2
+ import fs from 'fs-extra';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { ComponentID } from '@teambit/component-id';
6
+ import { healMissingMainFiles } from './heal-missing-main-files';
7
+
8
+ const ID_STR = 'teambit.api-reference/renderers/schema-node-member-summary';
9
+ /** A version this repository never installed: an orphaned tag whose Version object is not on the fs. */
10
+ const STALE_VERSION = '0.0.0-75238957ce4bc43dee7758156dde5bc3a343e70d';
11
+ const SCOPE_HEAD = '0.0.85';
12
+ const ROOT_DIR = 'components/renderers/schema-node-member-summary';
13
+
14
+ /**
15
+ * What the scope answers for the component. `absent` means `getModelComponentIfExist` resolves to
16
+ * undefined; `headless` and `mainFileless` exercise the other two paths that also yield an `absent`
17
+ * verdict, which must each report their own reason.
18
+ */
19
+ type ScopeState = 'has-head' | 'absent' | 'headless' | 'mainFileless';
20
+
21
+ /**
22
+ * Real heal code over stubbed deps. `getModelComponentIfExist` reproduces what made this fail:
23
+ * `sources.get` is version-sensitive, so an id carrying a version whose Version object is absent
24
+ * from the filesystem resolves to undefined, while the same id without a version resolves to the
25
+ * component. `lookedUp` records every id the heal asked the scope for.
26
+ */
27
+ function stubWorkspace(wsPath: string, scope: ScopeState, mainFileInBitmap = 'dist/index.d.ts') {
28
+ const id = ComponentID.fromString(`${ID_STR}@${STALE_VERSION}`);
29
+ const componentMap: any = { id, mainFile: mainFileInBitmap, rootDir: ROOT_DIR };
30
+ const lookedUp: string[] = [];
31
+ const removed: string[] = [];
32
+
33
+ const modelComponent = {
34
+ getHeadRegardlessOfLaneAsTagOrHash: () => (scope === 'headless' ? undefined : SCOPE_HEAD),
35
+ loadVersion: async () => (scope === 'mainFileless' ? {} : { mainFile: 'index.ts' }),
36
+ };
37
+
38
+ const workspace: any = {
39
+ path: wsPath,
40
+ consumer: {
41
+ bitMap: {
42
+ components: [componentMap],
43
+ markAsChanged: () => {},
44
+ removeComponent: (compId: ComponentID) => removed.push(compId.toString()),
45
+ write: async () => {},
46
+ },
47
+ },
48
+ scope: {
49
+ legacyScope: {
50
+ objects: {},
51
+ scopeImporter: { importWithoutDeps: async () => undefined },
52
+ getModelComponentIfExist: async (asked: ComponentID) => {
53
+ lookedUp.push(asked.toString());
54
+ // The crux: a versioned lookup resolves to undefined even when the component is there.
55
+ if (asked.hasVersion()) return undefined;
56
+ return scope === 'absent' ? undefined : modelComponent;
57
+ },
58
+ },
59
+ },
60
+ };
61
+
62
+ const warnings: string[] = [];
63
+ const logger: any = { console: () => {}, consoleWarning: (msg: string) => warnings.push(msg) };
64
+ return { workspace, logger, componentMap, lookedUp, warnings, removed };
65
+ }
66
+
67
+ describe('healMissingMainFiles', () => {
68
+ let wsPath: string;
69
+
70
+ beforeEach(() => {
71
+ wsPath = fs.mkdtempSync(path.join(os.tmpdir(), 'heal-main-file-'));
72
+ // The `.bitmap` entry points at dist/index.d.ts; only the source file is on disk.
73
+ fs.outputFileSync(path.join(wsPath, ROOT_DIR, 'index.ts'), 'export {};\n');
74
+ });
75
+
76
+ afterEach(() => {
77
+ fs.rmSync(wsPath, { recursive: true, force: true });
78
+ });
79
+
80
+ describe('when the .bitmap entry records a version the scope cannot resolve', () => {
81
+ it('retargets the entry at the main file the scope head records', async () => {
82
+ const { workspace, logger, componentMap } = stubWorkspace(wsPath, 'has-head');
83
+
84
+ const healed = await healMissingMainFiles(workspace, logger);
85
+
86
+ expect(healed).to.have.lengthOf(1);
87
+ expect(healed[0].retargetedTo).to.equal('index.ts');
88
+ expect(componentMap.mainFile).to.equal('index.ts');
89
+ });
90
+
91
+ // The regression guard. Looking the component up WITH the stale version made every scheduled
92
+ // main sync decline for a component the scope had, and no amount of retrying could clear it.
93
+ it('never asks the scope for the stale version, only for the component', async () => {
94
+ const { workspace, logger, lookedUp } = stubWorkspace(wsPath, 'has-head');
95
+
96
+ await healMissingMainFiles(workspace, logger);
97
+
98
+ expect(lookedUp).to.have.lengthOf(1);
99
+ expect(lookedUp[0]).to.equal(ID_STR);
100
+ expect(lookedUp[0]).to.not.contain(STALE_VERSION);
101
+ });
102
+ });
103
+
104
+ // All three leave the entry alone, so the reported reason is the only thing telling an operator
105
+ // which one happened. A blanket wording here is what made this bug look like a scope problem.
106
+ describe('when the scope cannot say what to repair to', () => {
107
+ const cases: Array<{ scope: ScopeState; reason: string }> = [
108
+ { scope: 'absent', reason: 'not on the scope' },
109
+ { scope: 'headless', reason: 'no head on the scope' },
110
+ { scope: 'mainFileless', reason: 'the head records no main file' },
111
+ ];
112
+
113
+ cases.forEach(({ scope, reason }) => {
114
+ it(`reports "${reason}" and touches nothing`, async () => {
115
+ const { workspace, logger, componentMap, warnings, removed } = stubWorkspace(wsPath, scope);
116
+
117
+ const healed = await healMissingMainFiles(workspace, logger);
118
+
119
+ expect(healed).to.have.lengthOf(0);
120
+ expect(removed).to.have.lengthOf(0);
121
+ expect(componentMap.mainFile).to.equal('dist/index.d.ts');
122
+ expect(warnings.join('\n')).to.contain(`${ID_STR} (${reason})`);
123
+ });
124
+ });
125
+ });
126
+
127
+ it('does nothing when every recorded main file is on disk', async () => {
128
+ const { workspace, logger, lookedUp } = stubWorkspace(wsPath, 'has-head', 'index.ts');
129
+
130
+ const healed = await healMissingMainFiles(workspace, logger);
131
+
132
+ expect(healed).to.have.lengthOf(0);
133
+ expect(lookedUp).to.have.lengthOf(0);
134
+ });
135
+ });
@@ -62,19 +62,39 @@ async function importHeadsOf(
62
62
  */
63
63
  type ScopeMainFile =
64
64
  | { status: 'read'; mainFile: string }
65
- /** the scope has no such component, or no head for it — nothing to compare against */
66
- | { status: 'absent' }
65
+ /**
66
+ * nothing on the scope to compare against. Carries which of the three it was, because they send
67
+ * an operator to three different places and a single blanket wording sent this bug's
68
+ * investigation to the wrong one.
69
+ */
70
+ | { status: 'absent'; reason: AbsentReason }
67
71
  /** objects could not be read (failed import, corrupt object) — the answer is unknown */
68
72
  | { status: 'unreadable'; reason: string };
69
73
 
74
+ /** Phrased for the skip line an operator reads, since that is the only consumer. */
75
+ type AbsentReason = 'not on the scope' | 'no head on the scope' | 'the head records no main file';
76
+
70
77
  async function mainFileOnScopeHead(workspace: Workspace, id: ComponentID): Promise<ScopeMainFile> {
71
78
  const legacyScope = workspace.scope.legacyScope;
72
79
  try {
73
- const modelComponent = await legacyScope.getModelComponentIfExist(id);
74
- const head = modelComponent?.getHeadRegardlessOfLaneAsTagOrHash();
75
- if (!modelComponent || !head) return { status: 'absent' };
80
+ // Ask for the component, NOT for the version `.bitmap` happens to record. `sources.get` is
81
+ // version-sensitive: when the id carries a version whose Version object is not on the
82
+ // filesystem, it returns `undefined` — indistinguishable here from "the scope has no such
83
+ // component", which sends this function down the `absent` path and makes the heal decline.
84
+ //
85
+ // That is the common case rather than an edge case. The entry being healed is stale by
86
+ // definition, and its recorded version is frequently one this repository never installed: an
87
+ // orphaned tag, or a snap that main has since moved past. `bit install` populates the scope
88
+ // from the `.bitmap` of the branch it ran on, and the main-scope reconciler then checks out a
89
+ // DIFFERENT branch (the sync branch) whose `.bitmap` names that stale version. So the lookup
90
+ // failed precisely when the heal was needed, and the run stayed red reporting a component the
91
+ // scope has as "not on the scope".
92
+ const modelComponent = await legacyScope.getModelComponentIfExist(id.changeVersion(undefined));
93
+ if (!modelComponent) return { status: 'absent', reason: 'not on the scope' };
94
+ const head = modelComponent.getHeadRegardlessOfLaneAsTagOrHash();
95
+ if (!head) return { status: 'absent', reason: 'no head on the scope' };
76
96
  const version = await modelComponent.loadVersion(head, legacyScope.objects);
77
- if (!version?.mainFile) return { status: 'absent' };
97
+ if (!version?.mainFile) return { status: 'absent', reason: 'the head records no main file' };
78
98
  return { status: 'read', mainFile: version.mainFile };
79
99
  } catch (e: any) {
80
100
  return { status: 'unreadable', reason: e?.message || String(e) };
@@ -117,7 +137,7 @@ export async function healMissingMainFiles(workspace: Workspace, logger: Logger)
117
137
  healed.push({ id });
118
138
  } else {
119
139
  // Unknown, never assumed: leave the entry and let the checkout fail with its own message.
120
- skipped.push(`${id} (${onScope.status === 'absent' ? 'not on the scope' : `unreadable: ${onScope.reason}`})`);
140
+ skipped.push(`${id} (${onScope.status === 'absent' ? onScope.reason : `unreadable: ${onScope.reason}`})`);
121
141
  }
122
142
  }
123
143
  if (healed.length) await bitMap.write();