@inventec/dsh-copilot-auth 1.2.7 → 1.2.8

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/src/state.mjs CHANGED
@@ -1,30 +1,55 @@
1
1
  // state.mjs — 持久状态的唯一读写层(薄层,落盘逻辑都在 atomic-json)。
2
- // 状态文件:~/.dsh/copilot-auth-state.json(路径由 host 决定),原子写。
3
- // lastError 规则(R3-6):顶层 lastError/lastErrorAt 是唯一诊断面;journal 相位内
4
- // 错误同时投影顶层;任一相位成功推进/消费时清除;state 损坏改名留存 .corrupt-<ts>
5
- // 不覆盖,返回带 state-corrupt 的安全态,且后续普通保存不得抹掉留存文件。
6
- import { renameSync } from "node:fs";
2
+ // v1.2.8 起 v1 函数族(loadState/saveState/journal 系列)已删除:生产路径自 v2 只走
3
+ // schema 2 函数(loadRefreshState/saveRefreshState),v1 全局状态文件自 T10 起只读检测。
4
+ // restartMarker 为 v2 事务内核(refresh-transaction.mjs)仍在使用的 restartState 构造器。
5
+ // 损坏语义(沿用 v1 规则):解析失败/形状非法 → 改名留存 .corrupt-<ts> 不覆盖,
6
+ // 返回带 state-corrupt 的安全态,且后续普通保存不得抹掉留存文件。
7
+ import { existsSync, mkdirSync, renameSync } from "node:fs";
8
+ import { join } from "node:path";
7
9
  import { readJson, writeJsonAtomic } from "./atomic-json.mjs";
8
10
 
9
- export const STATE_VERSION = 1;
10
- export const CATALOG_IDENTITY = { packageName: "@earendil-works/pi-ai", catalogSchemaVersion: 1 };
11
- export const JOURNAL_PHASES = ["prepared", "catalog-committed-needs-restart"];
12
11
  export const RESTART_REASONS = ["refresh", "self-heal"];
13
12
 
14
- export function freshState() {
13
+ // restartState 只表达「目录写入需跨 boot 加载」,不携带 settings 意图。
14
+ export function restartMarker(reason, expectedEntriesDigest) {
15
+ if (!RESTART_REASONS.includes(reason)) {
16
+ throw new Error(`unknown restart reason: ${reason}`);
17
+ }
18
+ return { reason, expectedEntriesDigest, since: new Date().toISOString() };
19
+ }
20
+
21
+ // ==================== schema 2:profile 隔离的刷新状态(T7) ====================
22
+ // 文件:<dataDir>/refresh-state.json(dataDir 由 runtime-scope 解析,profile 私有)。
23
+ // 旧 v1 全局文件(~/.dsh/copilot-auth-state.json)只读检测(legacyStateDetected),
24
+ // 绝不自动消费其 journal/overlay——历史条目经新 preview 重新确认采用(Q14/Q19)。
25
+ // 损坏改名留存 .corrupt-<ts> 不覆盖,沿用 v1 语义;未知 version 拒绝执行并保留原件。
26
+ export const REFRESH_STATE_VERSION = 2;
27
+
28
+ export function freshRefreshState(profileId) {
15
29
  return {
16
- version: STATE_VERSION,
30
+ version: REFRESH_STATE_VERSION,
31
+ profileId: profileId ?? null,
17
32
  activated: false,
33
+ appliedOverlay: {},
34
+ appliedProvenance: null,
35
+ intentVersion: 0, // 信息性镜像:权威来源是 auth-intent.json(T3)
36
+ activeOperation: null,
37
+ lastResult: null,
18
38
  restartState: null,
19
- appliedOverlay: {}, // 与 catalog 同构,不夹带插件私有字段
20
- appliedProvenance: null, // { sourcePiAiVersion, integrity, appliedAgainstPiAiVersion, catalogSchemaVersion: 1 }
21
- journal: null,
22
39
  lastError: null,
23
40
  lastErrorAt: null,
24
41
  };
25
42
  }
26
43
 
27
- export function loadState(path) {
44
+ export function refreshStateFile(scope) {
45
+ return join(scope.dataDir, "refresh-state.json");
46
+ }
47
+
48
+ export function loadRefreshState(scope, { legacyPath } = {}) {
49
+ if (!scope?.known) {
50
+ return { state: freshRefreshState(null), flags: { scopeUnavailable: true } };
51
+ }
52
+ const path = refreshStateFile(scope);
28
53
  let raw;
29
54
  let corrupt = false;
30
55
  try {
@@ -33,66 +58,42 @@ export function loadState(path) {
33
58
  corrupt = true;
34
59
  }
35
60
  if (!corrupt && raw !== undefined && (typeof raw !== "object" || raw === null || Array.isArray(raw))) {
36
- corrupt = true; // 解析成功但形状非法同样按损坏处理
61
+ corrupt = true;
37
62
  }
38
63
  if (corrupt) {
39
- // 解析失败/形状非法 → 改名留存 .corrupt-<ts>,返回安全态
40
64
  try {
41
65
  renameSync(path, `${path}.corrupt-${Date.now()}`);
42
66
  } catch { /* 改名失败不阻断安全态返回 */ }
43
- const s = freshState();
67
+ const s = freshRefreshState(scope.profileId);
44
68
  s.lastError = "state-corrupt";
45
69
  s.lastErrorAt = new Date().toISOString();
46
- return s;
70
+ return { state: s, flags: {} };
47
71
  }
48
- if (raw === undefined) return freshState();
49
- // 迁移:未知/缺失字段回落默认(当前仅 version 1)
50
- return { ...freshState(), ...raw, version: STATE_VERSION };
72
+ if (raw === undefined) {
73
+ const flags = {};
74
+ if (legacyPath && existsSync(legacyPath)) flags.legacyStateDetected = true;
75
+ return { state: freshRefreshState(scope.profileId), flags };
76
+ }
77
+ if (raw.version !== REFRESH_STATE_VERSION) {
78
+ // 未来/未知 schema:原件保留,不解析不消费,调用方禁写(要求重新预览/人工核实)
79
+ return { state: null, flags: { unknownVersion: true, preservedPath: path } };
80
+ }
81
+ return { state: { ...freshRefreshState(scope.profileId), ...raw, version: REFRESH_STATE_VERSION }, flags: {} };
51
82
  }
52
83
 
53
- export function saveState(path, state) {
54
- writeJsonAtomic(path, state);
84
+ export function saveRefreshState(scope, state) {
85
+ if (!scope?.known) throw new Error("scope-unavailable");
86
+ if (!state || state.version !== REFRESH_STATE_VERSION) {
87
+ throw new Error("refuse-save-unknown-version");
88
+ }
89
+ mkdirSync(scope.dataDir, { recursive: true });
90
+ writeJsonAtomic(refreshStateFile(scope), state);
55
91
  }
56
92
 
57
- export function setLastError(state, error) {
93
+ export function setRefreshError(state, error) {
58
94
  return { ...state, lastError: String(error?.message ?? error), lastErrorAt: new Date().toISOString() };
59
95
  }
60
96
 
61
- export function clearLastError(state) {
97
+ export function clearRefreshError(state) {
62
98
  return { ...state, lastError: null, lastErrorAt: null };
63
99
  }
64
-
65
- // journal 相位转移辅助:仅在 prepared → catalog-committed-needs-restart 单向前进。
66
- export function createJournal({
67
- settingsBaseline, targetIds, pendingOverlay, appliedAgainstPiAiVersion,
68
- catalogBaselineDigest, patchedCatalogDigest, source,
69
- }) {
70
- return {
71
- phase: "prepared",
72
- createdAt: new Date().toISOString(),
73
- appliedAgainstPiAiVersion,
74
- catalogIdentity: { ...CATALOG_IDENTITY },
75
- settingsBaseline, // raw user 层完整配置视图(R3-2)
76
- targetIds,
77
- pendingOverlay, // catalog-shaped 增量(R3-4),provenance 不夹带在条目里
78
- catalogBaselineDigest,
79
- patchedCatalogDigest,
80
- source, // { kind: "latest" | "overlay" | "local", piAiVersion?, integrity? }
81
- lastError: null,
82
- };
83
- }
84
-
85
- export function advanceJournal(journal) {
86
- if (journal?.phase !== "prepared") {
87
- throw new Error(`cannot advance journal from phase ${journal?.phase}`);
88
- }
89
- return { ...journal, phase: "catalog-committed-needs-restart" };
90
- }
91
-
92
- // restartState 只表达「目录写入需跨 boot 加载」(R3-5),不携带 settings 意图。
93
- export function restartMarker(reason, expectedEntriesDigest) {
94
- if (!RESTART_REASONS.includes(reason)) {
95
- throw new Error(`unknown restart reason: ${reason}`);
96
- }
97
- return { reason, expectedEntriesDigest, since: new Date().toISOString() };
98
- }