wendkeep 0.60.0 → 0.62.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/CHANGELOG.md +44 -0
- package/README.en.md +14 -7
- package/README.md +14 -7
- package/docs/en/commands/memory.md +6 -2
- package/docs/en/commands/operating-profiles.md +26 -4
- package/docs/pt-BR/commands/memory.md +6 -2
- package/docs/pt-BR/commands/operating-profiles.md +26 -3
- package/hooks/memory-handoff.mjs +1 -199
- package/hooks/memory-mode.mjs +1 -89
- package/hooks/memory-schema.mjs +1 -310
- package/hooks/memory-store.mjs +1 -900
- package/hooks/sensors-core.mjs +1 -102
- package/package.json +6 -3
- package/packages/harness/package.json +2 -1
- package/packages/harness/src/index.mjs +2 -0
- package/packages/harness/src/operating-profile.mjs +133 -0
- package/packages/harness/src/sensors-core.mjs +102 -0
- package/packages/vault/src/index.mjs +6 -0
- package/packages/vault/src/memory-handoff.mjs +199 -0
- package/packages/vault/src/memory-mode.mjs +89 -0
- package/packages/vault/src/memory-schema.mjs +310 -0
- package/packages/vault/src/memory-store.mjs +900 -0
- package/packages/vault/src/validate-core.mjs +181 -0
- package/packages/vault/src/validate-memory.mjs +128 -0
- package/src/memory.mjs +77 -11
- package/src/operating-profile.mjs +1 -133
- package/src/validate-core.mjs +1 -181
- package/src/validate-memory.mjs +1 -128
package/hooks/sensors-core.mjs
CHANGED
|
@@ -1,102 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
|
-
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
7
|
-
|
|
8
|
-
export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
|
|
9
|
-
|
|
10
|
-
export function sensorProcessEnv(vaultBase, inherited = process.env) {
|
|
11
|
-
return {
|
|
12
|
-
...inherited,
|
|
13
|
-
OBSIDIAN_VAULT_PATH: vaultBase,
|
|
14
|
-
[SENSOR_VAULT_ENV]: vaultBase,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
19
|
-
return loadSensorsDetailed(projectRoot, file).sensors;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Missing config and broken config are different failures: absent file usually means
|
|
23
|
-
// wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
|
|
24
|
-
// both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
|
|
25
|
-
export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
|
|
26
|
-
const path = join(projectRoot, file);
|
|
27
|
-
if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
|
|
28
|
-
try {
|
|
29
|
-
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
-
return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
|
|
31
|
-
} catch (e) {
|
|
32
|
-
return { sensors: [], missing: false, error: e.message, path };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Climb the directory tree looking for a project marker (wendkeep.sensors.json or
|
|
37
|
-
// .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
|
|
38
|
-
// across commands, so verify is often run from a subdirectory.
|
|
39
|
-
export function findProjectRoot(startDir) {
|
|
40
|
-
let dir = resolve(startDir);
|
|
41
|
-
for (;;) {
|
|
42
|
-
if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
|
|
43
|
-
const parent = dirname(dir);
|
|
44
|
-
if (parent === dir) return null;
|
|
45
|
-
dir = parent;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function requiredSensors(tasks) {
|
|
50
|
-
return [...new Set((tasks || []).flatMap((task) => (
|
|
51
|
-
Array.isArray(task.sensors) && task.sensors.length ? task.sensors : [task.sensor]
|
|
52
|
-
)).filter(Boolean))];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
|
|
56
|
-
const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
|
|
57
|
-
const ts = now || new Date().toISOString();
|
|
58
|
-
const evidence = [];
|
|
59
|
-
for (const id of ids) {
|
|
60
|
-
const s = byId[id];
|
|
61
|
-
if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
|
|
62
|
-
const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
|
|
63
|
-
const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
|
|
64
|
-
if (s.type === 'mutation' && s.report) {
|
|
65
|
-
// Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
|
|
66
|
-
// attach surviving mutants so verify can turn them into fix tasks.
|
|
67
|
-
try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
|
|
68
|
-
catch { /* report ausente/ilegível — segue só com o exit code */ }
|
|
69
|
-
}
|
|
70
|
-
evidence.push(entry);
|
|
71
|
-
}
|
|
72
|
-
return evidence;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
|
|
76
|
-
// (Survived | NoCoverage) as {file, line, mutator}.
|
|
77
|
-
export function parseMutationReport(json) {
|
|
78
|
-
const out = [];
|
|
79
|
-
const files = json && json.files ? json.files : {};
|
|
80
|
-
for (const [file, data] of Object.entries(files)) {
|
|
81
|
-
for (const m of (data && data.mutants) || []) {
|
|
82
|
-
if (m.status === 'Survived' || m.status === 'NoCoverage') {
|
|
83
|
-
out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// A required sensor blocks the gate when it is missing (never verified) or red at a
|
|
91
|
-
// non-warning severity. Warnings are advisory: a red warning does not block archive.
|
|
92
|
-
// Severity comes from the evidence entry (written by runSensors); absent -> critical.
|
|
93
|
-
export function evaluateGate(evidence, requiredIds) {
|
|
94
|
-
const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
|
|
95
|
-
const failing = (requiredIds || []).filter((id) => {
|
|
96
|
-
const e = byId[id];
|
|
97
|
-
if (!e) return true; // never verified
|
|
98
|
-
if (e.status === 'green') return false;
|
|
99
|
-
return (e.severity || 'critical') !== 'warning';
|
|
100
|
-
});
|
|
101
|
-
return { ok: failing.length === 0, failing };
|
|
102
|
-
}
|
|
1
|
+
export * from '../packages/harness/src/sensors-core.mjs';
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
7
7
|
"packages/*"
|
|
8
8
|
],
|
|
9
9
|
"exports": {
|
|
10
|
-
"./
|
|
10
|
+
"./harness": "./packages/harness/src/index.mjs",
|
|
11
|
+
"./vault": "./packages/vault/src/index.mjs",
|
|
12
|
+
"./*": "./*"
|
|
11
13
|
},
|
|
12
14
|
"bin": {
|
|
13
15
|
"wendkeep": "bin/wendkeep.mjs",
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
"node": ">=18"
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
|
-
"check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs",
|
|
34
|
+
"check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
33
35
|
"test": "node --test",
|
|
34
36
|
"release": "node scripts/release.mjs",
|
|
35
37
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
63
|
+
"acorn": "^8.18.0",
|
|
61
64
|
"wendkeep": "^0.57.2"
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export const OPERATING_PROFILES = Object.freeze([
|
|
2
|
+
'OFF',
|
|
3
|
+
'FLOW',
|
|
4
|
+
'GUIDE',
|
|
5
|
+
'GOVERN',
|
|
6
|
+
'ASSURE',
|
|
7
|
+
]);
|
|
8
|
+
export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
|
|
9
|
+
|
|
10
|
+
const PROFILE_SET = new Set(OPERATING_PROFILES);
|
|
11
|
+
|
|
12
|
+
function policy(profile, route, options) {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
profile,
|
|
15
|
+
route: Object.freeze(route),
|
|
16
|
+
keepCore: true,
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const OPERATING_PROFILE_POLICIES = Object.freeze({
|
|
22
|
+
OFF: policy('OFF', ['LLM'], {
|
|
23
|
+
harness: false,
|
|
24
|
+
contract: 'native',
|
|
25
|
+
requiresChange: false,
|
|
26
|
+
requiresReview: false,
|
|
27
|
+
requiresConfirmation: false,
|
|
28
|
+
}),
|
|
29
|
+
FLOW: policy('FLOW', ['E', 'V'], {
|
|
30
|
+
harness: true,
|
|
31
|
+
contract: 'flow',
|
|
32
|
+
requiresChange: false,
|
|
33
|
+
requiresReview: false,
|
|
34
|
+
requiresConfirmation: false,
|
|
35
|
+
}),
|
|
36
|
+
GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
|
|
37
|
+
harness: true,
|
|
38
|
+
contract: 'simple-change',
|
|
39
|
+
requiresChange: true,
|
|
40
|
+
requiresReview: false,
|
|
41
|
+
requiresConfirmation: false,
|
|
42
|
+
}),
|
|
43
|
+
GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
|
|
44
|
+
harness: true,
|
|
45
|
+
contract: 'change',
|
|
46
|
+
requiresChange: true,
|
|
47
|
+
requiresReview: true,
|
|
48
|
+
requiresConfirmation: false,
|
|
49
|
+
}),
|
|
50
|
+
ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
|
|
51
|
+
harness: true,
|
|
52
|
+
contract: 'change',
|
|
53
|
+
requiresChange: true,
|
|
54
|
+
requiresReview: true,
|
|
55
|
+
requiresConfirmation: true,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function invalidProfileError(value) {
|
|
60
|
+
const rendered = typeof value === 'string' ? `"${value}"` : String(value);
|
|
61
|
+
const error = new Error(
|
|
62
|
+
`Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
|
|
63
|
+
);
|
|
64
|
+
error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalProfile(value) {
|
|
69
|
+
if (typeof value !== 'string') return '';
|
|
70
|
+
return value.trim().toUpperCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeOperatingProfile(value, { strict = false } = {}) {
|
|
74
|
+
const normalized = canonicalProfile(value);
|
|
75
|
+
if (PROFILE_SET.has(normalized)) return normalized;
|
|
76
|
+
if (strict) throw invalidProfileError(value);
|
|
77
|
+
return DEFAULT_OPERATING_PROFILE;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveOperatingProfile(config = {}) {
|
|
81
|
+
const harness = config && typeof config === 'object' && !Array.isArray(config)
|
|
82
|
+
&& config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
|
|
83
|
+
? config.harness
|
|
84
|
+
: null;
|
|
85
|
+
const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
|
|
86
|
+
if (!configured) {
|
|
87
|
+
return {
|
|
88
|
+
profile: DEFAULT_OPERATING_PROFILE,
|
|
89
|
+
source: 'default',
|
|
90
|
+
valid: true,
|
|
91
|
+
configured: false,
|
|
92
|
+
raw: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const raw = harness.profile;
|
|
97
|
+
const normalized = canonicalProfile(raw);
|
|
98
|
+
if (PROFILE_SET.has(normalized)) {
|
|
99
|
+
return {
|
|
100
|
+
profile: normalized,
|
|
101
|
+
source: 'project-binding',
|
|
102
|
+
valid: true,
|
|
103
|
+
configured: true,
|
|
104
|
+
raw,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
profile: DEFAULT_OPERATING_PROFILE,
|
|
109
|
+
source: 'default-invalid',
|
|
110
|
+
valid: false,
|
|
111
|
+
configured: true,
|
|
112
|
+
raw,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function operatingProfilePolicy(value) {
|
|
117
|
+
return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setOperatingProfile(config = {}, value) {
|
|
121
|
+
const profile = normalizeOperatingProfile(value, { strict: true });
|
|
122
|
+
const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
|
123
|
+
const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
|
|
124
|
+
? base.harness
|
|
125
|
+
: {};
|
|
126
|
+
return {
|
|
127
|
+
...base,
|
|
128
|
+
harness: {
|
|
129
|
+
...harness,
|
|
130
|
+
profile,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// hooks/sensors-core.mjs — native sensor runner + evidence gate (Pilar C).
|
|
2
|
+
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
|
+
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
|
|
9
|
+
|
|
10
|
+
export function sensorProcessEnv(vaultBase, inherited = process.env) {
|
|
11
|
+
return {
|
|
12
|
+
...inherited,
|
|
13
|
+
OBSIDIAN_VAULT_PATH: vaultBase,
|
|
14
|
+
[SENSOR_VAULT_ENV]: vaultBase,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
19
|
+
return loadSensorsDetailed(projectRoot, file).sensors;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Missing config and broken config are different failures: absent file usually means
|
|
23
|
+
// wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
|
|
24
|
+
// both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
|
|
25
|
+
export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
|
|
26
|
+
const path = join(projectRoot, file);
|
|
27
|
+
if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
+
return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { sensors: [], missing: false, error: e.message, path };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Climb the directory tree looking for a project marker (wendkeep.sensors.json or
|
|
37
|
+
// .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
|
|
38
|
+
// across commands, so verify is often run from a subdirectory.
|
|
39
|
+
export function findProjectRoot(startDir) {
|
|
40
|
+
let dir = resolve(startDir);
|
|
41
|
+
for (;;) {
|
|
42
|
+
if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir) return null;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requiredSensors(tasks) {
|
|
50
|
+
return [...new Set((tasks || []).flatMap((task) => (
|
|
51
|
+
Array.isArray(task.sensors) && task.sensors.length ? task.sensors : [task.sensor]
|
|
52
|
+
)).filter(Boolean))];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
|
|
56
|
+
const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
|
|
57
|
+
const ts = now || new Date().toISOString();
|
|
58
|
+
const evidence = [];
|
|
59
|
+
for (const id of ids) {
|
|
60
|
+
const s = byId[id];
|
|
61
|
+
if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
|
|
62
|
+
const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
|
|
63
|
+
const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
|
|
64
|
+
if (s.type === 'mutation' && s.report) {
|
|
65
|
+
// Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
|
|
66
|
+
// attach surviving mutants so verify can turn them into fix tasks.
|
|
67
|
+
try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
|
|
68
|
+
catch { /* report ausente/ilegível — segue só com o exit code */ }
|
|
69
|
+
}
|
|
70
|
+
evidence.push(entry);
|
|
71
|
+
}
|
|
72
|
+
return evidence;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
|
|
76
|
+
// (Survived | NoCoverage) as {file, line, mutator}.
|
|
77
|
+
export function parseMutationReport(json) {
|
|
78
|
+
const out = [];
|
|
79
|
+
const files = json && json.files ? json.files : {};
|
|
80
|
+
for (const [file, data] of Object.entries(files)) {
|
|
81
|
+
for (const m of (data && data.mutants) || []) {
|
|
82
|
+
if (m.status === 'Survived' || m.status === 'NoCoverage') {
|
|
83
|
+
out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A required sensor blocks the gate when it is missing (never verified) or red at a
|
|
91
|
+
// non-warning severity. Warnings are advisory: a red warning does not block archive.
|
|
92
|
+
// Severity comes from the evidence entry (written by runSensors); absent -> critical.
|
|
93
|
+
export function evaluateGate(evidence, requiredIds) {
|
|
94
|
+
const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
|
|
95
|
+
const failing = (requiredIds || []).filter((id) => {
|
|
96
|
+
const e = byId[id];
|
|
97
|
+
if (!e) return true; // never verified
|
|
98
|
+
if (e.status === 'green') return false;
|
|
99
|
+
return (e.severity || 'critical') !== 'warning';
|
|
100
|
+
});
|
|
101
|
+
return { ok: failing.length === 0, failing };
|
|
102
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export * from './project-vault.mjs';
|
|
2
2
|
export * from './vault-path-safety.mjs';
|
|
3
|
+
export * from './memory-schema.mjs';
|
|
4
|
+
export * from './memory-mode.mjs';
|
|
5
|
+
export * from './memory-handoff.mjs';
|
|
6
|
+
export * from './memory-store.mjs';
|
|
7
|
+
export * from './validate-core.mjs';
|
|
8
|
+
export * from './validate-memory.mjs';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { basename, join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
6
|
+
|
|
7
|
+
function canonicalValue(value) {
|
|
8
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
9
|
+
if (!value || typeof value !== 'object') return value;
|
|
10
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function eventId(context, memoryKey, value) {
|
|
14
|
+
const digest = createHash('sha256')
|
|
15
|
+
.update(JSON.stringify([
|
|
16
|
+
context.projectId,
|
|
17
|
+
context.identity?.canonicalConversationId,
|
|
18
|
+
context.activation?.id,
|
|
19
|
+
context.turn?.id,
|
|
20
|
+
memoryKey,
|
|
21
|
+
canonicalValue(value),
|
|
22
|
+
]))
|
|
23
|
+
.digest('hex')
|
|
24
|
+
.slice(0, 24);
|
|
25
|
+
return `mem-${digest}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeEvent(context, { memoryKey, value, authority, evidence }) {
|
|
29
|
+
const cleanValue = typeof value === 'string' ? sanitizeMemoryText(value) : canonicalValue(value);
|
|
30
|
+
return {
|
|
31
|
+
v: 1,
|
|
32
|
+
event_id: eventId(context, memoryKey, cleanValue),
|
|
33
|
+
project_id: String(context.projectId || ''),
|
|
34
|
+
memory_key: memoryKey,
|
|
35
|
+
operation: 'assert',
|
|
36
|
+
value: cleanValue,
|
|
37
|
+
authority,
|
|
38
|
+
canonical_session_id: String(context.identity?.canonicalConversationId || ''),
|
|
39
|
+
activation_id: String(context.activation?.id || ''),
|
|
40
|
+
activation_epoch: Number(context.activation?.epoch || 0),
|
|
41
|
+
turn_sequence: Number(context.turn?.sequence || 0),
|
|
42
|
+
source_turn_id: String(context.turn?.id || ''),
|
|
43
|
+
observed_at: context.observedAt,
|
|
44
|
+
evidence: (evidence || []).filter(Boolean).map((item) => sanitizeMemoryText(item)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJson(path) {
|
|
49
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function filesBelow(dir, accept, found = []) {
|
|
53
|
+
let entries = [];
|
|
54
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; }
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const path = join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) filesBelow(path, accept, found);
|
|
58
|
+
else if (accept(entry.name)) found.push(path);
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function vaultRel(vaultBase, path) {
|
|
64
|
+
return relative(vaultBase, path).replaceAll('\\', '/');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nextActionFrom(summary) {
|
|
68
|
+
const match = String(summary || '').match(/(?:a\s+)?pr[oó]xima\s+(?:change\s+)?(?:ser[aá]|[ée]|:)\s+(?:a\s+)?([^.!?\n]+)/i);
|
|
69
|
+
if (!match) return null;
|
|
70
|
+
const text = sanitizeMemoryText(match[1].trim());
|
|
71
|
+
const id = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64);
|
|
73
|
+
return id && text ? { id, summary: text } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary = '', noteRel = '' } = {}) {
|
|
77
|
+
const evidence = {};
|
|
78
|
+
const slug = String(changeSlug || '').trim();
|
|
79
|
+
if (slug) {
|
|
80
|
+
const changeRoots = ['08-Mudanças', '08-Changes'];
|
|
81
|
+
let archivedDir = '';
|
|
82
|
+
for (const root of changeRoots) {
|
|
83
|
+
const archive = join(vaultBase, root, '_arquivo');
|
|
84
|
+
let names = [];
|
|
85
|
+
try { names = readdirSync(archive, { withFileTypes: true }); } catch { /* absent locale */ }
|
|
86
|
+
const match = names.find((entry) => entry.isDirectory() && (entry.name === slug || entry.name.endsWith(`-${slug}`)));
|
|
87
|
+
if (match) { archivedDir = join(archive, match.name); break; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const adrPattern = new RegExp(`^ADR-(\\d{4})-${slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.md$`, 'i');
|
|
91
|
+
const adrPath = filesBelow(vaultBase, (name) => adrPattern.test(name))[0];
|
|
92
|
+
if (archivedDir && adrPath) {
|
|
93
|
+
const adr = (basename(adrPath).match(/^ADR-\d{4}/i) || [''])[0].toUpperCase();
|
|
94
|
+
evidence.change = { slug, status: 'archived', adr, path: vaultRel(vaultBase, adrPath) };
|
|
95
|
+
const verdictPath = join(archivedDir, 'verdict.json');
|
|
96
|
+
const verdict = readJson(verdictPath);
|
|
97
|
+
if (verdict && typeof verdict.ok === 'boolean' && Array.isArray(verdict.coverage)) {
|
|
98
|
+
evidence.verdict = {
|
|
99
|
+
ok: verdict.ok,
|
|
100
|
+
covered: verdict.coverage.filter((item) => item?.covered === true).length,
|
|
101
|
+
total: verdict.coverage.length,
|
|
102
|
+
path: vaultRel(vaultBase, verdictPath),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const sensorPath = join(archivedDir, 'evidencia.json');
|
|
106
|
+
const sensors = readJson(sensorPath);
|
|
107
|
+
if (Array.isArray(sensors) && sensors.length && sensors.every((item) => item?.status === 'green')) {
|
|
108
|
+
evidence.sensors = [...new Set(sensors.map((item) => String(item.id || '')).filter(Boolean))].sort();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nextAction = nextActionFrom(summary);
|
|
114
|
+
if (nextAction) evidence.nextAction = nextAction;
|
|
115
|
+
const commit = String(summary || '').match(/\b[0-9a-f]{40}\b/i)?.[0];
|
|
116
|
+
if (commit) {
|
|
117
|
+
evidence.git = {
|
|
118
|
+
commit: commit.toLowerCase(),
|
|
119
|
+
pushed: !/(?:nenhum|sem)\s+push/i.test(String(summary || '')),
|
|
120
|
+
verified: false,
|
|
121
|
+
path: noteRel,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return evidence;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildSessionMemoryEvents({
|
|
128
|
+
projectId,
|
|
129
|
+
identity,
|
|
130
|
+
activation,
|
|
131
|
+
turn,
|
|
132
|
+
noteRel,
|
|
133
|
+
observedAt,
|
|
134
|
+
summary,
|
|
135
|
+
evidence = {},
|
|
136
|
+
}) {
|
|
137
|
+
const context = { projectId, identity, activation, turn, observedAt };
|
|
138
|
+
const events = [makeEvent(context, {
|
|
139
|
+
memoryKey: 'handoff.latest',
|
|
140
|
+
value: sanitizeMemoryText(summary),
|
|
141
|
+
authority: 'reported',
|
|
142
|
+
evidence: [noteRel],
|
|
143
|
+
})];
|
|
144
|
+
|
|
145
|
+
if (evidence.change?.slug && evidence.change?.status && evidence.change?.adr) {
|
|
146
|
+
events.push(makeEvent(context, {
|
|
147
|
+
memoryKey: `change.${evidence.change.slug}.status`,
|
|
148
|
+
value: { status: evidence.change.status, adr: evidence.change.adr },
|
|
149
|
+
authority: 'verified',
|
|
150
|
+
evidence: [evidence.change.path || evidence.change.adr],
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (evidence.verdict?.path && typeof evidence.verdict.ok === 'boolean') {
|
|
155
|
+
events.push(makeEvent(context, {
|
|
156
|
+
memoryKey: 'quality.latest-verdict',
|
|
157
|
+
value: {
|
|
158
|
+
ok: evidence.verdict.ok,
|
|
159
|
+
covered: Number(evidence.verdict.covered || 0),
|
|
160
|
+
total: Number(evidence.verdict.total || 0),
|
|
161
|
+
},
|
|
162
|
+
authority: 'verified',
|
|
163
|
+
evidence: [evidence.verdict.path],
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(evidence.sensors) && evidence.sensors.length) {
|
|
168
|
+
events.push(makeEvent(context, {
|
|
169
|
+
memoryKey: 'quality.latest-sensors',
|
|
170
|
+
value: [...new Set(evidence.sensors.map(String))].sort(),
|
|
171
|
+
authority: 'verified',
|
|
172
|
+
evidence: evidence.sensors,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (evidence.git?.commit) {
|
|
177
|
+
events.push(makeEvent(context, {
|
|
178
|
+
memoryKey: 'git.local-head',
|
|
179
|
+
value: {
|
|
180
|
+
commit: evidence.git.commit,
|
|
181
|
+
pushed: Boolean(evidence.git.pushed),
|
|
182
|
+
push_status: evidence.git.pushed ? 'pushed' : 'nenhum push',
|
|
183
|
+
},
|
|
184
|
+
authority: evidence.git.verified === false ? 'reported' : 'verified',
|
|
185
|
+
evidence: [evidence.git.path || evidence.git.commit],
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (evidence.nextAction?.id && evidence.nextAction?.summary) {
|
|
190
|
+
events.push(makeEvent(context, {
|
|
191
|
+
memoryKey: `next.${evidence.nextAction.id}`,
|
|
192
|
+
value: sanitizeMemoryText(evidence.nextAction.summary),
|
|
193
|
+
authority: 'verified',
|
|
194
|
+
evidence: [noteRel],
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return events;
|
|
199
|
+
}
|