@deftai/directive-core 0.84.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/fetch.d.ts +17 -0
- package/dist/cache/fetch.js +50 -0
- package/dist/doctor/checks.js +16 -1
- package/dist/doctor/constants.d.ts +4 -2
- package/dist/doctor/constants.js +10 -6
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.d.ts +2 -1
- package/dist/doctor/main.js +83 -22
- package/dist/doctor/npm-registry.d.ts +23 -0
- package/dist/doctor/npm-registry.js +128 -0
- package/dist/doctor/npm-view.d.ts +10 -0
- package/dist/doctor/npm-view.js +31 -0
- package/dist/doctor/payload-staleness.js +2 -9
- package/dist/doctor/signpost-checks.d.ts +2 -1
- package/dist/doctor/signpost-checks.js +2 -0
- package/dist/doctor/types.d.ts +12 -0
- package/dist/eval-health-relocation/evaluate.js +15 -1
- package/dist/fs/projection-containment.js +1 -1
- package/dist/hooks/cursor-hooks.d.ts +6 -20
- package/dist/hooks/cursor-hooks.js +7 -92
- package/dist/init-deposit/agent-hooks.d.ts +2 -2
- package/dist/init-deposit/agent-hooks.js +24 -38
- package/dist/init-deposit/gitignore.js +5 -0
- package/dist/init-deposit/hygiene.d.ts +22 -0
- package/dist/init-deposit/hygiene.js +136 -4
- package/dist/init-deposit/legacy-detect.d.ts +25 -1
- package/dist/init-deposit/legacy-detect.js +143 -11
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +18 -5
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/org-force-on-migration.d.ts +44 -0
- package/dist/policy/org-force-on-migration.js +242 -0
- package/dist/policy/product-signal.d.ts +3 -1
- package/dist/policy/product-signal.js +15 -4
- package/dist/policy/value-feedback.d.ts +18 -1
- package/dist/policy/value-feedback.js +78 -5
- package/dist/preflight-cache/evaluate.js +12 -1
- package/dist/product-signal/consent-prompt.d.ts +13 -0
- package/dist/product-signal/consent-prompt.js +30 -0
- package/dist/product-signal/submit.js +22 -1
- package/dist/render/framework-commands.d.ts +1 -1
- package/dist/render/framework-commands.js +6 -6
- package/dist/render/index.d.ts +1 -0
- package/dist/render/index.js +1 -0
- package/dist/render/roadmap-render.d.ts +5 -1
- package/dist/render/roadmap-render.js +20 -2
- package/dist/render/rule-map-template.d.ts +2 -0
- package/dist/render/rule-map-template.js +407 -0
- package/dist/render/rule-map.d.ts +2 -0
- package/dist/render/rule-map.js +708 -0
- package/dist/review-monitor/constants.d.ts +7 -1
- package/dist/review-monitor/constants.js +40 -14
- package/dist/review-monitor/github-lease.d.ts +45 -0
- package/dist/review-monitor/github-lease.js +106 -0
- package/dist/review-monitor/index.d.ts +2 -0
- package/dist/review-monitor/index.js +2 -0
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +152 -0
- package/dist/review-monitor/record.d.ts +61 -13
- package/dist/review-monitor/record.js +379 -132
- package/dist/review-monitor/verify.d.ts +2 -0
- package/dist/review-monitor/verify.js +32 -13
- package/dist/scm/gh-rest.d.ts +7 -1
- package/dist/scm/gh-rest.js +35 -0
- package/dist/session/session-start.js +5 -0
- package/dist/task-surface/index.js +53 -13
- package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
- package/dist/verify-env/agent-hooks-live-probe.js +216 -0
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/dist/xbrief-migrate/constants.d.ts +6 -0
- package/dist/xbrief-migrate/constants.js +6 -0
- package/dist/xbrief-migrate/index.d.ts +2 -2
- package/dist/xbrief-migrate/index.js +2 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
- package/dist/xbrief-migrate/migrate-project.js +44 -18
- package/package.json +3 -3
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Refs #1912, #1669.
|
|
25
25
|
*/
|
|
26
|
-
import {
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { rmSync, statSync } from "node:fs";
|
|
27
28
|
import { join } from "node:path";
|
|
28
29
|
import { GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL } from "../doctor/constants.js";
|
|
29
30
|
import { extractManagedSection, parseInstallRootFromAgentsMd } from "../doctor/manifest.js";
|
|
30
31
|
import { readTextSafe } from "../doctor/paths.js";
|
|
32
|
+
import { gitPorcelain } from "../story-ready/git.js";
|
|
31
33
|
/** Non-zero exit code for a use-time legacy-layout refusal (needs-action). */
|
|
32
34
|
export const LEGACY_LAYOUT_REFUSED_EXIT_CODE = 2;
|
|
33
35
|
const NOT_LEGACY = {
|
|
@@ -53,6 +55,16 @@ function defaultIsDir(p) {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
/** Does `.gitmodules` reference the deft framework at a deft install path? */
|
|
58
|
+
const LEGACY_DEFT_DIR = "deft";
|
|
59
|
+
function isFrameworkDeftDir(projectDir, isDir, isFile) {
|
|
60
|
+
const deftDir = join(projectDir, LEGACY_DEFT_DIR);
|
|
61
|
+
const deftMarkers = [
|
|
62
|
+
join(deftDir, "VERSION"),
|
|
63
|
+
join(deftDir, "main.md"),
|
|
64
|
+
join(deftDir, "Taskfile.yml"),
|
|
65
|
+
];
|
|
66
|
+
return isDir(deftDir) && (deftMarkers.some((m) => isFile(m)) || isDir(join(deftDir, "skills")));
|
|
67
|
+
}
|
|
56
68
|
function gitmodulesReferencesFramework(text) {
|
|
57
69
|
const normalized = text.replace(/\r\n/g, "\n").toLowerCase();
|
|
58
70
|
if (normalized.includes("deftai/directive")) {
|
|
@@ -62,6 +74,27 @@ function gitmodulesReferencesFramework(text) {
|
|
|
62
74
|
// when the url host differs (mirror, fork). Match `path = deft` / `path = .deft`.
|
|
63
75
|
return /(^|\n)\s*path\s*=\s*\.?deft(\/|\s|$)/.test(normalized);
|
|
64
76
|
}
|
|
77
|
+
const DUAL_LAYOUT_DETAIL = "Found both the canonical .deft/core/ deposit and a leftover legacy deft/ " +
|
|
78
|
+
"framework tree -- remove deft/ so .deft/core/ is the sole framework root.";
|
|
79
|
+
/**
|
|
80
|
+
* Detect a post-bridge dual-layout: canonical `.deft/core/` plus a leftover
|
|
81
|
+
* framework-marked `deft/` tree. Doctor-only -- the npm deposit path treats
|
|
82
|
+
* `.deft/core/` as canonical and must not refuse here (#2805 / #2804 wiring).
|
|
83
|
+
*/
|
|
84
|
+
export function detectDualLayout(projectDir, seams = {}) {
|
|
85
|
+
const isFile = seams.isFile ?? defaultIsFile;
|
|
86
|
+
const isDir = seams.isDir ?? defaultIsDir;
|
|
87
|
+
const hasCanonicalCore = isDir(join(projectDir, ".deft", "core"));
|
|
88
|
+
if (!hasCanonicalCore || !isFrameworkDeftDir(projectDir, isDir, isFile)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
legacy: true,
|
|
93
|
+
kind: "dual-layout",
|
|
94
|
+
detail: DUAL_LAYOUT_DETAIL,
|
|
95
|
+
evidence: [".deft/core/", "deft/"],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
65
98
|
/**
|
|
66
99
|
* Classify the on-disk layout at `projectDir`. Returns `legacy: false` for the
|
|
67
100
|
* canonical `.deft/core/` vendored layout and for a greenfield project (so init
|
|
@@ -71,10 +104,11 @@ export function detectLegacyLayout(projectDir, seams = {}) {
|
|
|
71
104
|
const isFile = seams.isFile ?? defaultIsFile;
|
|
72
105
|
const isDir = seams.isDir ?? defaultIsDir;
|
|
73
106
|
const readText = seams.readText ?? readTextSafe;
|
|
107
|
+
const hasCanonicalCore = isDir(join(projectDir, ".deft", "core"));
|
|
108
|
+
const deftDirIsFramework = isFrameworkDeftDir(projectDir, isDir, isFile);
|
|
74
109
|
// Canonical vendored layout present -> the npm path owns it; not legacy. Any
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
if (isDir(join(projectDir, ".deft", "core"))) {
|
|
110
|
+
// leftover deft/ tree is a dual-layout hygiene issue surfaced by Doctor only.
|
|
111
|
+
if (hasCanonicalCore) {
|
|
78
112
|
return NOT_LEGACY;
|
|
79
113
|
}
|
|
80
114
|
// Orphan `.deft/VERSION` (the old pre-`.deft/core/` manifest location) with no
|
|
@@ -92,13 +126,7 @@ export function detectLegacyLayout(projectDir, seams = {}) {
|
|
|
92
126
|
// framework markers. Distinguish a git-clone / submodule deposit (a `.git`
|
|
93
127
|
// inside `deft/`, or a `.gitmodules` pointing at the framework) from a plain
|
|
94
128
|
// legacy-prefixed vendored copy.
|
|
95
|
-
const deftDir = join(projectDir,
|
|
96
|
-
const deftMarkers = [
|
|
97
|
-
join(deftDir, "VERSION"),
|
|
98
|
-
join(deftDir, "main.md"),
|
|
99
|
-
join(deftDir, "Taskfile.yml"),
|
|
100
|
-
];
|
|
101
|
-
const deftDirIsFramework = isDir(deftDir) && (deftMarkers.some((m) => isFile(m)) || isDir(join(deftDir, "skills")));
|
|
129
|
+
const deftDir = join(projectDir, LEGACY_DEFT_DIR);
|
|
102
130
|
if (deftDirIsFramework) {
|
|
103
131
|
const submoduleGit = isFile(join(deftDir, ".git")) || isDir(join(deftDir, ".git"));
|
|
104
132
|
if (submoduleGit) {
|
|
@@ -215,9 +243,113 @@ export function buildLegacyRefusalJson(command, projectDir, detection) {
|
|
|
215
243
|
}
|
|
216
244
|
/** One-line doctor signpost carrying the stable URL (no baked version/command). */
|
|
217
245
|
export function legacyLayoutSignpostLine(detection) {
|
|
246
|
+
if (detection.kind === "dual-layout") {
|
|
247
|
+
return dualLayoutSignpostLine(detection);
|
|
248
|
+
}
|
|
218
249
|
return (`Legacy Deft layout detected (${detection.kind ?? "unknown"}): ${detection.detail} ` +
|
|
219
250
|
"Run the frozen Go bridge installer to migrate to .deft/core/, then use the npm " +
|
|
220
251
|
`CLI (\`npx @deftai/directive update\`). See ${UPGRADING_DOC_URL} ` +
|
|
221
252
|
`(frozen bridge: ${GO_BRIDGE_RELEASES_URL}).`);
|
|
222
253
|
}
|
|
254
|
+
/** Doctor / npm signpost for the post-bridge dual-layout state (#2805). */
|
|
255
|
+
export function dualLayoutSignpostLine(detection) {
|
|
256
|
+
return (`Dual Deft layout detected (${detection.kind ?? "dual-layout"}): ${detection.detail} ` +
|
|
257
|
+
"Remove the legacy deft/ tree so .deft/core/ is the sole framework root " +
|
|
258
|
+
"(when clean: `git rm -r deft/`; reconcile local edits first if `git status deft/` is dirty). " +
|
|
259
|
+
`See ${UPGRADING_DOC_URL}.`);
|
|
260
|
+
}
|
|
261
|
+
function porcelainEntryPath(line) {
|
|
262
|
+
let path = line.slice(3).trim().replace(/\\/g, "/");
|
|
263
|
+
if (path.startsWith('"') && path.endsWith('"')) {
|
|
264
|
+
path = path.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
265
|
+
}
|
|
266
|
+
return path;
|
|
267
|
+
}
|
|
268
|
+
function porcelainTouchesLegacyDeft(porcelain) {
|
|
269
|
+
const touched = [];
|
|
270
|
+
for (const line of porcelain.split("\n")) {
|
|
271
|
+
if (!line.trim())
|
|
272
|
+
continue;
|
|
273
|
+
const path = porcelainEntryPath(line);
|
|
274
|
+
if (path === LEGACY_DEFT_DIR || path.startsWith(`${LEGACY_DEFT_DIR}/`)) {
|
|
275
|
+
touched.push(path);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return touched;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Remove a clean tracked legacy `deft/` tree after bridge migration (#2805).
|
|
282
|
+
* Refuses when the tree has local modifications or untracked files.
|
|
283
|
+
*/
|
|
284
|
+
export function tryCleanupLegacyDeftTree(projectDir, seams = {}) {
|
|
285
|
+
const detection = detectDualLayout(projectDir);
|
|
286
|
+
if (detection === null) {
|
|
287
|
+
return {
|
|
288
|
+
ok: true,
|
|
289
|
+
action: "skipped",
|
|
290
|
+
detail: "No dual-layout legacy deft/ tree to remove.",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
|
|
294
|
+
const porcelain = readPorcelain(projectDir);
|
|
295
|
+
if (porcelain === null) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
action: "refused",
|
|
299
|
+
detail: "Cannot determine whether the legacy deft/ tree is clean (git unavailable). " +
|
|
300
|
+
"Inspect `git status deft/` and remove the tree manually once clean.",
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const dirtyPaths = porcelainTouchesLegacyDeft(porcelain);
|
|
304
|
+
if (dirtyPaths.length > 0) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
action: "refused",
|
|
308
|
+
detail: "Refusing to remove the legacy deft/ tree because it has local modifications or " +
|
|
309
|
+
"untracked files. Reconcile or commit those changes, then re-run " +
|
|
310
|
+
"`npx @deftai/directive update`, or remove deft/ manually once clean. " +
|
|
311
|
+
`Dirty paths: ${JSON.stringify(dirtyPaths.slice(0, 5))}` +
|
|
312
|
+
(dirtyPaths.length > 5 ? ", …" : "") +
|
|
313
|
+
".",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const deftDir = join(projectDir, LEGACY_DEFT_DIR);
|
|
317
|
+
const removeDir = seams.removeDir ?? ((p) => rmSync(p, { recursive: true, force: true }));
|
|
318
|
+
const runGitRm = seams.runGitRm ??
|
|
319
|
+
((root, relPath) => {
|
|
320
|
+
execFileSync("git", ["rm", "-r", "--ignore-unmatch", relPath], {
|
|
321
|
+
cwd: root,
|
|
322
|
+
encoding: "utf8",
|
|
323
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
try {
|
|
327
|
+
runGitRm(projectDir, LEGACY_DEFT_DIR);
|
|
328
|
+
removeDir(deftDir);
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
action: "staged",
|
|
332
|
+
detail: "Removed the legacy deft/ framework tree and staged its deletion; " +
|
|
333
|
+
".deft/core/ is now the sole framework root.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
try {
|
|
338
|
+
removeDir(deftDir);
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
action: "removed",
|
|
342
|
+
detail: "Removed the legacy deft/ framework tree from disk; stage the deletion with " +
|
|
343
|
+
"`git add -u deft/` if it was tracked.",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (cause) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
action: "refused",
|
|
350
|
+
detail: `Could not remove the legacy deft/ tree: ${String(cause)}`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
223
355
|
//# sourceMappingURL=legacy-detect.js.map
|
|
@@ -47,6 +47,8 @@ export interface RefreshDepositSeams {
|
|
|
47
47
|
gitLsFiles?: GitLsFiles;
|
|
48
48
|
/** #2530: injected git config seams for {@link writeConsumerGitHooks}. */
|
|
49
49
|
gitHooks?: GitHooksSeams;
|
|
50
|
+
/** #2822: optional seam for trusted-org policy force-on migration. */
|
|
51
|
+
runOrgForceOn?: (projectRoot: string) => void;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* The four states `directive update` classifies an EXISTING install into BEFORE
|
|
@@ -18,11 +18,13 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
|
18
18
|
import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
|
|
19
19
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
20
20
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
21
|
+
import { runOrgForceOnMigration } from "../policy/org-force-on-migration.js";
|
|
21
22
|
import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
|
|
22
23
|
import { gitPorcelain } from "../story-ready/git.js";
|
|
24
|
+
import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
|
|
23
25
|
import { writeAgentHookDeposit } from "./agent-hooks.js";
|
|
24
26
|
import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
|
|
25
|
-
import { depositStagePaths, isInstallerManagedPath, printCommitGuidance,
|
|
27
|
+
import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, prunePackageAbsentDepositPaths, } from "./hygiene.js";
|
|
26
28
|
import { parseInitArgv } from "./init-deposit.js";
|
|
27
29
|
import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
|
|
28
30
|
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
@@ -409,10 +411,10 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
409
411
|
else {
|
|
410
412
|
await copyContent(contentRoot, deftDir);
|
|
411
413
|
await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
|
|
412
|
-
// #2347: prune
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
await
|
|
414
|
+
// #2804 / #2347: prune deposit paths absent from the content package. The
|
|
415
|
+
// additive file-swap never removes them, causing stale bridge-era files to
|
|
416
|
+
// survive across upgrades until manually cleaned.
|
|
417
|
+
await prunePackageAbsentDepositPaths(deftDir, contentRoot, io);
|
|
416
418
|
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
417
419
|
const manifestFields = {
|
|
418
420
|
ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
|
|
@@ -441,6 +443,17 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
441
443
|
// lifecycle content before migrate:xbrief can transactionally converge it.
|
|
442
444
|
if (hasCanonicalXbriefLifecycle(projectDir)) {
|
|
443
445
|
syncConsumerXbriefSchemas(projectDir, deftDir);
|
|
446
|
+
removeStaleMigratedFrameworkNarrative(projectDir);
|
|
447
|
+
}
|
|
448
|
+
const runOrgForceOn = seams.runOrgForceOn ??
|
|
449
|
+
((root) => {
|
|
450
|
+
runOrgForceOnMigration(root, { actor: "directive-update" });
|
|
451
|
+
});
|
|
452
|
+
try {
|
|
453
|
+
runOrgForceOn(projectDir);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// Policy migration is best-effort; never block framework refresh (#2822).
|
|
444
457
|
}
|
|
445
458
|
const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
|
|
446
459
|
writeAgentHookDeposit(projectDir, io);
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./capacity.js";
|
|
|
4
4
|
export * from "./decisions.js";
|
|
5
5
|
export * from "./disclosure.js";
|
|
6
6
|
export * from "./host-hooks.js";
|
|
7
|
+
export * from "./org-force-on-migration.js";
|
|
7
8
|
export * from "./plan-extensions.js";
|
|
8
9
|
export * from "./policy-invocation.js";
|
|
9
10
|
export * from "./product-signal.js";
|
|
@@ -11,6 +12,7 @@ export * from "./resolve.js";
|
|
|
11
12
|
export * from "./runtime-authority.js";
|
|
12
13
|
export * from "./staleness-tickler.js";
|
|
13
14
|
export * from "./value-feedback.js";
|
|
15
|
+
export * from "./value-feedback-autoenable.js";
|
|
14
16
|
export * from "./wip.js";
|
|
15
17
|
export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
|
|
16
18
|
export declare const FIELD_WIP_CAP = "plan.policy.wipCap";
|
package/dist/policy/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./capacity.js";
|
|
|
12
12
|
export * from "./decisions.js";
|
|
13
13
|
export * from "./disclosure.js";
|
|
14
14
|
export * from "./host-hooks.js";
|
|
15
|
+
export * from "./org-force-on-migration.js";
|
|
15
16
|
export * from "./plan-extensions.js";
|
|
16
17
|
export * from "./policy-invocation.js";
|
|
17
18
|
export * from "./product-signal.js";
|
|
@@ -19,6 +20,7 @@ export * from "./resolve.js";
|
|
|
19
20
|
export * from "./runtime-authority.js";
|
|
20
21
|
export * from "./staleness-tickler.js";
|
|
21
22
|
export * from "./value-feedback.js";
|
|
23
|
+
export * from "./value-feedback-autoenable.js";
|
|
22
24
|
export * from "./wip.js";
|
|
23
25
|
export const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
|
|
24
26
|
export const FIELD_WIP_CAP = "plan.policy.wipCap";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type OrgAutoEnableOptions } from "./value-feedback-autoenable.js";
|
|
2
|
+
/** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
|
|
3
|
+
export declare const ORG_FORCE_ON_MARKER_REL: string;
|
|
4
|
+
export declare const ORG_FORCE_ON_MARKER_VERSION: 1;
|
|
5
|
+
export interface OrgForceOnMarker {
|
|
6
|
+
readonly version: typeof ORG_FORCE_ON_MARKER_VERSION;
|
|
7
|
+
readonly appliedAt: string;
|
|
8
|
+
readonly originOrg: string;
|
|
9
|
+
readonly valueFeedback: boolean;
|
|
10
|
+
readonly productSignal: boolean;
|
|
11
|
+
readonly directiveVersion: string;
|
|
12
|
+
}
|
|
13
|
+
/** Canonical typed block written by the one-time migration (#2822 Part A). */
|
|
14
|
+
export declare const FORCE_ON_VALUE_FEEDBACK_BLOCK: {
|
|
15
|
+
readonly enabled: true;
|
|
16
|
+
readonly emitEvents: true;
|
|
17
|
+
readonly sessionLine: true;
|
|
18
|
+
readonly upstreamPrompt: false;
|
|
19
|
+
};
|
|
20
|
+
/** Read the #2822 migration marker when present and valid. */
|
|
21
|
+
export declare function readOrgForceOnMarker(projectRoot: string): OrgForceOnMarker | null;
|
|
22
|
+
export interface RunOrgForceOnMigrationOptions {
|
|
23
|
+
readonly autoEnable?: OrgAutoEnableOptions;
|
|
24
|
+
readonly actor?: string;
|
|
25
|
+
readonly now?: Date;
|
|
26
|
+
}
|
|
27
|
+
export interface OrgForceOnMigrationResult {
|
|
28
|
+
readonly ran: boolean;
|
|
29
|
+
readonly skippedReason: string | null;
|
|
30
|
+
readonly valueFeedbackChanged: boolean;
|
|
31
|
+
readonly productSignalChanged: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
|
|
35
|
+
* (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runOrgForceOnMigration(projectRoot: string, options?: RunOrgForceOnMigrationOptions): OrgForceOnMigrationResult;
|
|
38
|
+
/** True when typed valueFeedback matches the #2822 force-on block shape. */
|
|
39
|
+
export declare function isForceOnValueFeedbackBlock(raw: unknown): boolean;
|
|
40
|
+
/** Resolve auditable source for valueFeedback after #2822 migration. */
|
|
41
|
+
export declare function valueFeedbackInstallForceOnSource(projectRoot: string, raw: unknown): "install-force-on" | null;
|
|
42
|
+
/** Resolve auditable source for productSignal after #2822 migration. */
|
|
43
|
+
export declare function productSignalInstallForceOnSource(projectRoot: string, raw: unknown): "install-force-on" | null;
|
|
44
|
+
//# sourceMappingURL=org-force-on-migration.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
4
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
5
|
+
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
6
|
+
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
7
|
+
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
|
|
8
|
+
import { detectOriginOrg, isTrustedOrgAutoEnable, } from "./value-feedback-autoenable.js";
|
|
9
|
+
/** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
|
|
10
|
+
export const ORG_FORCE_ON_MARKER_REL = join(".deft-cache", "org-force-on-v2822.json");
|
|
11
|
+
export const ORG_FORCE_ON_MARKER_VERSION = 1;
|
|
12
|
+
/** Canonical typed block written by the one-time migration (#2822 Part A). */
|
|
13
|
+
export const FORCE_ON_VALUE_FEEDBACK_BLOCK = {
|
|
14
|
+
enabled: true,
|
|
15
|
+
emitEvents: true,
|
|
16
|
+
sessionLine: true,
|
|
17
|
+
upstreamPrompt: false,
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_PRODUCT_SIGNAL_SINK = "deftai/product-signal";
|
|
20
|
+
function markerPath(projectRoot) {
|
|
21
|
+
return resolve(projectRoot, ORG_FORCE_ON_MARKER_REL);
|
|
22
|
+
}
|
|
23
|
+
function parseMarker(raw) {
|
|
24
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const rec = raw;
|
|
28
|
+
if (rec.version !== ORG_FORCE_ON_MARKER_VERSION ||
|
|
29
|
+
typeof rec.appliedAt !== "string" ||
|
|
30
|
+
typeof rec.originOrg !== "string" ||
|
|
31
|
+
typeof rec.valueFeedback !== "boolean" ||
|
|
32
|
+
typeof rec.productSignal !== "boolean" ||
|
|
33
|
+
typeof rec.directiveVersion !== "string") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
version: ORG_FORCE_ON_MARKER_VERSION,
|
|
38
|
+
appliedAt: rec.appliedAt,
|
|
39
|
+
originOrg: rec.originOrg,
|
|
40
|
+
valueFeedback: rec.valueFeedback,
|
|
41
|
+
productSignal: rec.productSignal,
|
|
42
|
+
directiveVersion: rec.directiveVersion,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Read the #2822 migration marker when present and valid. */
|
|
46
|
+
export function readOrgForceOnMarker(projectRoot) {
|
|
47
|
+
const path = markerPath(projectRoot);
|
|
48
|
+
if (!existsSync(path)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return parseMarker(JSON.parse(readFileSync(path, "utf8")));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writeOrgForceOnMarker(projectRoot, marker) {
|
|
59
|
+
const path = markerPath(projectRoot);
|
|
60
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
61
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
62
|
+
writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
63
|
+
}
|
|
64
|
+
function readTypedBoolean(raw, key, fallback) {
|
|
65
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
const value = raw[key];
|
|
69
|
+
return typeof value === "boolean" ? value : fallback;
|
|
70
|
+
}
|
|
71
|
+
function valueFeedbackNeedsForceOn(raw) {
|
|
72
|
+
const enabled = readTypedBoolean(raw, "enabled", false);
|
|
73
|
+
if (!enabled) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const emitEvents = readTypedBoolean(raw, "emitEvents", true);
|
|
77
|
+
const sessionLine = readTypedBoolean(raw, "sessionLine", true);
|
|
78
|
+
const upstreamPrompt = readTypedBoolean(raw, "upstreamPrompt", false);
|
|
79
|
+
return !emitEvents || !sessionLine || upstreamPrompt;
|
|
80
|
+
}
|
|
81
|
+
function productSignalNeedsForceOn(raw) {
|
|
82
|
+
return !readTypedBoolean(raw, "enabled", false);
|
|
83
|
+
}
|
|
84
|
+
function ensurePolicyBlock(plan) {
|
|
85
|
+
migrateLegacyPolicyKey(plan);
|
|
86
|
+
const existing = plan[PLAN_POLICY_KEY];
|
|
87
|
+
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
88
|
+
if (existing === undefined) {
|
|
89
|
+
plan[PLAN_POLICY_KEY] = {};
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
throw new Error("plan.policy is not an object");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return plan[PLAN_POLICY_KEY];
|
|
96
|
+
}
|
|
97
|
+
function writeMarkerForSkip(projectRoot, options, valueFeedback, productSignal) {
|
|
98
|
+
const originOrg = detectOriginOrg(projectRoot, options.autoEnable) ?? "unknown";
|
|
99
|
+
const now = options.now ?? new Date();
|
|
100
|
+
writeOrgForceOnMarker(projectRoot, {
|
|
101
|
+
version: ORG_FORCE_ON_MARKER_VERSION,
|
|
102
|
+
appliedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
103
|
+
originOrg,
|
|
104
|
+
valueFeedback,
|
|
105
|
+
productSignal,
|
|
106
|
+
directiveVersion: readCorePackageVersion(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
|
|
111
|
+
* (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
|
|
112
|
+
*/
|
|
113
|
+
export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
114
|
+
if (readOrgForceOnMarker(projectRoot) !== null) {
|
|
115
|
+
return {
|
|
116
|
+
ran: false,
|
|
117
|
+
skippedReason: "marker-present",
|
|
118
|
+
valueFeedbackChanged: false,
|
|
119
|
+
productSignalChanged: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
|
|
123
|
+
return {
|
|
124
|
+
ran: false,
|
|
125
|
+
skippedReason: "non-trusted-org",
|
|
126
|
+
valueFeedbackChanged: false,
|
|
127
|
+
productSignalChanged: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const [data, err] = loadProjectDefinition(projectRoot);
|
|
131
|
+
if (data === null) {
|
|
132
|
+
return {
|
|
133
|
+
ran: false,
|
|
134
|
+
skippedReason: err ?? "project-definition-missing",
|
|
135
|
+
valueFeedbackChanged: false,
|
|
136
|
+
productSignalChanged: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
140
|
+
const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
|
|
141
|
+
? policyBlock
|
|
142
|
+
: {};
|
|
143
|
+
const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
|
|
144
|
+
const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
|
|
145
|
+
const needsVf = valueFeedbackNeedsForceOn(vfRaw);
|
|
146
|
+
const needsPs = productSignalNeedsForceOn(psRaw);
|
|
147
|
+
if (!needsVf && !needsPs) {
|
|
148
|
+
writeMarkerForSkip(projectRoot, options, false, false);
|
|
149
|
+
return {
|
|
150
|
+
ran: false,
|
|
151
|
+
skippedReason: "already-enabled",
|
|
152
|
+
valueFeedbackChanged: false,
|
|
153
|
+
productSignalChanged: false,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const path = projectDefinitionPath(projectRoot);
|
|
157
|
+
let valueFeedbackChanged = false;
|
|
158
|
+
let productSignalChanged = false;
|
|
159
|
+
projectDefinitionMutationLock(projectRoot, () => {
|
|
160
|
+
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
161
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
162
|
+
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
163
|
+
}
|
|
164
|
+
const rootData = parsed;
|
|
165
|
+
if (typeof rootData.plan !== "object" ||
|
|
166
|
+
rootData.plan === null ||
|
|
167
|
+
Array.isArray(rootData.plan)) {
|
|
168
|
+
if (rootData.plan === undefined) {
|
|
169
|
+
rootData.plan = {};
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const plan = rootData.plan;
|
|
176
|
+
const policy = ensurePolicyBlock(plan);
|
|
177
|
+
const previousVf = policy.valueFeedback;
|
|
178
|
+
const previousPs = policy.productSignal;
|
|
179
|
+
if (needsVf) {
|
|
180
|
+
policy.valueFeedback = { ...FORCE_ON_VALUE_FEEDBACK_BLOCK };
|
|
181
|
+
valueFeedbackChanged = valueFeedbackNeedsForceOn(previousVf);
|
|
182
|
+
}
|
|
183
|
+
if (needsPs) {
|
|
184
|
+
const prevObj = typeof previousPs === "object" && previousPs !== null && !Array.isArray(previousPs)
|
|
185
|
+
? previousPs
|
|
186
|
+
: {};
|
|
187
|
+
const sinkRepo = typeof prevObj.sinkRepo === "string" && prevObj.sinkRepo.trim().length > 0
|
|
188
|
+
? prevObj.sinkRepo.trim()
|
|
189
|
+
: DEFAULT_PRODUCT_SIGNAL_SINK;
|
|
190
|
+
policy.productSignal = { enabled: true, sinkRepo };
|
|
191
|
+
productSignalChanged = productSignalNeedsForceOn(previousPs);
|
|
192
|
+
}
|
|
193
|
+
if (valueFeedbackChanged || productSignalChanged) {
|
|
194
|
+
atomicWriteProjectDefinition(path, rootData);
|
|
195
|
+
}
|
|
196
|
+
const actor = options.actor ?? "directive-update";
|
|
197
|
+
appendAuditLog(projectRoot, [
|
|
198
|
+
`actor=${actor}`,
|
|
199
|
+
"org-force-on-v2822",
|
|
200
|
+
`valueFeedback=${needsVf ? "forced-on" : "unchanged"}`,
|
|
201
|
+
`productSignal=${needsPs ? "forced-on" : "unchanged"}`,
|
|
202
|
+
`previousValueFeedback=${JSON.stringify(previousVf ?? null)}`,
|
|
203
|
+
`previousProductSignal=${JSON.stringify(previousPs ?? null)}`,
|
|
204
|
+
].join(" "));
|
|
205
|
+
writeMarkerForSkip(projectRoot, options, needsVf, needsPs);
|
|
206
|
+
return { changed: valueFeedbackChanged || productSignalChanged };
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
ran: true,
|
|
210
|
+
skippedReason: null,
|
|
211
|
+
valueFeedbackChanged,
|
|
212
|
+
productSignalChanged,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** True when typed valueFeedback matches the #2822 force-on block shape. */
|
|
216
|
+
export function isForceOnValueFeedbackBlock(raw) {
|
|
217
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const block = raw;
|
|
221
|
+
return (block.enabled === FORCE_ON_VALUE_FEEDBACK_BLOCK.enabled &&
|
|
222
|
+
readTypedBoolean(raw, "emitEvents", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.emitEvents &&
|
|
223
|
+
readTypedBoolean(raw, "sessionLine", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.sessionLine &&
|
|
224
|
+
readTypedBoolean(raw, "upstreamPrompt", true) === FORCE_ON_VALUE_FEEDBACK_BLOCK.upstreamPrompt);
|
|
225
|
+
}
|
|
226
|
+
/** Resolve auditable source for valueFeedback after #2822 migration. */
|
|
227
|
+
export function valueFeedbackInstallForceOnSource(projectRoot, raw) {
|
|
228
|
+
const marker = readOrgForceOnMarker(projectRoot);
|
|
229
|
+
if (marker === null || !marker.valueFeedback) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return isForceOnValueFeedbackBlock(raw) ? "install-force-on" : null;
|
|
233
|
+
}
|
|
234
|
+
/** Resolve auditable source for productSignal after #2822 migration. */
|
|
235
|
+
export function productSignalInstallForceOnSource(projectRoot, raw) {
|
|
236
|
+
const marker = readOrgForceOnMarker(projectRoot);
|
|
237
|
+
if (marker === null || !marker.productSignal) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return readTypedBoolean(raw, "enabled", false) ? "install-force-on" : null;
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=org-force-on-migration.js.map
|
|
@@ -9,7 +9,7 @@ export interface ProductSignalConfig {
|
|
|
9
9
|
readonly enabled: boolean;
|
|
10
10
|
readonly sinkRepo: string;
|
|
11
11
|
}
|
|
12
|
-
export type ProductSignalSource = "typed" | "default" | "default-on-error";
|
|
12
|
+
export type ProductSignalSource = "typed" | "install-force-on" | "default" | "default-on-error";
|
|
13
13
|
export interface ProductSignalResolved extends ProductSignalConfig {
|
|
14
14
|
readonly source: ProductSignalSource;
|
|
15
15
|
readonly error: string | null;
|
|
@@ -17,6 +17,8 @@ export interface ProductSignalResolved extends ProductSignalConfig {
|
|
|
17
17
|
export declare const PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE: string;
|
|
18
18
|
/** Validate a `plan.policy.productSignal` payload. */
|
|
19
19
|
export declare function validateProductSignal(value: unknown): string[];
|
|
20
|
+
/** Resolve a typed `productSignal` block without install-force-on overlay. */
|
|
21
|
+
export declare function resolveProductSignalFromTypedBlock(raw: unknown): ProductSignalResolved;
|
|
20
22
|
/** Resolve `plan.policy.productSignal` from PROJECT-DEFINITION (#2693). */
|
|
21
23
|
export declare function resolveProductSignal(projectRoot: string): ProductSignalResolved;
|
|
22
24
|
/** Human-readable status line for CLI surfaces. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
3
|
+
import { productSignalInstallForceOnSource } from "./org-force-on-migration.js";
|
|
3
4
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
4
5
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
5
6
|
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
|
|
@@ -48,7 +49,8 @@ export function validateProductSignal(value) {
|
|
|
48
49
|
}
|
|
49
50
|
return errors;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
/** Resolve a typed `productSignal` block without install-force-on overlay. */
|
|
53
|
+
export function resolveProductSignalFromTypedBlock(raw) {
|
|
52
54
|
const errors = validateProductSignal(raw);
|
|
53
55
|
if (errors.length > 0) {
|
|
54
56
|
return defaultResolved("default-on-error", errors[0] ?? "invalid productSignal block");
|
|
@@ -81,7 +83,13 @@ export function resolveProductSignal(projectRoot) {
|
|
|
81
83
|
!("productSignal" in policyBlock)) {
|
|
82
84
|
return defaultResolved("default");
|
|
83
85
|
}
|
|
84
|
-
|
|
86
|
+
const raw = policyBlock.productSignal;
|
|
87
|
+
const installSource = productSignalInstallForceOnSource(projectRoot, raw);
|
|
88
|
+
if (installSource !== null) {
|
|
89
|
+
const resolved = resolveProductSignalFromTypedBlock(raw);
|
|
90
|
+
return { ...resolved, source: installSource };
|
|
91
|
+
}
|
|
92
|
+
return resolveProductSignalFromTypedBlock(raw);
|
|
85
93
|
}
|
|
86
94
|
/** Human-readable status line for CLI surfaces. */
|
|
87
95
|
export function formatProductSignalStatusLine(policy) {
|
|
@@ -117,7 +125,10 @@ export function inspectProductSignal(data, projectRoot) {
|
|
|
117
125
|
}
|
|
118
126
|
return fieldFromResolved(defaultResolved("default"));
|
|
119
127
|
}
|
|
120
|
-
|
|
128
|
+
const raw = policyBlock.productSignal;
|
|
129
|
+
const installSource = projectRoot !== undefined ? productSignalInstallForceOnSource(projectRoot, raw) : null;
|
|
130
|
+
const resolved = resolveProductSignalFromTypedBlock(raw);
|
|
131
|
+
return fieldFromResolved(installSource !== null ? { ...resolved, source: installSource } : resolved);
|
|
121
132
|
}
|
|
122
133
|
/** Persist `productSignal.enabled=true` after capability-cost disclosure (#2693). */
|
|
123
134
|
export function enableProductSignal(projectRoot, options) {
|
|
@@ -168,7 +179,7 @@ export function enableProductSignal(projectRoot, options) {
|
|
|
168
179
|
? prevObj.sinkRepo.trim()
|
|
169
180
|
: DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
|
|
170
181
|
const nextBlock = { enabled: true, sinkRepo };
|
|
171
|
-
const previousNormalized =
|
|
182
|
+
const previousNormalized = resolveProductSignalFromTypedBlock(previous);
|
|
172
183
|
const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
|
|
173
184
|
previousNormalized.sinkRepo !== nextBlock.sinkRepo;
|
|
174
185
|
policyBlock.productSignal = nextBlock;
|