@tomflow/proflow-platform-cli 0.1.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.
Files changed (144) hide show
  1. package/README.md +37 -0
  2. package/conformance.json +1 -0
  3. package/dist/deployment/adapter.d.ts +57 -0
  4. package/dist/deployment/adapter.js +25 -0
  5. package/dist/deployment/descriptor.d.ts +39 -0
  6. package/dist/deployment/descriptor.js +41 -0
  7. package/dist/src/apply/apply.d.ts +24 -0
  8. package/dist/src/apply/apply.js +271 -0
  9. package/dist/src/apply/cleanup.d.ts +12 -0
  10. package/dist/src/apply/cleanup.js +37 -0
  11. package/dist/src/apply/current.d.ts +4 -0
  12. package/dist/src/apply/current.js +73 -0
  13. package/dist/src/apply/driver.d.ts +23 -0
  14. package/dist/src/apply/driver.js +154 -0
  15. package/dist/src/apply/execute.d.ts +36 -0
  16. package/dist/src/apply/execute.js +124 -0
  17. package/dist/src/apply/index.d.ts +8 -0
  18. package/dist/src/apply/index.js +4 -0
  19. package/dist/src/apply/reality.d.ts +20 -0
  20. package/dist/src/apply/reality.js +93 -0
  21. package/dist/src/binding/production-bindings.d.ts +45 -0
  22. package/dist/src/binding/production-bindings.js +102 -0
  23. package/dist/src/cli.d.ts +12 -0
  24. package/dist/src/cli.js +712 -0
  25. package/dist/src/contracts.d.ts +113 -0
  26. package/dist/src/contracts.js +1 -0
  27. package/dist/src/discovery/catalog.d.ts +10 -0
  28. package/dist/src/discovery/catalog.js +76 -0
  29. package/dist/src/discovery/discover.d.ts +19 -0
  30. package/dist/src/discovery/discover.js +124 -0
  31. package/dist/src/discovery/index.d.ts +5 -0
  32. package/dist/src/discovery/index.js +4 -0
  33. package/dist/src/discovery/installed.d.ts +21 -0
  34. package/dist/src/discovery/installed.js +130 -0
  35. package/dist/src/discovery/workspace.d.ts +10 -0
  36. package/dist/src/discovery/workspace.js +155 -0
  37. package/dist/src/docs/docs.d.ts +47 -0
  38. package/dist/src/docs/docs.js +129 -0
  39. package/dist/src/docs/index.d.ts +1 -0
  40. package/dist/src/docs/index.js +1 -0
  41. package/dist/src/doctor/doctor.d.ts +41 -0
  42. package/dist/src/doctor/doctor.js +80 -0
  43. package/dist/src/doctor/index.d.ts +2 -0
  44. package/dist/src/doctor/index.js +1 -0
  45. package/dist/src/errors.d.ts +6 -0
  46. package/dist/src/errors.js +35 -0
  47. package/dist/src/graph/graph.d.ts +23 -0
  48. package/dist/src/graph/graph.js +132 -0
  49. package/dist/src/graph/index.d.ts +2 -0
  50. package/dist/src/graph/index.js +1 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.js +18 -0
  53. package/dist/src/install/bootstrap.d.ts +10 -0
  54. package/dist/src/install/bootstrap.js +79 -0
  55. package/dist/src/install/environment.d.ts +29 -0
  56. package/dist/src/install/environment.js +226 -0
  57. package/dist/src/install/index.d.ts +6 -0
  58. package/dist/src/install/index.js +3 -0
  59. package/dist/src/install/install.d.ts +19 -0
  60. package/dist/src/install/install.js +131 -0
  61. package/dist/src/install/package-manager.d.ts +11 -0
  62. package/dist/src/install/package-manager.js +58 -0
  63. package/dist/src/lifecycle/dispatch.d.ts +47 -0
  64. package/dist/src/lifecycle/dispatch.js +142 -0
  65. package/dist/src/lifecycle/index.d.ts +3 -0
  66. package/dist/src/lifecycle/index.js +2 -0
  67. package/dist/src/lifecycle/service-process.d.ts +11 -0
  68. package/dist/src/lifecycle/service-process.js +326 -0
  69. package/dist/src/manifest/index.d.ts +2 -0
  70. package/dist/src/manifest/index.js +1 -0
  71. package/dist/src/manifest/manifest.d.ts +61 -0
  72. package/dist/src/manifest/manifest.js +160 -0
  73. package/dist/src/modules.d.ts +11 -0
  74. package/dist/src/modules.js +55 -0
  75. package/dist/src/observer/deployment-summary.d.ts +31 -0
  76. package/dist/src/observer/deployment-summary.js +34 -0
  77. package/dist/src/paths.d.ts +21 -0
  78. package/dist/src/paths.js +56 -0
  79. package/dist/src/persistence/atomic.d.ts +2 -0
  80. package/dist/src/persistence/atomic.js +16 -0
  81. package/dist/src/persistence/config.d.ts +12 -0
  82. package/dist/src/persistence/config.js +43 -0
  83. package/dist/src/persistence/generated.d.ts +2 -0
  84. package/dist/src/persistence/generated.js +4 -0
  85. package/dist/src/persistence/guards.d.ts +7 -0
  86. package/dist/src/persistence/guards.js +120 -0
  87. package/dist/src/persistence/index.d.ts +6 -0
  88. package/dist/src/persistence/index.js +5 -0
  89. package/dist/src/persistence/plans.d.ts +5 -0
  90. package/dist/src/persistence/plans.js +34 -0
  91. package/dist/src/persistence/state.d.ts +7 -0
  92. package/dist/src/persistence/state.js +34 -0
  93. package/dist/src/persistence/verification.d.ts +5 -0
  94. package/dist/src/persistence/verification.js +47 -0
  95. package/dist/src/planner/assemble.d.ts +16 -0
  96. package/dist/src/planner/assemble.js +107 -0
  97. package/dist/src/planner/check.d.ts +20 -0
  98. package/dist/src/planner/check.js +96 -0
  99. package/dist/src/planner/fingerprint.d.ts +12 -0
  100. package/dist/src/planner/fingerprint.js +164 -0
  101. package/dist/src/planner/index.d.ts +13 -0
  102. package/dist/src/planner/index.js +7 -0
  103. package/dist/src/planner/plan.d.ts +14 -0
  104. package/dist/src/planner/plan.js +156 -0
  105. package/dist/src/planner/repair.d.ts +28 -0
  106. package/dist/src/planner/repair.js +131 -0
  107. package/dist/src/planner/stale.d.ts +7 -0
  108. package/dist/src/planner/stale.js +23 -0
  109. package/dist/src/planner/steps.d.ts +37 -0
  110. package/dist/src/planner/steps.js +165 -0
  111. package/dist/src/planner/target-catalog.d.ts +13 -0
  112. package/dist/src/planner/target-catalog.js +48 -0
  113. package/dist/src/planner/upgrade.d.ts +18 -0
  114. package/dist/src/planner/upgrade.js +105 -0
  115. package/dist/src/preflight/config.d.ts +10 -0
  116. package/dist/src/preflight/config.js +45 -0
  117. package/dist/src/preflight/findings.d.ts +26 -0
  118. package/dist/src/preflight/findings.js +1 -0
  119. package/dist/src/preflight/index.d.ts +7 -0
  120. package/dist/src/preflight/index.js +3 -0
  121. package/dist/src/preflight/preflight.d.ts +8 -0
  122. package/dist/src/preflight/preflight.js +174 -0
  123. package/dist/src/preflight/requirements.d.ts +11 -0
  124. package/dist/src/preflight/requirements.js +224 -0
  125. package/dist/src/ready/index.d.ts +2 -0
  126. package/dist/src/ready/index.js +1 -0
  127. package/dist/src/ready/ready.d.ts +40 -0
  128. package/dist/src/ready/ready.js +201 -0
  129. package/dist/src/registry/index.d.ts +2 -0
  130. package/dist/src/registry/index.js +1 -0
  131. package/dist/src/registry/npm-registry.d.ts +51 -0
  132. package/dist/src/registry/npm-registry.js +341 -0
  133. package/dist/src/security/index.d.ts +3 -0
  134. package/dist/src/security/index.js +2 -0
  135. package/dist/src/security/lock.d.ts +9 -0
  136. package/dist/src/security/lock.js +89 -0
  137. package/dist/src/security/redact.d.ts +6 -0
  138. package/dist/src/security/redact.js +48 -0
  139. package/dist/src/verification/index.d.ts +2 -0
  140. package/dist/src/verification/index.js +1 -0
  141. package/dist/src/verification/verify.d.ts +33 -0
  142. package/dist/src/verification/verify.js +114 -0
  143. package/package.json +49 -0
  144. package/proflow.module.json +45 -0
@@ -0,0 +1,96 @@
1
+ import { CheckStrategy } from "./steps.js";
2
+ function satisfied(reason) {
3
+ return { status: "SATISFIED", reason };
4
+ }
5
+ function notSatisfied(reason) {
6
+ return { status: "NOT_SATISFIED", reason };
7
+ }
8
+ function unknown(reason) {
9
+ return { status: "UNKNOWN", reason };
10
+ }
11
+ function moduleOf(plan, step) {
12
+ return plan.resolvedModules.find((module) => module.moduleRef === step.moduleRef);
13
+ }
14
+ export function evaluateStepCheck(step, plan, reality) {
15
+ switch (step.checkStrategy) {
16
+ case CheckStrategy.packageInstalled:
17
+ return checkPackage(step, plan, reality);
18
+ case CheckStrategy.packageAbsent:
19
+ return reality.installedVersion === undefined
20
+ ? satisfied(`${step.moduleRef} package is absent`)
21
+ : notSatisfied(`${step.moduleRef} package is still installed at ${reality.installedVersion}`);
22
+ case CheckStrategy.configMaterialized:
23
+ return checkConfig(step, plan, reality);
24
+ case CheckStrategy.lifecycleRunning:
25
+ return reality.processRunning === true
26
+ ? satisfied(`${step.moduleRef} is running`)
27
+ : notSatisfied(`${step.moduleRef} is not observed running`);
28
+ case CheckStrategy.lifecycleStopped:
29
+ return reality.processRunning === false
30
+ ? satisfied(`${step.moduleRef} is stopped`)
31
+ : notSatisfied(`${step.moduleRef} is not observed stopped`);
32
+ case CheckStrategy.externalResourceConfigured:
33
+ return reality.resourceConfigured === true
34
+ ? satisfied(`external resource ${step.moduleRef} configured`)
35
+ : notSatisfied(`external resource ${step.moduleRef} not observed configured`);
36
+ case CheckStrategy.humanVerified:
37
+ return reality.humanActionVerified === true
38
+ ? satisfied(`human action for ${step.moduleRef} verified`)
39
+ : notSatisfied(`human action for ${step.moduleRef} not observed verified`);
40
+ case CheckStrategy.migrateComplete:
41
+ return reality.migrated === true
42
+ ? satisfied(`${step.moduleRef} migrated`)
43
+ : notSatisfied(`${step.moduleRef} not observed migrated`);
44
+ case CheckStrategy.verifyChecksPass:
45
+ return checkVerify(step, plan, reality);
46
+ default:
47
+ return unknown(`unknown check strategy ${step.checkStrategy}`);
48
+ }
49
+ }
50
+ function checkPackage(step, plan, reality) {
51
+ const module = moduleOf(plan, step);
52
+ if (module === undefined) {
53
+ return unknown(`module ${step.moduleRef} is not in the plan`);
54
+ }
55
+ if (reality.installedVersion === undefined) {
56
+ return notSatisfied(`installed version for ${step.moduleRef} is not observed`);
57
+ }
58
+ if (reality.installedVersion !== module.moduleVersion) {
59
+ return notSatisfied(`installed ${reality.installedVersion} != expected ${module.moduleVersion}`);
60
+ }
61
+ return satisfied(`installed ${module.moduleVersion} matches target`);
62
+ }
63
+ function checkConfig(step, plan, reality) {
64
+ const module = moduleOf(plan, step);
65
+ if (module === undefined) {
66
+ return unknown(`module ${step.moduleRef} is not in the plan`);
67
+ }
68
+ if (reality.configValues === undefined) {
69
+ return notSatisfied(`config for ${step.moduleRef} is not materialized`);
70
+ }
71
+ for (const slot of module.configSlots) {
72
+ if (!slot.required)
73
+ continue;
74
+ const value = reality.configValues[slot.key];
75
+ if (value === undefined || value === "") {
76
+ return notSatisfied(`required config ${slot.key} is missing`);
77
+ }
78
+ }
79
+ return satisfied(`required config materialized for ${step.moduleRef}`);
80
+ }
81
+ function checkVerify(step, plan, reality) {
82
+ const module = moduleOf(plan, step);
83
+ if (module === undefined) {
84
+ return unknown(`module ${step.moduleRef} is not in the plan`);
85
+ }
86
+ if (reality.verificationChecks === undefined) {
87
+ return notSatisfied(`verification for ${step.moduleRef} is not run`);
88
+ }
89
+ const byId = new Map(reality.verificationChecks.map((check) => [check.id, check.status]));
90
+ for (const check of module.verification.checks) {
91
+ if (byId.get(check.id) !== "PASS") {
92
+ return notSatisfied(`verification check ${check.id} is not passing`);
93
+ }
94
+ }
95
+ return satisfied(`verification checks pass for ${step.moduleRef}`);
96
+ }
@@ -0,0 +1,12 @@
1
+ import type { DeploymentPlan } from "../contracts.ts";
2
+ export interface FingerprintSource {
3
+ intent: string;
4
+ targets: string;
5
+ modules: string;
6
+ steps: string;
7
+ effects: string;
8
+ humanActions: string;
9
+ verification: string;
10
+ }
11
+ export declare function buildFingerprintSource(plan: DeploymentPlan): FingerprintSource;
12
+ export declare function computeFingerprint(plan: DeploymentPlan): string;
@@ -0,0 +1,164 @@
1
+ import { createHash } from "node:crypto";
2
+ function compareRef(a, b) {
3
+ return a < b ? -1 : a > b ? 1 : 0;
4
+ }
5
+ function stableStringify(value) {
6
+ if (value === null || value === undefined)
7
+ return "null";
8
+ if (typeof value !== "object")
9
+ return JSON.stringify(value);
10
+ if (Array.isArray(value)) {
11
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
12
+ }
13
+ const record = value;
14
+ const keys = Object.keys(record).sort();
15
+ return `{${keys
16
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
17
+ .join(",")}}`;
18
+ }
19
+ function sha256(value) {
20
+ return createHash("sha256").update(value).digest("hex");
21
+ }
22
+ // Maps each moduleRef to the config keys that hold RAW secret values and must
23
+ // be redacted from the fingerprint. secretRef slots are explicitly excluded:
24
+ // their values are opaque reference identities (e.g. `secret://…`), which are
25
+ // fingerprinted verbatim so a reference change (A→B) changes the fingerprint.
26
+ function rawSecretKeyMap(modules) {
27
+ const map = new Map();
28
+ for (const module of modules) {
29
+ const set = new Set();
30
+ for (const slot of module.configSlots) {
31
+ if (slot.type !== "secretRef" && slot.sensitive === true) {
32
+ set.add(slot.key);
33
+ }
34
+ }
35
+ map.set(module.moduleRef, set);
36
+ }
37
+ return map;
38
+ }
39
+ function canonicalModules(modules) {
40
+ return [...modules]
41
+ .sort((a, b) => compareRef(a.moduleRef, b.moduleRef))
42
+ .map((module) => ({
43
+ moduleRef: module.moduleRef,
44
+ packageName: module.packageName,
45
+ moduleVersion: module.moduleVersion,
46
+ kind: module.kind,
47
+ installClass: module.installClass,
48
+ identity: module.identity,
49
+ documentation: [...module.documentation]
50
+ .sort((a, b) => compareRef(a.id, b.id))
51
+ .map((entry) => ({
52
+ id: entry.id,
53
+ path: entry.path,
54
+ description: entry.description ?? null,
55
+ })),
56
+ provides: [...module.provides]
57
+ .sort((a, b) => compareRef(a.contractRef, b.contractRef))
58
+ .map((provide) => ({
59
+ contractRef: provide.contractRef,
60
+ version: provide.version,
61
+ })),
62
+ requires: [...module.requires]
63
+ .sort((a, b) => compareRef(a.contractRef, b.contractRef))
64
+ .map((require) => ({
65
+ contractRef: require.contractRef,
66
+ versionRange: require.versionRange,
67
+ optional: require.optional ?? false,
68
+ })),
69
+ requirements: [...module.requirements]
70
+ .sort((a, b) => compareRef(stableStringify(a), stableStringify(b)))
71
+ .map((requirement) => requirement),
72
+ configSlots: [...module.configSlots]
73
+ .sort((a, b) => compareRef(a.key, b.key))
74
+ .map((slot) => ({
75
+ key: slot.key,
76
+ type: slot.type,
77
+ required: slot.required,
78
+ sensitive: slot.sensitive ?? false,
79
+ default: slot.default === undefined ? null : stableStringify(slot.default),
80
+ })),
81
+ lifecycle: [...module.lifecycle].sort(compareRef),
82
+ verification: [...module.verification.checks]
83
+ .sort((a, b) => compareRef(a.id, b.id))
84
+ .map((check) => ({
85
+ id: check.id,
86
+ lifecycle: check.lifecycle,
87
+ description: check.description,
88
+ })),
89
+ effects: [...module.effects]
90
+ .sort((a, b) => compareRef(`${a.kind}:${a.description}`, `${b.kind}:${b.description}`))
91
+ .map((effect) => ({
92
+ kind: effect.kind,
93
+ description: effect.description,
94
+ path: effect.path ?? null,
95
+ retention: effect.retention,
96
+ })),
97
+ }));
98
+ }
99
+ function redactConfig(config, rawSecretKeys) {
100
+ const result = {};
101
+ for (const key of Object.keys(config).sort()) {
102
+ result[key] = rawSecretKeys.has(key) ? "<redacted>" : (config[key] ?? "");
103
+ }
104
+ return result;
105
+ }
106
+ function redactedTargets(targets, rawSecretByRef) {
107
+ return [...targets]
108
+ .sort((a, b) => compareRef(a.moduleRef, b.moduleRef))
109
+ .map((target) => {
110
+ const rawSecretKeys = rawSecretByRef.get(target.moduleRef) ?? new Set();
111
+ const config = target.config === undefined
112
+ ? null
113
+ : redactConfig(target.config, rawSecretKeys);
114
+ return {
115
+ moduleRef: target.moduleRef,
116
+ targetVersion: target.targetVersion ?? null,
117
+ config,
118
+ };
119
+ });
120
+ }
121
+ function canonicalSteps(steps) {
122
+ return steps.map((step) => ({
123
+ stepRef: step.stepRef,
124
+ moduleRef: step.moduleRef,
125
+ kind: step.kind,
126
+ preconditions: [...step.preconditions].sort(compareRef),
127
+ expectedEffect: step.expectedEffect,
128
+ checkStrategy: step.checkStrategy,
129
+ executeStrategy: step.executeStrategy ?? null,
130
+ postcondition: step.postcondition,
131
+ }));
132
+ }
133
+ export function buildFingerprintSource(plan) {
134
+ const rawSecretByRef = rawSecretKeyMap(plan.resolvedModules);
135
+ return {
136
+ intent: plan.intent,
137
+ targets: stableStringify(redactedTargets(plan.moduleTargets, rawSecretByRef)),
138
+ modules: stableStringify(canonicalModules(plan.resolvedModules)),
139
+ steps: stableStringify(canonicalSteps(plan.steps)),
140
+ effects: stableStringify([...plan.effects]
141
+ .sort((a, b) => compareRef(`${a.kind}:${a.description}`, `${b.kind}:${b.description}`))
142
+ .map((effect) => ({
143
+ kind: effect.kind,
144
+ description: effect.description,
145
+ path: effect.path ?? null,
146
+ retention: effect.retention,
147
+ }))),
148
+ humanActions: stableStringify([...plan.humanActions]
149
+ .sort((a, b) => compareRef(a.action, b.action))
150
+ .map((action) => ({
151
+ action: action.action,
152
+ description: action.description,
153
+ }))),
154
+ verification: stableStringify([...plan.verification]
155
+ .sort((a, b) => compareRef(a.moduleRef, b.moduleRef))
156
+ .map((verification) => ({
157
+ moduleRef: verification.moduleRef,
158
+ checks: [...verification.checks].sort(compareRef),
159
+ }))),
160
+ };
161
+ }
162
+ export function computeFingerprint(plan) {
163
+ return sha256(stableStringify(buildFingerprintSource(plan)));
164
+ }
@@ -0,0 +1,13 @@
1
+ export type { StepCheckResult, StepCheckStatus, StepReality, VerificationCheckResult, } from "./check.ts";
2
+ export { evaluateStepCheck } from "./check.ts";
3
+ export type { FingerprintSource } from "./fingerprint.ts";
4
+ export { computeFingerprint } from "./fingerprint.ts";
5
+ export type { PlanInput } from "./plan.ts";
6
+ export { planDeployment } from "./plan.ts";
7
+ export type { RepairFact, RepairFactCode, RepairPlanInput } from "./repair.ts";
8
+ export { planRepair, repairFactCodes } from "./repair.ts";
9
+ export type { StalenessResult } from "./stale.ts";
10
+ export { checkPlanStale } from "./stale.ts";
11
+ export { CheckStrategy, ExecuteStrategy } from "./steps.ts";
12
+ export type { UpgradeAssessment } from "./upgrade.ts";
13
+ export { assessUpgrade, planUpgrade } from "./upgrade.ts";
@@ -0,0 +1,7 @@
1
+ export { evaluateStepCheck } from "./check.js";
2
+ export { computeFingerprint } from "./fingerprint.js";
3
+ export { planDeployment } from "./plan.js";
4
+ export { planRepair, repairFactCodes } from "./repair.js";
5
+ export { checkPlanStale } from "./stale.js";
6
+ export { CheckStrategy, ExecuteStrategy } from "./steps.js";
7
+ export { assessUpgrade, planUpgrade } from "./upgrade.js";
@@ -0,0 +1,14 @@
1
+ import type { ModuleDescriptor } from "@tomflow/proflow-module-contract";
2
+ import type { DeploymentIntent, DeploymentPlan, ModuleTarget, ResolvedModule } from "../contracts.ts";
3
+ import type { RepairFact } from "./repair.ts";
4
+ export interface PlanInput {
5
+ intent: DeploymentIntent;
6
+ modules?: readonly ResolvedModule[];
7
+ targets?: readonly ModuleTarget[];
8
+ config?: Record<string, Record<string, string>>;
9
+ currentDescriptors?: readonly ModuleDescriptor[];
10
+ targetDescriptors?: readonly ModuleDescriptor[];
11
+ facts?: readonly RepairFact[];
12
+ now?: Date;
13
+ }
14
+ export declare function planDeployment(input: PlanInput): DeploymentPlan;
@@ -0,0 +1,156 @@
1
+ import { PlatformError } from "../errors.js";
2
+ import { buildDependencyGraph } from "../graph/graph.js";
3
+ import { assemblePlan } from "./assemble.js";
4
+ import { planRepair } from "./repair.js";
5
+ import { configStep, createSequencer, externalResourceStep, humanActionsFromModules, humanStep, lifecycleStep, packageStep, upstreamDependencies, } from "./steps.js";
6
+ import { planUpgrade } from "./upgrade.js";
7
+ export function planDeployment(input) {
8
+ switch (input.intent) {
9
+ case "upgrade":
10
+ return input.modules?.every((module) => module.source.type === "registry")
11
+ ? planRegistryPackageUpgrade(input)
12
+ : planUpgrade(input);
13
+ case "repair":
14
+ return planRepair(input);
15
+ case "uninstall":
16
+ return planUninstall(input);
17
+ case "install":
18
+ case "configure":
19
+ return planInstallOrConfigure(input);
20
+ }
21
+ }
22
+ function planRegistryPackageUpgrade(input) {
23
+ const modules = input.modules;
24
+ if (modules === undefined) {
25
+ throw new PlatformError("INVALID_REQUEST", "registry-backed upgrade requires modules");
26
+ }
27
+ const graph = buildDependencyGraph(modules);
28
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
29
+ const seq = createSequencer();
30
+ const steps = [];
31
+ for (const ref of graph.order) {
32
+ const module = byRef.get(ref);
33
+ if (module === undefined)
34
+ continue;
35
+ steps.push(packageStep(seq, module, "upgrade", []));
36
+ }
37
+ return assemblePlan({
38
+ intent: "upgrade",
39
+ modules,
40
+ targets: input.targets ?? [],
41
+ config: undefined,
42
+ steps,
43
+ humanActions: [],
44
+ now: input.now ?? new Date(),
45
+ });
46
+ }
47
+ function planInstallOrConfigure(input) {
48
+ const modules = input.modules;
49
+ if (modules === undefined) {
50
+ throw new PlatformError("INVALID_REQUEST", `${input.intent} requires modules`);
51
+ }
52
+ const graph = buildDependencyGraph(modules);
53
+ const steps = input.intent === "install"
54
+ ? installSteps(modules, graph)
55
+ : configureSteps(modules, graph);
56
+ return assemblePlan({
57
+ intent: input.intent,
58
+ modules,
59
+ targets: input.targets ?? [],
60
+ config: input.config,
61
+ steps,
62
+ humanActions: humanActionsFromModules(modules),
63
+ now: input.now ?? new Date(),
64
+ });
65
+ }
66
+ function installSteps(modules, graph) {
67
+ const seq = createSequencer();
68
+ const deps = upstreamDependencies(graph);
69
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
70
+ const steps = [];
71
+ for (const ref of graph.order) {
72
+ const module = byRef.get(ref);
73
+ if (module === undefined)
74
+ continue;
75
+ const dependencies = deps.get(ref) ?? [];
76
+ steps.push(packageStep(seq, module, "install", dependencies));
77
+ if (module.configSlots.length > 0) {
78
+ steps.push(configStep(seq, module));
79
+ }
80
+ if (module.kind === "external-resource") {
81
+ const step = externalResourceStep(seq, module);
82
+ if (step !== undefined)
83
+ steps.push(step);
84
+ }
85
+ for (const requirement of module.requirements) {
86
+ if (requirement.kind === "human") {
87
+ steps.push(humanStep(seq, module, requirement.action));
88
+ }
89
+ }
90
+ }
91
+ return steps;
92
+ }
93
+ function configureSteps(modules, graph) {
94
+ const seq = createSequencer();
95
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
96
+ const steps = [];
97
+ for (const ref of graph.order) {
98
+ const module = byRef.get(ref);
99
+ if (module === undefined)
100
+ continue;
101
+ if (module.configSlots.length > 0) {
102
+ steps.push(configStep(seq, module));
103
+ }
104
+ if (module.kind === "external-resource") {
105
+ const step = externalResourceStep(seq, module);
106
+ if (step !== undefined)
107
+ steps.push(step);
108
+ }
109
+ if (module.kind === "service") {
110
+ if (module.lifecycle.includes("restart")) {
111
+ steps.push(lifecycleStep(seq, module, "restart"));
112
+ }
113
+ else if (module.lifecycle.includes("start")) {
114
+ steps.push(lifecycleStep(seq, module, "start"));
115
+ }
116
+ }
117
+ for (const requirement of module.requirements) {
118
+ if (requirement.kind === "human") {
119
+ steps.push(humanStep(seq, module, requirement.action));
120
+ }
121
+ }
122
+ }
123
+ return steps;
124
+ }
125
+ function planUninstall(input) {
126
+ const modules = input.modules;
127
+ if (modules === undefined || modules.length === 0) {
128
+ throw new PlatformError("INVALID_REQUEST", "uninstall requires modules");
129
+ }
130
+ const core = modules.find((module) => module.installClass === "core");
131
+ if (core !== undefined) {
132
+ throw new PlatformError("CORE_PACKAGE_REQUIRED", `core module ${core.moduleRef} cannot be individually uninstalled`);
133
+ }
134
+ const graph = buildDependencyGraph(modules);
135
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
136
+ const seq = createSequencer();
137
+ const steps = [];
138
+ for (const ref of [...graph.order].reverse()) {
139
+ const module = byRef.get(ref);
140
+ if (module === undefined)
141
+ continue;
142
+ if (module.lifecycle.includes("uninstall")) {
143
+ steps.push(lifecycleStep(seq, module, "uninstall"));
144
+ }
145
+ steps.push(packageStep(seq, module, "remove", []));
146
+ }
147
+ return assemblePlan({
148
+ intent: "uninstall",
149
+ modules,
150
+ targets: input.targets ?? [],
151
+ config: undefined,
152
+ steps,
153
+ humanActions: [],
154
+ now: input.now ?? new Date(),
155
+ });
156
+ }
@@ -0,0 +1,28 @@
1
+ import type { DeploymentPlan, ModuleTarget, ResolvedModule } from "../contracts.ts";
2
+ import type { DoctorReport } from "../doctor/doctor.ts";
3
+ export declare const repairFactCodes: readonly ["CONFIG_MISSING", "LIFECYCLE_NOT_RUNNING", "VERIFY_FAILED", "EXTERNAL_RESOURCE_UNAVAILABLE", "ACTION_REQUIRED", "DEPENDENCY_UNRESOLVED"];
4
+ export type RepairFactCode = (typeof repairFactCodes)[number];
5
+ export interface RepairFact {
6
+ moduleRef: string;
7
+ code: RepairFactCode;
8
+ message: string;
9
+ }
10
+ export interface RepairPlanInput {
11
+ modules?: readonly ResolvedModule[];
12
+ targets?: readonly ModuleTarget[];
13
+ config?: Record<string, Record<string, string>>;
14
+ facts?: readonly RepairFact[];
15
+ now?: Date;
16
+ }
17
+ export interface RepairBlockedReason {
18
+ moduleRef: string;
19
+ code: string;
20
+ message: string;
21
+ }
22
+ export interface RepairDiagnosis {
23
+ facts: RepairFact[];
24
+ blocked: RepairBlockedReason[];
25
+ }
26
+ export declare function repairFactsFromDoctor(report: DoctorReport): RepairDiagnosis;
27
+ export declare function diagnoseRepair(reports: readonly DoctorReport[]): RepairDiagnosis;
28
+ export declare function planRepair(input: RepairPlanInput): DeploymentPlan;
@@ -0,0 +1,131 @@
1
+ import { PlatformError } from "../errors.js";
2
+ import { assemblePlan } from "./assemble.js";
3
+ import { configStep, createSequencer, externalResourceStep, humanStep, lifecycleStep, } from "./steps.js";
4
+ export const repairFactCodes = [
5
+ "CONFIG_MISSING",
6
+ "LIFECYCLE_NOT_RUNNING",
7
+ "VERIFY_FAILED",
8
+ "EXTERNAL_RESOURCE_UNAVAILABLE",
9
+ "ACTION_REQUIRED",
10
+ "DEPENDENCY_UNRESOLVED",
11
+ ];
12
+ // Frozen error codes that carry an automatic repair mapping. Everything else
13
+ // is left BLOCKED for human review rather than inventing a mutation.
14
+ const ERROR_CODE_TO_FACT = {
15
+ CONFIG_REQUIRED: "CONFIG_MISSING",
16
+ DEPENDENCY_UNRESOLVED: "DEPENDENCY_UNRESOLVED",
17
+ EXTERNAL_RESOURCE_UNAVAILABLE: "EXTERNAL_RESOURCE_UNAVAILABLE",
18
+ VERIFY_FAILED: "VERIFY_FAILED",
19
+ };
20
+ export function repairFactsFromDoctor(report) {
21
+ const facts = [];
22
+ const blocked = [];
23
+ if (report.nextAction.kind === "human-action") {
24
+ facts.push({
25
+ moduleRef: report.moduleRef,
26
+ code: "ACTION_REQUIRED",
27
+ message: report.nextAction.action,
28
+ });
29
+ return { facts, blocked };
30
+ }
31
+ if (report.status === "SUCCEEDED") {
32
+ return { facts, blocked };
33
+ }
34
+ if (report.errors.length === 0) {
35
+ blocked.push({
36
+ moduleRef: report.moduleRef,
37
+ code: report.status,
38
+ message: `module ${report.moduleRef} is ${report.status.toLowerCase()} with no typed error`,
39
+ });
40
+ return { facts, blocked };
41
+ }
42
+ for (const error of report.errors) {
43
+ const factCode = ERROR_CODE_TO_FACT[error.code];
44
+ if (factCode === undefined) {
45
+ blocked.push({
46
+ moduleRef: report.moduleRef,
47
+ code: error.code,
48
+ message: error.message,
49
+ });
50
+ continue;
51
+ }
52
+ facts.push({
53
+ moduleRef: report.moduleRef,
54
+ code: factCode,
55
+ message: error.message,
56
+ });
57
+ }
58
+ return { facts, blocked };
59
+ }
60
+ export function diagnoseRepair(reports) {
61
+ const facts = [];
62
+ const blocked = [];
63
+ for (const report of reports) {
64
+ const diagnosis = repairFactsFromDoctor(report);
65
+ facts.push(...diagnosis.facts);
66
+ blocked.push(...diagnosis.blocked);
67
+ }
68
+ return { facts, blocked };
69
+ }
70
+ export function planRepair(input) {
71
+ const modules = input.modules;
72
+ if (modules === undefined) {
73
+ throw new PlatformError("INVALID_REQUEST", "repair requires modules");
74
+ }
75
+ const facts = input.facts ?? [];
76
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
77
+ const seq = createSequencer();
78
+ const steps = [];
79
+ const humanActions = [];
80
+ const seen = new Set();
81
+ for (const fact of facts) {
82
+ const module = byRef.get(fact.moduleRef);
83
+ if (module === undefined)
84
+ continue;
85
+ const dedupeKey = `${fact.moduleRef}|${fact.code}`;
86
+ if (seen.has(dedupeKey))
87
+ continue;
88
+ seen.add(dedupeKey);
89
+ switch (fact.code) {
90
+ case "CONFIG_MISSING":
91
+ if (module.configSlots.length > 0) {
92
+ steps.push(configStep(seq, module));
93
+ }
94
+ break;
95
+ case "LIFECYCLE_NOT_RUNNING":
96
+ if (module.lifecycle.includes("start")) {
97
+ steps.push(lifecycleStep(seq, module, "start"));
98
+ }
99
+ break;
100
+ case "EXTERNAL_RESOURCE_UNAVAILABLE":
101
+ if (module.kind === "external-resource") {
102
+ const step = externalResourceStep(seq, module);
103
+ if (step !== undefined)
104
+ steps.push(step);
105
+ }
106
+ break;
107
+ case "ACTION_REQUIRED":
108
+ steps.push(humanStep(seq, module, fact.message));
109
+ humanActions.push({
110
+ action: fact.message,
111
+ description: `action required for ${module.moduleRef}`,
112
+ });
113
+ break;
114
+ case "VERIFY_FAILED":
115
+ // Verification re-runs after repair; no mutation is invented here.
116
+ break;
117
+ case "DEPENDENCY_UNRESOLVED":
118
+ // Resolution requires a plan/human decision; no mutation is invented.
119
+ break;
120
+ }
121
+ }
122
+ return assemblePlan({
123
+ intent: "repair",
124
+ modules,
125
+ targets: input.targets ?? [],
126
+ config: input.config,
127
+ steps,
128
+ humanActions,
129
+ now: input.now ?? new Date(),
130
+ });
131
+ }
@@ -0,0 +1,7 @@
1
+ import type { DeploymentPlan } from "../contracts.ts";
2
+ import { type PlanInput } from "./plan.ts";
3
+ export interface StalenessResult {
4
+ stale: boolean;
5
+ reasons: string[];
6
+ }
7
+ export declare function checkPlanStale(plan: DeploymentPlan, current: PlanInput): StalenessResult;
@@ -0,0 +1,23 @@
1
+ import { buildFingerprintSource } from "./fingerprint.js";
2
+ import { planDeployment } from "./plan.js";
3
+ const COMPARED_FIELDS = [
4
+ "intent",
5
+ "targets",
6
+ "modules",
7
+ "steps",
8
+ "effects",
9
+ "humanActions",
10
+ "verification",
11
+ ];
12
+ export function checkPlanStale(plan, current) {
13
+ const fresh = planDeployment(current);
14
+ const before = buildFingerprintSource(plan);
15
+ const after = buildFingerprintSource(fresh);
16
+ const reasons = [];
17
+ for (const field of COMPARED_FIELDS) {
18
+ if (before[field] !== after[field]) {
19
+ reasons.push(`${field} changed`);
20
+ }
21
+ }
22
+ return { stale: reasons.length > 0, reasons };
23
+ }