aienvmp 0.1.4 → 0.1.5
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/CHANGELOG.md +7 -0
- package/README.md +8 -8
- package/ROADMAP.md +6 -6
- package/package.json +1 -1
- package/src/commands/compile.js +1 -1
- package/src/commands/context.js +2 -1
- package/src/commands/dash.js +1 -1
- package/src/commands/doctor.js +7 -2
- package/src/commands/handoff.js +3 -1
- package/src/commands/intent.js +5 -2
- package/src/commands/record.js +6 -2
- package/src/doctor.js +41 -1
- package/src/manifest.js +8 -0
- package/src/render.js +12 -4
- package/src/trust.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
- Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
|
|
6
|
+
- Added multi-agent intent conflict warnings for shared runtime/package manager targets.
|
|
7
|
+
- Added trust and schema context to `context`, `handoff`, `doctor`, `AIENV.md`, and the dashboard.
|
|
8
|
+
- Repositioned the docs around AI environment coordination instead of general AI project memory.
|
|
9
|
+
|
|
3
10
|
## 0.1.4
|
|
4
11
|
|
|
5
12
|
- Added `aienvmp handoff` for next-agent environment handoff summaries.
|
package/README.md
CHANGED
|
@@ -6,22 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
**AI Environment Map.**
|
|
8
8
|
|
|
9
|
-
`aienvmp`
|
|
9
|
+
`aienvmp` is an AI environment coordination tool for shared coding machines.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- package managers
|
|
13
|
-
- Docker state
|
|
14
|
-
- project version hints
|
|
15
|
-
- planned env changes
|
|
16
|
-
- recent env changes
|
|
11
|
+
It helps Codex, Claude, Gemini, and other agents avoid silently using or installing different runtime and tool versions.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
Core loop: scan the env, give AI a preflight context, and hand off safe next steps to the next agent.
|
|
19
14
|
|
|
20
15
|
## Use
|
|
21
16
|
|
|
22
17
|
```bash
|
|
23
18
|
npx aienvmp sync
|
|
24
19
|
npx aienvmp context
|
|
20
|
+
npx aienvmp handoff
|
|
25
21
|
```
|
|
26
22
|
|
|
27
23
|
## Output
|
|
@@ -34,6 +30,10 @@ AIENV.md
|
|
|
34
30
|
.aienvmp/dashboard.html
|
|
35
31
|
```
|
|
36
32
|
|
|
33
|
+
Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
|
|
34
|
+
|
|
35
|
+
AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
|
|
36
|
+
|
|
37
37
|
## For AGENTS.md
|
|
38
38
|
|
|
39
39
|
`aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
|
package/ROADMAP.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Near Term
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- Strengthen trust states: observed, planned, changed, review, verified, stale
|
|
8
|
+
- Detect multi-agent environment intent conflicts
|
|
9
|
+
- Stabilize `.aienvmp/manifest.json` and JSON command schemas
|
|
10
|
+
- Keep `sync`, `context`, and `handoff` as the simple core flow
|
|
11
|
+
- Improve the dashboard for 10-second human review
|
|
12
12
|
|
|
13
13
|
## Next
|
|
14
14
|
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
- `pipx list`
|
|
22
22
|
- `uv tool list`
|
|
23
23
|
- Conflict detection:
|
|
24
|
-
- multiple open intents for the same target
|
|
25
24
|
- stale unresolved intents
|
|
25
|
+
- recent runtime changes without a fresh handoff
|
|
26
26
|
- package manager policy vs lockfile mismatch
|
|
27
27
|
- CI mode:
|
|
28
28
|
- stable exit codes
|
package/package.json
CHANGED
package/src/commands/compile.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function compileWorkspace(args) {
|
|
|
13
13
|
const timeline = await readTimeline(timelinePath(dir));
|
|
14
14
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
15
15
|
const policy = await loadPolicy(dir);
|
|
16
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
16
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
17
17
|
const rendered = renderAIEnv(manifest, timeline, warnings, intents, policy);
|
|
18
18
|
await fs.writeFile(aiEnvPath(dir), rendered, "utf8");
|
|
19
19
|
if (!args.quiet) {
|
package/src/commands/context.js
CHANGED
|
@@ -12,12 +12,13 @@ export async function contextWorkspace(args) {
|
|
|
12
12
|
const timeline = await readTimeline(timelinePath(dir));
|
|
13
13
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
14
|
const policy = await loadPolicy(dir);
|
|
15
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
15
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
16
|
const decision = contextDecision(warnings, intents);
|
|
17
17
|
if (args.json) {
|
|
18
18
|
console.log(JSON.stringify({
|
|
19
19
|
status: warnings.length ? "review-required" : "clear",
|
|
20
20
|
decision,
|
|
21
|
+
trust: manifest.trust || {},
|
|
21
22
|
guidance: decision,
|
|
22
23
|
workspace: manifest.workspace,
|
|
23
24
|
runtimes: manifest.runtimes,
|
package/src/commands/dash.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function dashWorkspace(args) {
|
|
|
15
15
|
const timeline = await readTimeline(timelinePath(dir));
|
|
16
16
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
17
17
|
const policy = await loadPolicy(dir);
|
|
18
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
18
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
19
19
|
const html = renderDashboard(manifest, timeline, warnings, intents, policy);
|
|
20
20
|
const out = dashboardPath(dir);
|
|
21
21
|
await fs.mkdir(path.dirname(out), { recursive: true });
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diagnose } from "../doctor.js";
|
|
2
2
|
import { readJson } from "../fsutil.js";
|
|
3
|
-
import {
|
|
3
|
+
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
4
|
+
import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
|
|
4
5
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
5
6
|
|
|
6
7
|
export async function doctorWorkspace(args) {
|
|
@@ -8,11 +9,15 @@ export async function doctorWorkspace(args) {
|
|
|
8
9
|
const manifest = await readJson(manifestPath(dir));
|
|
9
10
|
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
10
11
|
const policy = await loadPolicy(dir);
|
|
11
|
-
const
|
|
12
|
+
const timeline = await readTimeline(timelinePath(dir));
|
|
13
|
+
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
12
15
|
if (args.json) {
|
|
13
16
|
console.log(JSON.stringify({
|
|
14
17
|
status: warnings.length ? "warning" : "ok",
|
|
18
|
+
trust: manifest.trust || {},
|
|
15
19
|
policy,
|
|
20
|
+
openIntentCount: intents.length,
|
|
16
21
|
warnings
|
|
17
22
|
}, null, 2));
|
|
18
23
|
if (args.ci && warnings.length) {
|
package/src/commands/handoff.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function handoffWorkspace(args) {
|
|
|
12
12
|
const timeline = await readTimeline(timelinePath(dir));
|
|
13
13
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
14
|
const policy = await loadPolicy(dir);
|
|
15
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
15
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
16
|
const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
|
|
17
17
|
|
|
18
18
|
if (args.json) {
|
|
@@ -27,6 +27,8 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
27
27
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
28
28
|
return {
|
|
29
29
|
status: reviewRequired ? "review-required" : "clear",
|
|
30
|
+
trust: manifest.trust || {},
|
|
31
|
+
schemaVersion: manifest.schemaVersion || 1,
|
|
30
32
|
workspace: manifest.workspace,
|
|
31
33
|
safeRuntime: {
|
|
32
34
|
node: manifest.runtimes?.node || "not detected",
|
package/src/commands/intent.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { appendJsonLine } from "../fsutil.js";
|
|
2
2
|
import { intentsPath, workspaceDir } from "../paths.js";
|
|
3
3
|
import { newIntentID } from "../timeline.js";
|
|
4
|
+
import { plannedTrust } from "../trust.js";
|
|
4
5
|
|
|
5
6
|
export async function intentWorkspace(args) {
|
|
6
7
|
const dir = workspaceDir(args);
|
|
7
8
|
const actor = required(args.actor, "actor");
|
|
8
9
|
const action = required(args.action, "action");
|
|
10
|
+
const now = new Date();
|
|
9
11
|
const entry = {
|
|
10
|
-
at:
|
|
12
|
+
at: now.toISOString(),
|
|
11
13
|
type: "intent",
|
|
12
14
|
actor,
|
|
13
15
|
action,
|
|
14
16
|
target: args.target || "",
|
|
15
17
|
reason: args.reason || "",
|
|
16
|
-
status: "open"
|
|
18
|
+
status: "open",
|
|
19
|
+
trust: plannedTrust(now)
|
|
17
20
|
};
|
|
18
21
|
entry.id = newIntentID();
|
|
19
22
|
await appendJsonLine(intentsPath(dir), entry);
|
package/src/commands/record.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { appendJsonLine } from "../fsutil.js";
|
|
2
2
|
import { timelinePath, workspaceDir } from "../paths.js";
|
|
3
|
+
import { changedTrust } from "../trust.js";
|
|
3
4
|
|
|
4
5
|
export async function recordWorkspace(args) {
|
|
5
6
|
const dir = workspaceDir(args);
|
|
6
7
|
const actor = required(args.actor, "actor");
|
|
7
8
|
const summary = required(args.summary || args.change, "summary");
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const requiresReview = args.review === true || args.review === "true";
|
|
8
11
|
const entry = {
|
|
9
|
-
at:
|
|
12
|
+
at: now.toISOString(),
|
|
10
13
|
actor,
|
|
11
14
|
type: args.type || "agent-record",
|
|
12
15
|
summary,
|
|
@@ -14,7 +17,8 @@ export async function recordWorkspace(args) {
|
|
|
14
17
|
before: args.before || "",
|
|
15
18
|
after: args.after || "",
|
|
16
19
|
evidence: args.evidence || "",
|
|
17
|
-
requiresReview
|
|
20
|
+
requiresReview,
|
|
21
|
+
trust: changedTrust(now, requiresReview)
|
|
18
22
|
};
|
|
19
23
|
await appendJsonLine(timelinePath(dir), entry);
|
|
20
24
|
console.log(`recorded ${entry.type} by ${actor}`);
|
package/src/doctor.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { isStaleTimestamp } from "./trust.js";
|
|
2
|
+
|
|
3
|
+
export function diagnose(manifest, context = {}) {
|
|
2
4
|
const warnings = [];
|
|
3
5
|
const hints = manifest.projectHints || {};
|
|
4
6
|
const runtimes = manifest.runtimes || {};
|
|
@@ -34,5 +36,43 @@ export function diagnose(manifest) {
|
|
|
34
36
|
message: "Dockerfile detected, but Docker CLI was not found."
|
|
35
37
|
});
|
|
36
38
|
}
|
|
39
|
+
if (isStaleTimestamp(manifest.generatedAt)) {
|
|
40
|
+
warnings.push({
|
|
41
|
+
code: "manifest-stale",
|
|
42
|
+
message: "Environment snapshot is older than 24 hours. Run `aienvmp sync` before changing the environment."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
warnings.push(...coordinationWarnings(context.intents || []));
|
|
46
|
+
return warnings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function coordinationWarnings(intents = []) {
|
|
50
|
+
const warnings = [];
|
|
51
|
+
const byTarget = new Map();
|
|
52
|
+
for (const intent of intents) {
|
|
53
|
+
const target = String(intent.target || inferTarget(intent.action) || "").trim().toLowerCase();
|
|
54
|
+
if (!target) continue;
|
|
55
|
+
const list = byTarget.get(target) || [];
|
|
56
|
+
list.push(intent);
|
|
57
|
+
byTarget.set(target, list);
|
|
58
|
+
}
|
|
59
|
+
for (const [target, list] of byTarget) {
|
|
60
|
+
const actors = new Set(list.map((intent) => intent.actor).filter(Boolean));
|
|
61
|
+
if (list.length > 1 && actors.size > 1) {
|
|
62
|
+
warnings.push({
|
|
63
|
+
code: "conflicting-open-intents",
|
|
64
|
+
target,
|
|
65
|
+
message: `Multiple agents have open environment intents for ${target}. Resolve or coordinate before changing it.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
37
69
|
return warnings;
|
|
38
70
|
}
|
|
71
|
+
|
|
72
|
+
function inferTarget(action = "") {
|
|
73
|
+
const normalized = String(action).toLowerCase();
|
|
74
|
+
for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
|
|
75
|
+
if (normalized.includes(target)) return target;
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { commandOutput, commandVersion } from "./shell.js";
|
|
5
5
|
import { exists } from "./fsutil.js";
|
|
6
|
+
import { observedTrust } from "./trust.js";
|
|
6
7
|
|
|
7
8
|
export async function buildManifest(dir) {
|
|
8
9
|
const now = new Date().toISOString();
|
|
@@ -14,6 +15,7 @@ export async function buildManifest(dir) {
|
|
|
14
15
|
name: "aienvmp",
|
|
15
16
|
command: "aienvmp sync"
|
|
16
17
|
},
|
|
18
|
+
trust: observedTrust(new Date(now)),
|
|
17
19
|
workspace: {
|
|
18
20
|
path: dir,
|
|
19
21
|
name: path.basename(dir)
|
|
@@ -27,9 +29,15 @@ export async function buildManifest(dir) {
|
|
|
27
29
|
agentProtocol: {
|
|
28
30
|
sourceOfTruth: "AIENV.md",
|
|
29
31
|
preflightCommand: "aienvmp context",
|
|
32
|
+
handoffCommand: "aienvmp handoff",
|
|
30
33
|
intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
|
|
31
34
|
recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
|
|
32
35
|
afterEnvironmentChange: ["aienvmp sync"],
|
|
36
|
+
trustModel: {
|
|
37
|
+
agentWritable: ["observed", "planned", "changed", "review", "stale"],
|
|
38
|
+
verifiedRequires: "human-or-ci",
|
|
39
|
+
rule: "AI agents may report observations and plans, but must not mark environment facts as verified."
|
|
40
|
+
},
|
|
33
41
|
globalRuntimeChangeRequiresUserApproval: true,
|
|
34
42
|
globalInstallPolicy: "ask-first",
|
|
35
43
|
projectLocalChanges: "allowed-when-task-requires"
|
package/src/render.js
CHANGED
|
@@ -19,6 +19,9 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
19
19
|
lines.push(...policyLines(policy));
|
|
20
20
|
lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
|
|
21
21
|
lines.push("- Project-local dependency installs: allowed when required by the user task.", "");
|
|
22
|
+
lines.push("## Trust State", "");
|
|
23
|
+
lines.push(`- State: ${manifest.trust?.state || "observed"}`);
|
|
24
|
+
lines.push("- Rule: AI agents may observe, plan, and record changes, but verified requires human or CI review.", "");
|
|
22
25
|
lines.push("## AI Preflight Summary", "");
|
|
23
26
|
lines.push(...contextLines(manifest, warnings, intents), "");
|
|
24
27
|
lines.push("## Runtime Map", "");
|
|
@@ -83,6 +86,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
83
86
|
"",
|
|
84
87
|
`Status: ${status}`,
|
|
85
88
|
`Next: ${next}`,
|
|
89
|
+
`Trust: ${manifest.trust?.state || "observed"} (verified requires human or CI)`,
|
|
86
90
|
`Workspace: ${manifest.workspace.path}`,
|
|
87
91
|
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
88
92
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
@@ -116,6 +120,8 @@ export function renderHandoff(handoff) {
|
|
|
116
120
|
"# AI Handoff",
|
|
117
121
|
"",
|
|
118
122
|
`Status: ${handoff.status}`,
|
|
123
|
+
`Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
|
|
124
|
+
`Schema: ${handoff.schemaVersion}`,
|
|
119
125
|
`Workspace: ${handoff.workspace?.path || "unknown"}`,
|
|
120
126
|
"",
|
|
121
127
|
"Safe runtime:",
|
|
@@ -208,9 +214,11 @@ const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<di
|
|
|
208
214
|
const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
|
|
209
215
|
const reviewRequired=warnings.length>0||intents.length>0;
|
|
210
216
|
const recentChanges=timeline.slice(-8).length;
|
|
217
|
+
const trustState=manifest.trust?.state||'observed';
|
|
211
218
|
const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
|
|
212
219
|
const auditItem=(key,value,hint,klass='')=>\`<div class="audit-item \${klass}"><div class="audit-k">\${key}</div><div class="audit-v">\${value}</div><div class="audit-hint">\${hint}</div></div>\`;
|
|
213
|
-
const
|
|
220
|
+
const driftLabel=warnings.length?'detected':'none';
|
|
221
|
+
const handoffHtml=\`<table><tr><th>Status</th><td>\${reviewRequired?'review-required':'clear'}</td></tr><tr><th>Trust</th><td><code>\${esc(trustState)}</code></td></tr><tr><th>Node</th><td><code>\${esc(manifest.runtimes.node||'not detected')}</code></td></tr><tr><th>Python</th><td><code>\${esc(manifest.runtimes.python||manifest.runtimes.python3||'not detected')}</code></td></tr><tr><th>Docker</th><td>\${manifest.containers?.docker?'available':'not detected'}</td></tr><tr><th>Next</th><td>\${reviewRequired?'Review warnings and open intents':'Continue project-local work'}</td></tr></table>\`;
|
|
214
222
|
document.getElementById('app').innerHTML=\`
|
|
215
223
|
<header>
|
|
216
224
|
<div>
|
|
@@ -222,9 +230,9 @@ document.getElementById('app').innerHTML=\`
|
|
|
222
230
|
</header>
|
|
223
231
|
<section class="audit" aria-label="Audit summary">
|
|
224
232
|
\${auditItem('AI decision',reviewRequired?'review required':'can proceed',nextAction,reviewRequired?'review':'primary')}
|
|
225
|
-
\${auditItem('
|
|
226
|
-
\${auditItem('
|
|
227
|
-
\${auditItem('
|
|
233
|
+
\${auditItem('Runtime drift',driftLabel,warnings.length?'Policy, runtime, or coordination warning detected':'No drift warnings detected',warnings.length?'review':'')}
|
|
234
|
+
\${auditItem('Open env changes',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
|
|
235
|
+
\${auditItem('Trust',trustState,trustState==='verified'?'Human or CI verified':'Machine observed; not AI-verified')}
|
|
228
236
|
</section>
|
|
229
237
|
<section class="metrics">
|
|
230
238
|
<div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
|
package/src/trust.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const TRUST_STATES = {
|
|
2
|
+
OBSERVED: "observed",
|
|
3
|
+
PLANNED: "planned",
|
|
4
|
+
CHANGED: "changed",
|
|
5
|
+
REVIEW: "review",
|
|
6
|
+
VERIFIED: "verified",
|
|
7
|
+
STALE: "stale"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function observedTrust(now = new Date()) {
|
|
11
|
+
return {
|
|
12
|
+
state: TRUST_STATES.OBSERVED,
|
|
13
|
+
at: now.toISOString(),
|
|
14
|
+
by: "aienvmp scan",
|
|
15
|
+
verified: false,
|
|
16
|
+
note: "Machine-observed only. AI agents cannot mark environment facts as verified."
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function plannedTrust(now = new Date()) {
|
|
21
|
+
return {
|
|
22
|
+
state: TRUST_STATES.PLANNED,
|
|
23
|
+
at: now.toISOString(),
|
|
24
|
+
verified: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function changedTrust(now = new Date(), requiresReview = false) {
|
|
29
|
+
return {
|
|
30
|
+
state: requiresReview ? TRUST_STATES.REVIEW : TRUST_STATES.CHANGED,
|
|
31
|
+
at: now.toISOString(),
|
|
32
|
+
verified: false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isStaleTimestamp(value, now = new Date(), maxAgeHours = 24) {
|
|
37
|
+
if (!value) return false;
|
|
38
|
+
const then = new Date(value).getTime();
|
|
39
|
+
if (!Number.isFinite(then)) return false;
|
|
40
|
+
return now.getTime() - then > maxAgeHours * 60 * 60 * 1000;
|
|
41
|
+
}
|