@tenonhq/dovetail-core 0.0.109 → 0.0.111

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/dist/appUtils.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = exports.stampMetadataContent = void 0;
39
+ exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.narrowManifestToTables = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = exports.stampMetadataContent = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const progress_1 = __importDefault(require("progress"));
42
42
  const fUtils = __importStar(require("./FileUtils"));
@@ -272,6 +272,32 @@ const processManifest = async (manifest, forceWrite = false, sourcePath) => {
272
272
  }
273
273
  };
274
274
  exports.processManifest = processManifest;
275
+ /**
276
+ * Narrow a manifest's table map to `tables`, for the FILE refresh only.
277
+ *
278
+ * Returns a NEW manifest and NEVER mutates its input — the caller must keep
279
+ * writing the original, full manifest to disk. Narrowing the persisted manifest
280
+ * would drop every other table from dove.manifest.<scope>.json and break
281
+ * push/watch. That non-mutation is the whole safety property of `--table`, so
282
+ * it lives in one testable function rather than inline in syncManifest.
283
+ *
284
+ * An empty table map means the scope holds none of the requested tables;
285
+ * refreshAllFiles no-ops on an empty map, so the scope is skipped untouched.
286
+ */
287
+ const narrowManifestToTables = (manifest, tables) => {
288
+ if (!tables || tables.length === 0)
289
+ return manifest;
290
+ var wantedTables = {};
291
+ var presentTableNames = Object.keys(manifest.tables || {});
292
+ for (var w = 0; w < presentTableNames.length; w++) {
293
+ var wName = presentTableNames[w];
294
+ if (tables.indexOf(wName) !== -1) {
295
+ wantedTables[wName] = manifest.tables[wName];
296
+ }
297
+ }
298
+ return Object.assign({}, manifest, { tables: wantedTables });
299
+ };
300
+ exports.narrowManifestToTables = narrowManifestToTables;
275
301
  const syncManifest = async (scope, options = {}) => {
276
302
  // Top-level entry owns the collector lifecycle. Recursive calls (all-scopes
277
303
  // → per-scope) inherit the collector via options._benchmarkCollector.
@@ -333,10 +359,31 @@ const syncManifest = async (scope, options = {}) => {
333
359
  }
334
360
  const refreshTableCount = Object.keys(newManifest.tables).length;
335
361
  FileLogger_1.fileLogger.debug("Refreshed manifest for " + scope + ": " + refreshTableCount + " tables");
362
+ // The manifest is always written in FULL. Narrowing it to the --table
363
+ // subset would drop every other table from dove.manifest.<scope>.json and
364
+ // break push/watch, so the filter applies only to the file download below.
336
365
  await fUtils.writeScopeManifest(scope, newManifest);
366
+ // --table gate: refresh file content for only the requested tables. Any
367
+ // table outside this scope's `_tables` whitelist was already dropped above,
368
+ // so intersecting the manifest is enough. `newManifest` is left intact —
369
+ // see narrowManifestToTables.
370
+ var refreshManifest = (0, exports.narrowManifestToTables)(newManifest, options.tables);
371
+ if (options.tables && options.tables.length > 0) {
372
+ var wantedNames = Object.keys(refreshManifest.tables);
373
+ if (wantedNames.length === 0) {
374
+ // Expected on every non-matching scope of an all-scopes filtered
375
+ // refresh, so this is debug — at info it would emit one noise line per
376
+ // scope that simply doesn't hold the table you asked for.
377
+ FileLogger_1.fileLogger.debug("syncManifest: scope " + scope + " holds none of the requested tables — skipping file refresh");
378
+ }
379
+ else {
380
+ Logger_1.logger.info("Refreshing " + wantedNames.length + " of " + refreshTableCount + " tables in " +
381
+ scope + " (--table): " + wantedNames.join(", "));
382
+ }
383
+ }
337
384
  if (collector)
338
385
  collector.startScope(scope);
339
- await (0, exports.refreshAllFiles)(newManifest, scopeSourcePath, {
386
+ await (0, exports.refreshAllFiles)(refreshManifest, scopeSourcePath, {
340
387
  force: options.force,
341
388
  benchmarkCollector: collector,
342
389
  });
@@ -352,6 +399,7 @@ const syncManifest = async (scope, options = {}) => {
352
399
  // scopes that leaked in before the whitelist gate existed.
353
400
  var childOptions = {
354
401
  force: options.force,
402
+ tables: options.tables,
355
403
  _benchmarkCollector: collector,
356
404
  };
357
405
  if (declaredScopes.length > 0) {
package/dist/commander.js CHANGED
@@ -79,6 +79,13 @@ async function initCommands() {
79
79
  type: "string",
80
80
  describe: "Refresh a single scope (default: all declared scopes)",
81
81
  },
82
+ table: {
83
+ alias: "t",
84
+ type: "array",
85
+ describe: "Refresh file content for only these tables (repeatable or comma-separated). " +
86
+ "Must already be synced via includes._tables or includes._scopes.<scope>._tables. " +
87
+ "Default: every synced table.",
88
+ },
82
89
  benchmark: {
83
90
  alias: "b",
84
91
  type: "boolean",
package/dist/commands.js CHANGED
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.setLogLevel = setLogLevel;
40
+ exports.normalizeTableFilter = normalizeTableFilter;
41
+ exports.unknownTablesForScopes = unknownTablesForScopes;
40
42
  exports.refreshCommand = refreshCommand;
41
43
  exports.pushCommand = pushCommand;
42
44
  exports.downloadCommand = downloadCommand;
@@ -61,15 +63,73 @@ const fs = __importStar(require("fs"));
61
63
  function setLogLevel(args) {
62
64
  Logger_1.logger.setLogLevel(args.logLevel);
63
65
  }
66
+ /**
67
+ * Normalize the --table flag into a clean list. yargs `type: "array"` already
68
+ * yields a string[], but accept comma-separated values too (`-t a,b`) so the
69
+ * flag behaves the way people reflexively type it.
70
+ */
71
+ function normalizeTableFilter(raw) {
72
+ if (!raw)
73
+ return [];
74
+ const parts = Array.isArray(raw) ? raw : [raw];
75
+ const out = [];
76
+ for (const part of parts) {
77
+ for (const piece of String(part).split(",")) {
78
+ const name = piece.trim();
79
+ if (name !== "" && out.indexOf(name) === -1)
80
+ out.push(name);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Requested tables that no target scope actually syncs. A typo here would
87
+ * otherwise refresh nothing and look like a successful no-op, so the caller
88
+ * turns a non-empty result into a hard failure.
89
+ */
90
+ function unknownTablesForScopes(tables, scope) {
91
+ const config = ConfigManager.getConfig();
92
+ const declaredScopes = scope ? [scope] : Object.keys(config.scopes || {});
93
+ // A project with no declared `scopes` (single-scope / legacy config) still
94
+ // refreshes — syncManifest falls back to the manifest's scopes. resolveConfigForScope
95
+ // on an unrecognized name yields the GLOBAL `_tables` whitelist, which is the
96
+ // right allow-list there. Without this fallback the validation below would be
97
+ // stricter than the refresh it guards and reject every table outright.
98
+ const scopeNames = declaredScopes.length > 0 ? declaredScopes : [""];
99
+ const allowed = [];
100
+ for (const scopeName of scopeNames) {
101
+ const resolved = ConfigManager.resolveConfigForScope(scopeName);
102
+ for (const table of resolved.tables || []) {
103
+ if (allowed.indexOf(table) === -1)
104
+ allowed.push(table);
105
+ }
106
+ }
107
+ return tables.filter((t) => allowed.indexOf(t) === -1);
108
+ }
64
109
  async function refreshCommand(args, log = true) {
65
110
  setLogLevel(args);
66
111
  try {
67
112
  if (!log)
68
113
  setLogLevel({ logLevel: "warn" });
69
- FileLogger_1.fileLogger.debug("Syncing manifest from instance (force=" + !!args.force + ", benchmark=" + !!args.benchmark + ")");
114
+ const tables = normalizeTableFilter(args.table);
115
+ if (tables.length > 0) {
116
+ const unknown = unknownTablesForScopes(tables, args.scope);
117
+ if (unknown.length > 0) {
118
+ const where = args.scope ? "scope '" + args.scope + "'" : "any configured scope";
119
+ const noun = unknown.length === 1 ? "table" : "tables";
120
+ const verb = unknown.length === 1 ? "is" : "are";
121
+ const pronoun = unknown.length === 1 ? "it" : "them";
122
+ throw new Error("--table: " + noun + " " + unknown.join(", ") + " " + verb + " not synced by " + where + ". " +
123
+ "Add " + pronoun + " to includes._tables (or includes._scopes.<scope>._tables) in dove.config.js first.");
124
+ }
125
+ }
126
+ FileLogger_1.fileLogger.debug("Syncing manifest from instance (force=" + !!args.force +
127
+ ", benchmark=" + !!args.benchmark +
128
+ ", tables=" + (tables.length > 0 ? tables.join(",") : "all") + ")");
70
129
  await AppUtils.syncManifest(args.scope, {
71
130
  force: !!args.force,
72
131
  benchmark: !!args.benchmark,
132
+ tables: tables.length > 0 ? tables : undefined,
73
133
  });
74
134
  Logger_1.logger.success("Refresh complete!");
75
135
  setLogLevel(args);
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for `dove refresh --table` — narrow the file refresh to specific tables.
4
+ *
5
+ * Validates:
6
+ * - normalizeTableFilter accepts repeated flags AND comma-separated values, trimming + de-duping
7
+ * - unknownTablesForScopes resolves against the target scope, or the union of all scopes
8
+ * - refreshCommand HARD-FAILS on a table that no scope syncs (a typo must not look like a no-op)
9
+ * - refreshCommand forwards the table list to syncManifest, and forwards undefined when unfiltered
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ const commands_1 = require("../commands");
46
+ const AppUtils = __importStar(require("../appUtils"));
47
+ const ConfigManager = __importStar(require("../config"));
48
+ // --- Mock setup (mirrors taskClear.test.ts) ---
49
+ var logMessages = [];
50
+ jest.mock("../Logger", function () {
51
+ return {
52
+ logger: {
53
+ setLogLevel: jest.fn(),
54
+ success: jest.fn(function (msg) {
55
+ logMessages.push({ level: "success", msg: msg });
56
+ }),
57
+ info: jest.fn(function (msg) {
58
+ logMessages.push({ level: "info", msg: msg });
59
+ }),
60
+ error: jest.fn(function (msg) {
61
+ logMessages.push({ level: "error", msg: msg });
62
+ }),
63
+ warn: jest.fn(function (msg) {
64
+ logMessages.push({ level: "warn", msg: msg });
65
+ }),
66
+ debug: jest.fn(),
67
+ getInternalLogger: jest.fn(function () {
68
+ return { error: jest.fn() };
69
+ }),
70
+ },
71
+ };
72
+ });
73
+ jest.mock("../config", function () {
74
+ return {
75
+ getConfig: jest.fn(),
76
+ resolveConfigForScope: jest.fn(),
77
+ loadConfigs: jest.fn(),
78
+ };
79
+ });
80
+ jest.mock("../appUtils", function () {
81
+ return { syncManifest: jest.fn() };
82
+ });
83
+ jest.mock("../snClient", function () {
84
+ return { defaultClient: jest.fn(), unwrapSNResponse: jest.fn() };
85
+ });
86
+ jest.mock("../FileLogger", function () {
87
+ return { fileLogger: { debug: jest.fn() } };
88
+ });
89
+ jest.mock("../logMessages", function () {
90
+ return { logPushResults: jest.fn(), logBuildResults: jest.fn() };
91
+ });
92
+ jest.mock("../gitUtils", function () {
93
+ return { gitDiffToEncodedPaths: jest.fn() };
94
+ });
95
+ jest.mock("../FileUtils", function () {
96
+ return { encodedPathsToFilePaths: jest.fn() };
97
+ });
98
+ jest.mock("inquirer", function () {
99
+ return { prompt: jest.fn() };
100
+ });
101
+ var mockConfig = ConfigManager;
102
+ var mockAppUtils = AppUtils;
103
+ // Two scopes, each syncing a different extra table — enough to prove the
104
+ // union-vs-single-scope resolution.
105
+ function stubTwoScopes() {
106
+ mockConfig.getConfig.mockReturnValue({
107
+ scopes: { x_cadso_journey: {}, x_cadso_automate: {} },
108
+ });
109
+ mockConfig.resolveConfigForScope.mockImplementation(function (scopeName) {
110
+ var extra = scopeName === "x_cadso_journey"
111
+ ? "x_cadso_journey_action"
112
+ : "x_cadso_core_setting";
113
+ return { tables: ["sys_script", extra] };
114
+ });
115
+ }
116
+ var defaultArgs = { logLevel: "info" };
117
+ describe("normalizeTableFilter", function () {
118
+ it("returns an empty list when the flag is absent", function () {
119
+ expect((0, commands_1.normalizeTableFilter)(undefined)).toEqual([]);
120
+ expect((0, commands_1.normalizeTableFilter)("")).toEqual([]);
121
+ });
122
+ it("accepts a single string", function () {
123
+ expect((0, commands_1.normalizeTableFilter)("sys_script")).toEqual(["sys_script"]);
124
+ });
125
+ it("accepts a repeated flag (yargs array)", function () {
126
+ expect((0, commands_1.normalizeTableFilter)(["sys_script", "sys_ui_action"])).toEqual([
127
+ "sys_script",
128
+ "sys_ui_action",
129
+ ]);
130
+ });
131
+ it("splits comma-separated values", function () {
132
+ expect((0, commands_1.normalizeTableFilter)("sys_script,sys_ui_action")).toEqual([
133
+ "sys_script",
134
+ "sys_ui_action",
135
+ ]);
136
+ });
137
+ it("trims whitespace and de-dupes", function () {
138
+ expect((0, commands_1.normalizeTableFilter)([" sys_script , sys_ui_action ", "sys_script"])).toEqual([
139
+ "sys_script",
140
+ "sys_ui_action",
141
+ ]);
142
+ });
143
+ });
144
+ describe("unknownTablesForScopes", function () {
145
+ beforeEach(function () {
146
+ logMessages = [];
147
+ jest.clearAllMocks();
148
+ stubTwoScopes();
149
+ });
150
+ it("accepts a table synced by the named scope", function () {
151
+ expect((0, commands_1.unknownTablesForScopes)(["x_cadso_journey_action"], "x_cadso_journey")).toEqual([]);
152
+ });
153
+ it("rejects a table that belongs to a DIFFERENT scope when one is named", function () {
154
+ expect((0, commands_1.unknownTablesForScopes)(["x_cadso_core_setting"], "x_cadso_journey")).toEqual(["x_cadso_core_setting"]);
155
+ });
156
+ it("accepts a table from any scope when no scope is named", function () {
157
+ expect((0, commands_1.unknownTablesForScopes)(["x_cadso_core_setting"])).toEqual([]);
158
+ });
159
+ it("reports tables no scope syncs", function () {
160
+ expect((0, commands_1.unknownTablesForScopes)(["sys_script", "nope_not_a_table"])).toEqual([
161
+ "nope_not_a_table",
162
+ ]);
163
+ });
164
+ // Regression: a project with no declared `scopes` (single-scope / legacy
165
+ // config) still refreshes — syncManifest falls back to the manifest's scopes.
166
+ // Validating against an empty scope list would make the allow-list empty and
167
+ // reject EVERY table, i.e. be stricter than the refresh it guards.
168
+ it("falls back to the global _tables when the config declares no scopes", function () {
169
+ mockConfig.getConfig.mockReturnValue({}); // no `scopes` key
170
+ mockConfig.resolveConfigForScope.mockImplementation(function () {
171
+ return { tables: ["sys_script", "sys_script_include"] };
172
+ });
173
+ expect((0, commands_1.unknownTablesForScopes)(["sys_script"])).toEqual([]);
174
+ expect((0, commands_1.unknownTablesForScopes)(["nope_not_a_table"])).toEqual(["nope_not_a_table"]);
175
+ });
176
+ });
177
+ describe("refreshCommand --table", function () {
178
+ beforeEach(function () {
179
+ logMessages = [];
180
+ jest.clearAllMocks();
181
+ stubTwoScopes();
182
+ mockAppUtils.syncManifest.mockResolvedValue(undefined);
183
+ });
184
+ it("hard-fails on an unsynced table and does NOT refresh", async function () {
185
+ await expect((0, commands_1.refreshCommand)({
186
+ ...defaultArgs,
187
+ scope: "x_cadso_journey",
188
+ table: ["nope_not_a_table"],
189
+ })).rejects.toThrow(/nope_not_a_table is not synced by scope 'x_cadso_journey'/);
190
+ expect(mockAppUtils.syncManifest).not.toHaveBeenCalled();
191
+ });
192
+ it("forwards the normalized table list to syncManifest", async function () {
193
+ await (0, commands_1.refreshCommand)({
194
+ ...defaultArgs,
195
+ scope: "x_cadso_journey",
196
+ table: "x_cadso_journey_action",
197
+ });
198
+ expect(mockAppUtils.syncManifest).toHaveBeenCalledWith("x_cadso_journey", expect.objectContaining({ tables: ["x_cadso_journey_action"] }));
199
+ });
200
+ it("forwards undefined when no --table is given (unfiltered default)", async function () {
201
+ await (0, commands_1.refreshCommand)({ ...defaultArgs, scope: "x_cadso_journey" });
202
+ expect(mockAppUtils.syncManifest).toHaveBeenCalledWith("x_cadso_journey", expect.objectContaining({ tables: undefined }));
203
+ });
204
+ });
@@ -67,6 +67,10 @@ var mockFileLogger = {
67
67
  };
68
68
  var mockClient = {
69
69
  getManifest: jest.fn(),
70
+ // refreshAllFiles asks for file content per table. Returning {} means "no
71
+ // files to write", which keeps these tests off the plugin/write path while
72
+ // still recording WHICH tables were asked for.
73
+ getMissingFiles: jest.fn().mockResolvedValue({}),
70
74
  };
71
75
  var mockFUtils = {
72
76
  writeScopeManifest: jest.fn().mockResolvedValue(undefined),
@@ -190,3 +194,111 @@ describe("syncManifest — scope + table whitelist gates", function () {
190
194
  expect(refreshedScopes).not.toContain("x_cadso_ti_agile");
191
195
  });
192
196
  });
197
+ /**
198
+ * `dove refresh --table` narrows the FILE refresh only.
199
+ *
200
+ * The load-bearing invariant is that the PERSISTED manifest stays complete:
201
+ * narrowing dove.manifest.<scope>.json to the requested table would drop every
202
+ * other table from it and break push/watch. These tests exist so that a future
203
+ * "optimization" that narrows newManifest fails loudly instead of silently
204
+ * corrupting the manifest.
205
+ */
206
+ describe("syncManifest — --table filter", function () {
207
+ function recordWithFile(name, sysId) {
208
+ return { records: { [name]: { name: name, sys_id: sysId, files: [{ name: "script", type: "js" }] } } };
209
+ }
210
+ beforeEach(function () {
211
+ jest.clearAllMocks();
212
+ mockClient.getMissingFiles.mockResolvedValue({});
213
+ mockConfig.isMultiScopeManifest.mockReturnValue(true);
214
+ mockConfig.resolveConfigForScope.mockImplementation(function (_scope) {
215
+ return {
216
+ tables: ["sys_script_include", "sys_script", "sys_ux_macroponent"],
217
+ fieldOverrides: {},
218
+ apiIncludes: {},
219
+ apiExcludes: {},
220
+ };
221
+ });
222
+ mockConfig.getConfig.mockReturnValue({ scopes: { x_cadso_core: {} } });
223
+ mockConfig.getManifest.mockResolvedValue({
224
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
225
+ });
226
+ mockClient.getManifest.mockResolvedValue({
227
+ scope: "x_cadso_core",
228
+ tables: {
229
+ sys_script_include: recordWithFile("FooInclude", "a1"),
230
+ sys_script: recordWithFile("BarBR", "a2"),
231
+ sys_ux_macroponent: recordWithFile("BazComp", "a3"),
232
+ },
233
+ });
234
+ });
235
+ test("does NOT narrow the persisted manifest — all whitelisted tables still written", async function () {
236
+ await AppUtils.syncManifest("x_cadso_core", { tables: ["sys_script"] });
237
+ expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(1);
238
+ var writtenTables = Object.keys(mockFUtils.writeScopeManifest.mock.calls[0][1].tables);
239
+ // The manifest keeps every whitelisted table, not just the requested one.
240
+ expect(writtenTables.sort()).toEqual([
241
+ "sys_script",
242
+ "sys_script_include",
243
+ "sys_ux_macroponent",
244
+ ]);
245
+ });
246
+ test("narrows the file refresh to the requested tables only", async function () {
247
+ await AppUtils.syncManifest("x_cadso_core", { tables: ["sys_script"] });
248
+ expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
249
+ var askedFor = Object.keys(mockClient.getMissingFiles.mock.calls[0][0]);
250
+ expect(askedFor).toEqual(["sys_script"]);
251
+ });
252
+ test("refreshes every table when no --table filter is given", async function () {
253
+ await AppUtils.syncManifest("x_cadso_core", {});
254
+ var askedFor = Object.keys(mockClient.getMissingFiles.mock.calls[0][0]);
255
+ expect(askedFor.sort()).toEqual([
256
+ "sys_script",
257
+ "sys_script_include",
258
+ "sys_ux_macroponent",
259
+ ]);
260
+ });
261
+ test("a scope holding none of the requested tables still writes its manifest, but refreshes no files", async function () {
262
+ await AppUtils.syncManifest("x_cadso_core", { tables: ["sys_ui_action"] });
263
+ // Manifest untouched-in-full …
264
+ expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(1);
265
+ expect(Object.keys(mockFUtils.writeScopeManifest.mock.calls[0][1].tables)).toHaveLength(3);
266
+ // … and not a single file downloaded.
267
+ expect(mockClient.getMissingFiles).not.toHaveBeenCalled();
268
+ });
269
+ });
270
+ describe("narrowManifestToTables", function () {
271
+ var manifest;
272
+ beforeEach(function () {
273
+ manifest = {
274
+ scope: "x_cadso_core",
275
+ tables: {
276
+ sys_script: { records: {} },
277
+ sys_script_include: { records: {} },
278
+ },
279
+ };
280
+ });
281
+ test("returns the SAME manifest when no tables are requested", function () {
282
+ expect(AppUtils.narrowManifestToTables(manifest, undefined)).toBe(manifest);
283
+ expect(AppUtils.narrowManifestToTables(manifest, [])).toBe(manifest);
284
+ });
285
+ test("returns a NEW manifest and never mutates the input", function () {
286
+ var narrowed = AppUtils.narrowManifestToTables(manifest, ["sys_script"]);
287
+ expect(narrowed).not.toBe(manifest);
288
+ expect(Object.keys(narrowed.tables)).toEqual(["sys_script"]);
289
+ // The corruption guard: the caller still writes `manifest` to disk in full.
290
+ expect(Object.keys(manifest.tables).sort()).toEqual([
291
+ "sys_script",
292
+ "sys_script_include",
293
+ ]);
294
+ });
295
+ test("yields an empty table map when nothing matches", function () {
296
+ var narrowed = AppUtils.narrowManifestToTables(manifest, ["sys_ui_action"]);
297
+ expect(Object.keys(narrowed.tables)).toEqual([]);
298
+ expect(narrowed.scope).toBe("x_cadso_core");
299
+ });
300
+ test("ignores requested tables the scope does not hold", function () {
301
+ var narrowed = AppUtils.narrowManifestToTables(manifest, ["sys_script", "sys_ui_action"]);
302
+ expect(Object.keys(narrowed.tables)).toEqual(["sys_script"]);
303
+ });
304
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.109",
3
+ "version": "0.0.111",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "typescript": "^5.2.2"
39
39
  },
40
40
  "dependencies": {
41
- "@tenonhq/dovetail-clickup": "~0.0.12",
41
+ "@tenonhq/dovetail-clickup": "~0.0.13",
42
42
  "@tenonhq/dovetail-schema": "~0.0.12",
43
43
  "axios": "^1.5.1",
44
44
  "axios-cookiejar-support": "^4.0.7",
@@ -1,7 +1,171 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-06-23T19:16:43.134Z",
3
+ "generated_at": "2026-07-10T18:41:37.097Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-deploy@0.0.2",
8
+ "package": "@tenonhq/dovetail-deploy",
9
+ "version": "0.0.2",
10
+ "prev_version": "0.0.1",
11
+ "published_at": "2026-07-10T18:41:37.089Z",
12
+ "git": {
13
+ "sha": "58c416a421d7b9eaff3483468601cc1eda753b20",
14
+ "tag": "dovetail-deploy@0.0.2"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "58c416a42",
20
+ "subject": "feat(deploy): dynamic per-developer promotion source (#211)",
21
+ "pr": 211
22
+ }
23
+ ],
24
+ "exports_delta": null,
25
+ "narrative": null
26
+ },
27
+ {
28
+ "$schema": "dovetail-release-event/v1",
29
+ "event_id": "dovetail-servicenow@0.0.26",
30
+ "package": "@tenonhq/dovetail-servicenow",
31
+ "version": "0.0.26",
32
+ "prev_version": "0.0.25",
33
+ "published_at": "2026-07-10T16:27:58.460Z",
34
+ "git": {
35
+ "sha": "9b3b58b8fb9aa53b32473c8c7711446f5a52a576",
36
+ "tag": "dovetail-servicenow@0.0.26"
37
+ },
38
+ "semver_bump": "patch",
39
+ "commits": [
40
+ {
41
+ "sha": "9b3b58b8f",
42
+ "subject": "feat: invoke arbitrary authenticated Scripted REST operations (invoke-rest) (#214)",
43
+ "pr": 214
44
+ }
45
+ ],
46
+ "exports_delta": null,
47
+ "narrative": null
48
+ },
49
+ {
50
+ "$schema": "dovetail-release-event/v1",
51
+ "event_id": "dovetail-mcp@0.0.23",
52
+ "package": "@tenonhq/dovetail-mcp",
53
+ "version": "0.0.23",
54
+ "prev_version": "0.0.22",
55
+ "published_at": "2026-07-10T16:27:58.460Z",
56
+ "git": {
57
+ "sha": "9b3b58b8fb9aa53b32473c8c7711446f5a52a576",
58
+ "tag": "dovetail-mcp@0.0.23"
59
+ },
60
+ "semver_bump": "patch",
61
+ "commits": [
62
+ {
63
+ "sha": "534f517e4",
64
+ "subject": "chore(release): publish 3 packages [skip ci]"
65
+ }
66
+ ],
67
+ "exports_delta": null,
68
+ "narrative": null
69
+ },
70
+ {
71
+ "$schema": "dovetail-release-event/v1",
72
+ "event_id": "dovetail-dashboard@0.0.48",
73
+ "package": "@tenonhq/dovetail-dashboard",
74
+ "version": "0.0.48",
75
+ "prev_version": "0.0.47",
76
+ "published_at": "2026-07-08T21:55:56.082Z",
77
+ "git": {
78
+ "sha": "7ef92071de6726ccfd751ae617a661f95fe685fc",
79
+ "tag": "dovetail-dashboard@0.0.48"
80
+ },
81
+ "semver_bump": "patch",
82
+ "commits": [
83
+ {
84
+ "sha": "7ef92071d",
85
+ "subject": "feat(ci): Slack alerts for publish success/failure + cut dashboard release (#209)",
86
+ "pr": 209
87
+ }
88
+ ],
89
+ "exports_delta": null,
90
+ "narrative": null
91
+ },
92
+ {
93
+ "$schema": "dovetail-release-event/v1",
94
+ "event_id": "dovetail-clickup@0.0.13",
95
+ "package": "@tenonhq/dovetail-clickup",
96
+ "version": "0.0.13",
97
+ "prev_version": "0.0.12",
98
+ "published_at": "2026-07-08T18:19:16.768Z",
99
+ "git": {
100
+ "sha": "84fc7a200e0840aaa4b2c93d6764dce959bb6536",
101
+ "tag": "dovetail-clickup@0.0.13"
102
+ },
103
+ "semver_bump": "patch",
104
+ "commits": [
105
+ {
106
+ "sha": "84fc7a200",
107
+ "subject": "fix: clickup get_task resolves custom ids instead of a 401 (#208)",
108
+ "pr": 208
109
+ },
110
+ {
111
+ "sha": "4bc78bc61",
112
+ "subject": "chore(release): publish 2 packages [skip ci]"
113
+ },
114
+ {
115
+ "sha": "c82439b30",
116
+ "subject": "chore(deps): bump js-yaml, jest and ts-jest (#183)",
117
+ "pr": 183
118
+ },
119
+ {
120
+ "sha": "9332fdf7a",
121
+ "subject": "chore(release): publish 20 packages [skip ci]"
122
+ },
123
+ {
124
+ "sha": "9046da774",
125
+ "subject": "chore(release): publish 19 packages [skip ci]"
126
+ }
127
+ ],
128
+ "exports_delta": null,
129
+ "narrative": null
130
+ },
131
+ {
132
+ "$schema": "dovetail-release-event/v1",
133
+ "event_id": "dovetail-core@0.0.110",
134
+ "package": "@tenonhq/dovetail-core",
135
+ "version": "0.0.110",
136
+ "prev_version": "0.0.109",
137
+ "published_at": "2026-07-08T18:19:16.768Z",
138
+ "git": {
139
+ "sha": "84fc7a200e0840aaa4b2c93d6764dce959bb6536",
140
+ "tag": "dovetail-core@0.0.110"
141
+ },
142
+ "semver_bump": "patch",
143
+ "commits": [],
144
+ "exports_delta": null,
145
+ "narrative": null
146
+ },
147
+ {
148
+ "$schema": "dovetail-release-event/v1",
149
+ "event_id": "dovetail-mcp@0.0.22",
150
+ "package": "@tenonhq/dovetail-mcp",
151
+ "version": "0.0.22",
152
+ "prev_version": "0.0.21",
153
+ "published_at": "2026-07-08T18:19:16.768Z",
154
+ "git": {
155
+ "sha": "84fc7a200e0840aaa4b2c93d6764dce959bb6536",
156
+ "tag": "dovetail-mcp@0.0.22"
157
+ },
158
+ "semver_bump": "patch",
159
+ "commits": [
160
+ {
161
+ "sha": "84fc7a200",
162
+ "subject": "fix: clickup get_task resolves custom ids instead of a 401 (#208)",
163
+ "pr": 208
164
+ }
165
+ ],
166
+ "exports_delta": null,
167
+ "narrative": null
168
+ },
5
169
  {
6
170
  "$schema": "dovetail-release-event/v1",
7
171
  "event_id": "dovetail-dashboard@0.0.44",
@@ -1105,204 +1269,6 @@
1105
1269
  ],
1106
1270
  "exports_delta": null,
1107
1271
  "narrative": null
1108
- },
1109
- {
1110
- "$schema": "dovetail-release-event/v1",
1111
- "event_id": "dovetail-sass-plugin@0.0.11",
1112
- "package": "@tenonhq/dovetail-sass-plugin",
1113
- "version": "0.0.11",
1114
- "prev_version": "0.0.10",
1115
- "published_at": "2026-06-05T06:43:18.252Z",
1116
- "git": {
1117
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1118
- "tag": "dovetail-sass-plugin@0.0.11"
1119
- },
1120
- "semver_bump": "patch",
1121
- "commits": [
1122
- {
1123
- "sha": "71bdf9bba",
1124
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1125
- "pr": 148
1126
- },
1127
- {
1128
- "sha": "9332fdf7a",
1129
- "subject": "chore(release): publish 20 packages [skip ci]"
1130
- },
1131
- {
1132
- "sha": "9046da774",
1133
- "subject": "chore(release): publish 19 packages [skip ci]"
1134
- }
1135
- ],
1136
- "exports_delta": null,
1137
- "narrative": null
1138
- },
1139
- {
1140
- "$schema": "dovetail-release-event/v1",
1141
- "event_id": "dovetail-typescript-plugin@0.0.11",
1142
- "package": "@tenonhq/dovetail-typescript-plugin",
1143
- "version": "0.0.11",
1144
- "prev_version": "0.0.10",
1145
- "published_at": "2026-06-05T06:43:18.252Z",
1146
- "git": {
1147
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1148
- "tag": "dovetail-typescript-plugin@0.0.11"
1149
- },
1150
- "semver_bump": "patch",
1151
- "commits": [
1152
- {
1153
- "sha": "71bdf9bba",
1154
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1155
- "pr": 148
1156
- },
1157
- {
1158
- "sha": "9332fdf7a",
1159
- "subject": "chore(release): publish 20 packages [skip ci]"
1160
- },
1161
- {
1162
- "sha": "9046da774",
1163
- "subject": "chore(release): publish 19 packages [skip ci]"
1164
- }
1165
- ],
1166
- "exports_delta": null,
1167
- "narrative": null
1168
- },
1169
- {
1170
- "$schema": "dovetail-release-event/v1",
1171
- "event_id": "dovetail-webpack-plugin@0.0.10",
1172
- "package": "@tenonhq/dovetail-webpack-plugin",
1173
- "version": "0.0.10",
1174
- "prev_version": "0.0.9",
1175
- "published_at": "2026-06-05T06:43:18.252Z",
1176
- "git": {
1177
- "sha": "71bdf9bba8bd5e3849a212a18d9ebc9241232b32",
1178
- "tag": "dovetail-webpack-plugin@0.0.10"
1179
- },
1180
- "semver_bump": "patch",
1181
- "commits": [
1182
- {
1183
- "sha": "71bdf9bba",
1184
- "subject": "fix: float internal dep ranges to ~0.0.x so consumers need no overrides (#148)",
1185
- "pr": 148
1186
- },
1187
- {
1188
- "sha": "9332fdf7a",
1189
- "subject": "chore(release): publish 20 packages [skip ci]"
1190
- },
1191
- {
1192
- "sha": "9046da774",
1193
- "subject": "chore(release): publish 19 packages [skip ci]"
1194
- }
1195
- ],
1196
- "exports_delta": null,
1197
- "narrative": null
1198
- },
1199
- {
1200
- "$schema": "dovetail-release-event/v1",
1201
- "event_id": "dovetail-servicenow@0.0.15",
1202
- "package": "@tenonhq/dovetail-servicenow",
1203
- "version": "0.0.15",
1204
- "prev_version": "0.0.14",
1205
- "published_at": "2026-06-04T22:16:51.160Z",
1206
- "git": {
1207
- "sha": "d46b5bd2dce0fb3b13f0d1d5933f88d1d421b550",
1208
- "tag": "dovetail-servicenow@0.0.15"
1209
- },
1210
- "semver_bump": "patch",
1211
- "commits": [
1212
- {
1213
- "sha": "d46b5bd2d",
1214
- "subject": "feat(servicenow): dove-sn edit-action — headless action-type script/output edit (#147)",
1215
- "pr": 147
1216
- },
1217
- {
1218
- "sha": "af1417796",
1219
- "subject": "chore(release): publish 3 packages [skip ci]"
1220
- }
1221
- ],
1222
- "exports_delta": null,
1223
- "narrative": null
1224
- },
1225
- {
1226
- "$schema": "dovetail-release-event/v1",
1227
- "event_id": "dovetail-claude-plans@0.0.21",
1228
- "package": "@tenonhq/dovetail-claude-plans",
1229
- "version": "0.0.21",
1230
- "prev_version": "0.0.20",
1231
- "published_at": "2026-06-04T08:56:53.804Z",
1232
- "git": {
1233
- "sha": "44e5c84b85007cf6e9223b279ff79afd5319c5ab",
1234
- "tag": "dovetail-claude-plans@0.0.21"
1235
- },
1236
- "semver_bump": "patch",
1237
- "commits": [
1238
- {
1239
- "sha": "44e5c84b8",
1240
- "subject": "chore(docs): neutralize upstream operating-repo references in package docs (#145)",
1241
- "pr": 145
1242
- },
1243
- {
1244
- "sha": "92213e1f8",
1245
- "subject": "chore(release): publish 2 packages [skip ci]"
1246
- }
1247
- ],
1248
- "exports_delta": null,
1249
- "narrative": null
1250
- },
1251
- {
1252
- "$schema": "dovetail-release-event/v1",
1253
- "event_id": "dovetail-servicenow@0.0.14",
1254
- "package": "@tenonhq/dovetail-servicenow",
1255
- "version": "0.0.14",
1256
- "prev_version": "0.0.13",
1257
- "published_at": "2026-06-04T08:56:53.804Z",
1258
- "git": {
1259
- "sha": "44e5c84b85007cf6e9223b279ff79afd5319c5ab",
1260
- "tag": "dovetail-servicenow@0.0.14"
1261
- },
1262
- "semver_bump": "patch",
1263
- "commits": [
1264
- {
1265
- "sha": "44e5c84b8",
1266
- "subject": "chore(docs): neutralize upstream operating-repo references in package docs (#145)",
1267
- "pr": 145
1268
- },
1269
- {
1270
- "sha": "e0cf83a8c",
1271
- "subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.13 [skip ci]"
1272
- }
1273
- ],
1274
- "exports_delta": null,
1275
- "narrative": null
1276
- },
1277
- {
1278
- "$schema": "dovetail-release-event/v1",
1279
- "event_id": "dovetail-mcp@0.0.8",
1280
- "package": "@tenonhq/dovetail-mcp",
1281
- "version": "0.0.8",
1282
- "prev_version": "0.0.7",
1283
- "published_at": "2026-06-04T08:56:53.804Z",
1284
- "git": {
1285
- "sha": "44e5c84b85007cf6e9223b279ff79afd5319c5ab",
1286
- "tag": "dovetail-mcp@0.0.8"
1287
- },
1288
- "semver_bump": "patch",
1289
- "commits": [
1290
- {
1291
- "sha": "44e5c84b8",
1292
- "subject": "chore(docs): neutralize upstream operating-repo references in package docs (#145)",
1293
- "pr": 145
1294
- },
1295
- {
1296
- "sha": "bd127c726",
1297
- "subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.12 [skip ci]"
1298
- },
1299
- {
1300
- "sha": "448696846",
1301
- "subject": "chore(release): publish 3 packages [skip ci]"
1302
- }
1303
- ],
1304
- "exports_delta": null,
1305
- "narrative": null
1306
1272
  }
1307
1273
  ]
1308
1274
  }