@fitlab-ai/agent-infra 0.9.3 → 0.9.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/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +170 -27
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +110 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +127 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { detectRepoRoot } from '../task/resolve-ref.js';
|
|
5
|
+
import { normalizeObjects } from './normalize.js';
|
|
6
|
+
import { reconcileRecords } from './reconcile.js';
|
|
7
|
+
import { readAppliedOverlays, repairSnapshot } from './repair.js';
|
|
8
|
+
import { collectGitHubObjects, collectLocalObjects } from './sources.js';
|
|
9
|
+
import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.js';
|
|
10
|
+
const USAGE = `Usage: ai data <command> [options]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
capture [--source all|local|github] [--root <dir>] [--include-excerpts]
|
|
14
|
+
verify <snapshot-id> [--root <dir>]
|
|
15
|
+
audit <snapshot-id> [--root <dir>] [--format json|text]
|
|
16
|
+
repair <snapshot-id> [--root <dir>] [--apply]
|
|
17
|
+
export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
|
|
18
|
+
`;
|
|
19
|
+
function parseArgs(args, valueOptions, booleanOptions) {
|
|
20
|
+
const positionals = [];
|
|
21
|
+
const options = new Map();
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = args[index];
|
|
24
|
+
if (!arg.startsWith('--')) {
|
|
25
|
+
positionals.push(arg);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (booleanOptions.has(arg)) {
|
|
29
|
+
if (options.has(arg))
|
|
30
|
+
throw new Error(`duplicate option: ${arg}`);
|
|
31
|
+
options.set(arg, true);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!valueOptions.has(arg))
|
|
35
|
+
throw new Error(`unknown option: ${arg}`);
|
|
36
|
+
const value = args[index + 1];
|
|
37
|
+
if (!value || value.startsWith('--'))
|
|
38
|
+
throw new Error(`missing value for ${arg}`);
|
|
39
|
+
if (options.has(arg))
|
|
40
|
+
throw new Error(`duplicate option: ${arg}`);
|
|
41
|
+
options.set(arg, value);
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return { positionals, options };
|
|
45
|
+
}
|
|
46
|
+
function dataRoot(repoRoot, requested) {
|
|
47
|
+
const workspace = path.resolve(repoRoot, '.agents', 'workspace');
|
|
48
|
+
const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
|
|
49
|
+
const relative = path.relative(repoRoot, resolved);
|
|
50
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
51
|
+
throw new Error('data root must be inside the repository');
|
|
52
|
+
const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
|
|
53
|
+
if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
|
|
54
|
+
throw new Error('data root cannot be the workspace root or a task directory');
|
|
55
|
+
}
|
|
56
|
+
let current = resolved;
|
|
57
|
+
while (current !== repoRoot) {
|
|
58
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
59
|
+
throw new Error('data root cannot traverse a symlink');
|
|
60
|
+
current = path.dirname(current);
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
function repositoryIdentity(repoRoot) {
|
|
65
|
+
try {
|
|
66
|
+
return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return path.basename(repoRoot);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function writeJson(value) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
74
|
+
}
|
|
75
|
+
function optionString(parsed, name) {
|
|
76
|
+
const value = parsed.options.get(name);
|
|
77
|
+
return typeof value === 'string' ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
function capture(args) {
|
|
80
|
+
const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
|
|
81
|
+
if (parsed.positionals.length > 0)
|
|
82
|
+
throw new Error('capture does not accept positional arguments');
|
|
83
|
+
const scope = optionString(parsed, '--source') ?? 'all';
|
|
84
|
+
if (!['all', 'local', 'github'].includes(scope))
|
|
85
|
+
throw new Error('--source must be all, local, or github');
|
|
86
|
+
const repoRoot = detectRepoRoot();
|
|
87
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
88
|
+
const observedFrom = new Date().toISOString();
|
|
89
|
+
const objects = [];
|
|
90
|
+
const endpoints = [];
|
|
91
|
+
let repository = repositoryIdentity(repoRoot);
|
|
92
|
+
if (scope === 'all' || scope === 'local') {
|
|
93
|
+
const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
|
|
94
|
+
if (!local.ok) {
|
|
95
|
+
writeJson({ ok: false, error: local.error });
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
objects.push(...local.value);
|
|
99
|
+
}
|
|
100
|
+
if (scope === 'all' || scope === 'github') {
|
|
101
|
+
const github = collectGitHubObjects(repoRoot);
|
|
102
|
+
if (!github.ok) {
|
|
103
|
+
writeJson({ ok: false, error: github.error });
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
objects.push(...github.value.objects);
|
|
107
|
+
endpoints.push(...github.value.endpoints);
|
|
108
|
+
repository = github.value.repository;
|
|
109
|
+
}
|
|
110
|
+
const store = createObjectStore(root);
|
|
111
|
+
for (const object of objects) {
|
|
112
|
+
if (object.content !== undefined && object.disposition?.state === 'included') {
|
|
113
|
+
const stored = store.put(Buffer.from(object.content, 'utf8'));
|
|
114
|
+
if (stored.sha256 !== object.sha256)
|
|
115
|
+
throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const records = normalizeObjects(objects);
|
|
119
|
+
const reconciliation = reconcileRecords(records, scope);
|
|
120
|
+
const observedTo = new Date().toISOString();
|
|
121
|
+
const published = publishSnapshot(root, {
|
|
122
|
+
scope: scope,
|
|
123
|
+
repository,
|
|
124
|
+
observedFrom,
|
|
125
|
+
observedTo,
|
|
126
|
+
objects: objects.map(({ content: _content, ...object }) => object),
|
|
127
|
+
endpoints,
|
|
128
|
+
excerptsEnabled: parsed.options.has('--include-excerpts'),
|
|
129
|
+
records,
|
|
130
|
+
quality: reconciliation.findings,
|
|
131
|
+
repairs: reconciliation.repairs
|
|
132
|
+
});
|
|
133
|
+
writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
function verify(args) {
|
|
137
|
+
const parsed = parseArgs(args, new Set(['--root']), new Set());
|
|
138
|
+
if (parsed.positionals.length !== 1)
|
|
139
|
+
throw new Error('verify requires one snapshot id');
|
|
140
|
+
const repoRoot = detectRepoRoot();
|
|
141
|
+
const result = verifySnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]);
|
|
142
|
+
writeJson(result);
|
|
143
|
+
return result.ok ? 0 : 1;
|
|
144
|
+
}
|
|
145
|
+
function audit(args) {
|
|
146
|
+
const parsed = parseArgs(args, new Set(['--root', '--format']), new Set());
|
|
147
|
+
if (parsed.positionals.length !== 1)
|
|
148
|
+
throw new Error('audit requires one snapshot id');
|
|
149
|
+
const format = optionString(parsed, '--format') ?? 'text';
|
|
150
|
+
if (!['json', 'text'].includes(format))
|
|
151
|
+
throw new Error('--format must be json or text');
|
|
152
|
+
const repoRoot = detectRepoRoot();
|
|
153
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
154
|
+
const verified = verifySnapshot(root, parsed.positionals[0]);
|
|
155
|
+
if (!verified.ok || !verified.snapshotPath || !verified.manifest) {
|
|
156
|
+
writeJson(verified);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
const quality = JSON.parse(fs.readFileSync(path.join(verified.snapshotPath, 'quality.json'), 'utf8'));
|
|
160
|
+
const records = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
161
|
+
const { schema: _schema, ...record } = JSON.parse(line);
|
|
162
|
+
return record;
|
|
163
|
+
});
|
|
164
|
+
const recomputed = reconcileRecords(records, verified.manifest.scope);
|
|
165
|
+
if (!canonicalJsonBytes(recomputed.findings).equals(canonicalJsonBytes(quality.findings))) {
|
|
166
|
+
writeJson({ ok: false, error: { code: 'QUALITY_MISMATCH', message: 'stored quality baseline does not match recomputed findings' } });
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
if (format === 'json')
|
|
170
|
+
writeJson({ ok: true, snapshotId: parsed.positionals[0], findings: recomputed.findings });
|
|
171
|
+
else
|
|
172
|
+
process.stdout.write(`Snapshot ${parsed.positionals[0]}: ${quality.findings.length} findings\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
function repair(args) {
|
|
176
|
+
const parsed = parseArgs(args, new Set(['--root']), new Set(['--apply']));
|
|
177
|
+
if (parsed.positionals.length !== 1)
|
|
178
|
+
throw new Error('repair requires one snapshot id');
|
|
179
|
+
const repoRoot = detectRepoRoot();
|
|
180
|
+
const result = repairSnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0], parsed.options.has('--apply'));
|
|
181
|
+
writeJson({ ok: true, ...result });
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
function exportSnapshot(args) {
|
|
185
|
+
const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
|
|
186
|
+
if (parsed.positionals.length !== 1)
|
|
187
|
+
throw new Error('export requires one snapshot id');
|
|
188
|
+
const repairMode = optionString(parsed, '--repairs') ?? 'none';
|
|
189
|
+
if (!['none', 'applied'].includes(repairMode))
|
|
190
|
+
throw new Error('--repairs must be none or applied');
|
|
191
|
+
const output = optionString(parsed, '--output') ?? '-';
|
|
192
|
+
const repoRoot = detectRepoRoot();
|
|
193
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
194
|
+
const verified = verifySnapshot(root, parsed.positionals[0]);
|
|
195
|
+
if (!verified.ok || !verified.snapshotPath) {
|
|
196
|
+
writeJson(verified);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
|
|
200
|
+
if (repairMode === 'applied')
|
|
201
|
+
content += readAppliedOverlays(root, parsed.positionals[0]);
|
|
202
|
+
if (output === '-')
|
|
203
|
+
process.stdout.write(content);
|
|
204
|
+
else {
|
|
205
|
+
const destination = path.resolve(repoRoot, output);
|
|
206
|
+
const relative = path.relative(repoRoot, destination);
|
|
207
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
208
|
+
throw new Error('export output must be inside the repository');
|
|
209
|
+
fs.writeFileSync(destination, content, { flag: 'wx', mode: 0o600 });
|
|
210
|
+
writeJson({ ok: true, snapshotId: parsed.positionals[0], output: relative });
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
async function cmdData(args) {
|
|
215
|
+
const [command, ...rest] = args;
|
|
216
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
217
|
+
process.stdout.write(USAGE);
|
|
218
|
+
return command ? 0 : 1;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
switch (command) {
|
|
222
|
+
case 'capture': return capture(rest);
|
|
223
|
+
case 'verify': return verify(rest);
|
|
224
|
+
case 'audit': return audit(rest);
|
|
225
|
+
case 'repair': return repair(rest);
|
|
226
|
+
case 'export': return exportSnapshot(rest);
|
|
227
|
+
default:
|
|
228
|
+
process.stderr.write(`Unknown data command: ${command}\n`);
|
|
229
|
+
process.stdout.write(USAGE);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
writeJson({ ok: false, error: { code: 'DATA_COMMAND_FAILED', message: error instanceof Error ? error.message : String(error) } });
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export { cmdData, dataRoot };
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { sha256 } from './store.js';
|
|
2
|
+
const TASK_ID_RE = /TASK-\d{8}-\d{6}/;
|
|
3
|
+
const ACTIVITY_RE = /^- (\d{4}-\d{2}-\d{2} [^ ]+) — \*\*(.+?)\*\* by ([^ ]+) — (.+)$/gm;
|
|
4
|
+
const LEDGER_RE = /^\|\s*((?:AN|PL|CD)-\d+)\s*\|\s*(analysis|plan|code)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$/gm;
|
|
5
|
+
const RULING_RE = /^###\s+(HDR-\d+)\s*$([\s\S]*?)(?=^###\s+|^##\s+|(?![\s\S]))/gm;
|
|
6
|
+
const CONFLICT_STATUSES = new Set(['open', 'accepted', 'adjusted', 'refuted', 'cannot-judge', 'needs-human-decision']);
|
|
7
|
+
function recordId(kind, identity, suffix = '') {
|
|
8
|
+
return `${kind}:${sha256(`${identity}\0${suffix}`).slice(0, 24)}`;
|
|
9
|
+
}
|
|
10
|
+
function canonicalActor(raw) {
|
|
11
|
+
const lowered = raw.toLowerCase();
|
|
12
|
+
const aliases = {
|
|
13
|
+
'claude-code': 'claude',
|
|
14
|
+
'antigravity-cli': 'antigravity',
|
|
15
|
+
'gemini-cli': 'gemini',
|
|
16
|
+
gemini: 'gemini'
|
|
17
|
+
};
|
|
18
|
+
if (['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'human'].includes(lowered))
|
|
19
|
+
return lowered;
|
|
20
|
+
return aliases[lowered] ?? null;
|
|
21
|
+
}
|
|
22
|
+
function nextOccurrence(occurrences, identity) {
|
|
23
|
+
const occurrence = (occurrences.get(identity) ?? 0) + 1;
|
|
24
|
+
occurrences.set(identity, occurrence);
|
|
25
|
+
return occurrence === 1 ? '' : `occurrence=${occurrence}`;
|
|
26
|
+
}
|
|
27
|
+
function normalizePlatformPage(object) {
|
|
28
|
+
if (!object.content)
|
|
29
|
+
return [];
|
|
30
|
+
try {
|
|
31
|
+
const page = JSON.parse(object.content);
|
|
32
|
+
if (!Array.isArray(page))
|
|
33
|
+
return [];
|
|
34
|
+
return page.map((entry, index) => {
|
|
35
|
+
const value = entry;
|
|
36
|
+
const identity = String(value.id ?? value.node_id ?? value.sha ?? value.number ?? `${object.sourceIdentity}:${index}`);
|
|
37
|
+
const platformIdentity = /\/issues\?/.test(object.sourceIdentity) && typeof value.number === 'number'
|
|
38
|
+
? `issue:${value.number}`
|
|
39
|
+
: /\/pulls\?/.test(object.sourceIdentity) && typeof value.number === 'number'
|
|
40
|
+
? `pr:${value.number}`
|
|
41
|
+
: /\/issues\/\d+\/comments/.test(object.sourceIdentity)
|
|
42
|
+
? `issue-comment:${identity}`
|
|
43
|
+
: /\/pulls\/\d+\/reviews/.test(object.sourceIdentity)
|
|
44
|
+
? `review:${identity}`
|
|
45
|
+
: /\/pulls\/\d+\/commits/.test(object.sourceIdentity)
|
|
46
|
+
? `commit:${identity}`
|
|
47
|
+
: `platform:${identity}`;
|
|
48
|
+
const binding = typeof value.body === 'string' ? value.body.match(TASK_ID_RE)?.[0] : undefined;
|
|
49
|
+
return {
|
|
50
|
+
recordId: recordId('platform-resource', object.sourceIdentity, identity),
|
|
51
|
+
kind: 'platform-resource',
|
|
52
|
+
sourceIdentity: platformIdentity,
|
|
53
|
+
sourceSha256: object.sha256,
|
|
54
|
+
...(binding ? { binding } : {}),
|
|
55
|
+
data: entry
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function normalizeObjects(objects) {
|
|
64
|
+
const records = [];
|
|
65
|
+
for (const object of objects) {
|
|
66
|
+
if (object.disposition?.state && object.disposition.state !== 'included') {
|
|
67
|
+
records.push({
|
|
68
|
+
recordId: recordId('missing', object.sourceIdentity),
|
|
69
|
+
kind: 'missing',
|
|
70
|
+
sourceIdentity: object.sourceIdentity,
|
|
71
|
+
sourceSha256: object.sha256,
|
|
72
|
+
data: object.disposition.state === 'excluded-sensitive'
|
|
73
|
+
? { reason: 'privacy-excluded', ruleId: object.disposition.ruleId }
|
|
74
|
+
: { reason: object.disposition.reason }
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (object.sourceKind === 'github-rest') {
|
|
79
|
+
records.push(...normalizePlatformPage(object));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (object.sourceKind === 'operational-report') {
|
|
83
|
+
records.push({
|
|
84
|
+
recordId: recordId('operational-report', object.sourceIdentity),
|
|
85
|
+
kind: 'operational-report',
|
|
86
|
+
sourceIdentity: object.sourceIdentity,
|
|
87
|
+
sourceSha256: object.sha256
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (object.sourceKind === 'structured-telemetry') {
|
|
92
|
+
let data = { availability: 'unknown' };
|
|
93
|
+
try {
|
|
94
|
+
data = JSON.parse(object.content ?? '{}');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
data = { availability: 'unavailable', reason: 'invalid receipt JSON' };
|
|
98
|
+
}
|
|
99
|
+
records.push({
|
|
100
|
+
recordId: recordId('telemetry', object.sourceIdentity),
|
|
101
|
+
kind: 'telemetry',
|
|
102
|
+
sourceIdentity: object.sourceIdentity,
|
|
103
|
+
sourceSha256: object.sha256,
|
|
104
|
+
data
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const taskId = object.sourceIdentity.match(TASK_ID_RE)?.[0];
|
|
109
|
+
if (/\/task\.md$/.test(object.sourceIdentity) && taskId) {
|
|
110
|
+
const activityOccurrences = new Map();
|
|
111
|
+
const findingOccurrences = new Map();
|
|
112
|
+
const issueNumber = object.content?.match(/^issue_number:\s*(\d+)$/m)?.[1];
|
|
113
|
+
const agentInfraVersion = object.content?.match(/^agent_infra_version:\s*(\S+)$/m)?.[1];
|
|
114
|
+
records.push({
|
|
115
|
+
recordId: recordId('task', taskId),
|
|
116
|
+
kind: 'task',
|
|
117
|
+
sourceIdentity: taskId,
|
|
118
|
+
sourceSha256: object.sha256,
|
|
119
|
+
...(issueNumber ? { binding: `issue:${issueNumber}` } : {}),
|
|
120
|
+
data: {
|
|
121
|
+
agentInfraVersion: agentInfraVersion
|
|
122
|
+
? { state: 'known', value: agentInfraVersion }
|
|
123
|
+
: { state: 'unknown', reason: 'legacy-task-missing-agent-infra-version' }
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
for (const match of object.content?.matchAll(ACTIVITY_RE) ?? []) {
|
|
127
|
+
const rawActor = match[3];
|
|
128
|
+
const identity = `${match[1]}:${match[2]}`;
|
|
129
|
+
const occurrence = nextOccurrence(activityOccurrences, identity);
|
|
130
|
+
records.push({
|
|
131
|
+
recordId: recordId('lifecycle-event', taskId, occurrence ? `${identity}\0${occurrence}` : identity),
|
|
132
|
+
kind: 'lifecycle-event',
|
|
133
|
+
sourceIdentity: `${taskId}#activity=${match[1]}`,
|
|
134
|
+
sourceSha256: object.sha256,
|
|
135
|
+
data: {
|
|
136
|
+
timestamp: match[1],
|
|
137
|
+
action: match[2],
|
|
138
|
+
actorRaw: rawActor,
|
|
139
|
+
actorCanonical: canonicalActor(rawActor),
|
|
140
|
+
note: match[4]
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
for (const match of object.content?.matchAll(LEDGER_RE) ?? []) {
|
|
145
|
+
const findingId = match[1].trim();
|
|
146
|
+
const stage = match[2].trim();
|
|
147
|
+
const round = match[3].trim();
|
|
148
|
+
const severity = match[4].trim();
|
|
149
|
+
const status = match[5].trim();
|
|
150
|
+
const evidence = match[6].trim();
|
|
151
|
+
const identity = `${taskId}#finding=${findingId}`;
|
|
152
|
+
const occurrence = nextOccurrence(findingOccurrences, identity);
|
|
153
|
+
const data = { findingId, stage, round, severity, status, evidence };
|
|
154
|
+
records.push({
|
|
155
|
+
recordId: recordId('review-finding', identity, occurrence),
|
|
156
|
+
kind: 'review-finding',
|
|
157
|
+
sourceIdentity: identity,
|
|
158
|
+
sourceSha256: object.sha256,
|
|
159
|
+
binding: taskId,
|
|
160
|
+
data
|
|
161
|
+
});
|
|
162
|
+
if (status && CONFLICT_STATUSES.has(status)) {
|
|
163
|
+
records.push({
|
|
164
|
+
recordId: recordId('conflict', identity, occurrence),
|
|
165
|
+
kind: 'conflict',
|
|
166
|
+
sourceIdentity: `${identity}#status=${status}`,
|
|
167
|
+
sourceSha256: object.sha256,
|
|
168
|
+
binding: taskId,
|
|
169
|
+
data
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const match of object.content?.matchAll(RULING_RE) ?? []) {
|
|
174
|
+
const rulingId = match[1];
|
|
175
|
+
const identity = `${taskId}#ruling=${rulingId}`;
|
|
176
|
+
records.push({
|
|
177
|
+
recordId: recordId('human-ruling', identity),
|
|
178
|
+
kind: 'human-ruling',
|
|
179
|
+
sourceIdentity: identity,
|
|
180
|
+
sourceSha256: object.sha256,
|
|
181
|
+
binding: taskId,
|
|
182
|
+
data: { rulingId, evidence: match[2].trim() }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
records.push({
|
|
188
|
+
recordId: recordId('artifact', object.sourceIdentity),
|
|
189
|
+
kind: 'artifact',
|
|
190
|
+
sourceIdentity: object.sourceIdentity,
|
|
191
|
+
sourceSha256: object.sha256,
|
|
192
|
+
...(taskId ? { binding: taskId } : {})
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return records.sort((left, right) => left.recordId.localeCompare(right.recordId));
|
|
196
|
+
}
|
|
197
|
+
export { normalizeObjects };
|
|
198
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sha256 } from './store.js';
|
|
2
|
+
const RULES = [
|
|
3
|
+
{ id: 'private-key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
|
|
4
|
+
{ id: 'github-token', pattern: /\b(?:gh[opusr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/ },
|
|
5
|
+
{ id: 'bearer-token', pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i },
|
|
6
|
+
{ id: 'aws-access-key', pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
7
|
+
{ id: 'slack-token', pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
8
|
+
{ id: 'slack-webhook', pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]{20,}/i },
|
|
9
|
+
{ id: 'credential-field', pattern: /(?:api[_-]?key|password|secret)\s*[:=]\s*["']?[^\s"']{8,}/i }
|
|
10
|
+
];
|
|
11
|
+
function inspectPrivacy(content) {
|
|
12
|
+
for (const rule of RULES) {
|
|
13
|
+
if (rule.pattern.test(content))
|
|
14
|
+
return { state: 'excluded-sensitive', ruleId: rule.id };
|
|
15
|
+
}
|
|
16
|
+
return { state: 'included' };
|
|
17
|
+
}
|
|
18
|
+
function captureText(sourceKind, sourceIdentity, content) {
|
|
19
|
+
const bytes = Buffer.byteLength(content);
|
|
20
|
+
const digest = sha256(Buffer.from(content, 'utf8'));
|
|
21
|
+
const disposition = inspectPrivacy(content);
|
|
22
|
+
return disposition.state === 'included'
|
|
23
|
+
? { sourceKind, sourceIdentity, sha256: digest, bytes, content, disposition }
|
|
24
|
+
: { sourceKind, sourceIdentity, sha256: digest, bytes, disposition };
|
|
25
|
+
}
|
|
26
|
+
function redactExcerpt(content, maxBytes = 4096) {
|
|
27
|
+
if (inspectPrivacy(content).state !== 'included')
|
|
28
|
+
return null;
|
|
29
|
+
const buffer = Buffer.from(content, 'utf8');
|
|
30
|
+
return buffer.subarray(0, maxBytes).toString('utf8').replace(/\b(token|password|secret)=\S+/gi, '$1=[REDACTED]');
|
|
31
|
+
}
|
|
32
|
+
export { captureText, inspectPrivacy, redactExcerpt };
|
|
33
|
+
//# sourceMappingURL=privacy.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { sha256 } from './store.js';
|
|
2
|
+
function reconcileRecords(records, scope) {
|
|
3
|
+
const findings = [];
|
|
4
|
+
const repairs = [];
|
|
5
|
+
const findingKeys = new Set();
|
|
6
|
+
const addFinding = (category, severity, identities, repairable, reason) => {
|
|
7
|
+
const sortedIdentities = [...identities].sort();
|
|
8
|
+
const key = `${category}:${sortedIdentities.join('\0')}`;
|
|
9
|
+
if (findingKeys.has(key))
|
|
10
|
+
return;
|
|
11
|
+
findingKeys.add(key);
|
|
12
|
+
findings.push({
|
|
13
|
+
findingId: `quality:${sha256(key).slice(0, 20)}`,
|
|
14
|
+
category,
|
|
15
|
+
severity,
|
|
16
|
+
identities: sortedIdentities,
|
|
17
|
+
repairable,
|
|
18
|
+
reason
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const byIdentity = new Map();
|
|
22
|
+
for (const record of records) {
|
|
23
|
+
const group = byIdentity.get(record.sourceIdentity) ?? [];
|
|
24
|
+
group.push(record);
|
|
25
|
+
byIdentity.set(record.sourceIdentity, group);
|
|
26
|
+
if (record.kind === 'missing' &&
|
|
27
|
+
record.data !== null &&
|
|
28
|
+
typeof record.data === 'object' &&
|
|
29
|
+
!Array.isArray(record.data) &&
|
|
30
|
+
record.data.reason === 'privacy-excluded') {
|
|
31
|
+
addFinding('privacy-excluded', 'warning', [record.sourceIdentity], false, 'Sensitive content was excluded by policy');
|
|
32
|
+
}
|
|
33
|
+
else if (record.kind === 'missing') {
|
|
34
|
+
addFinding('unrecoverable', 'info', [record.sourceIdentity], false, 'Source body is unavailable under the capture policy');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [identity, group] of byIdentity) {
|
|
38
|
+
if (group.length < 2)
|
|
39
|
+
continue;
|
|
40
|
+
addFinding('duplicate-identity', 'error', group.map((record) => record.recordId), false, 'Multiple records claim the same stable identity');
|
|
41
|
+
if (group.some((record) => record.kind === 'platform-resource') && new Set(group.map((record) => record.sourceSha256)).size > 1) {
|
|
42
|
+
addFinding('mutable-remote', 'warning', group.map((record) => record.recordId), false, `Remote identity changed content: ${identity}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const tasks = records.filter((record) => record.kind === 'task');
|
|
46
|
+
const resources = records.filter((record) => record.kind === 'platform-resource');
|
|
47
|
+
const taskByIdentity = new Map(tasks.map((task) => [task.sourceIdentity, task]));
|
|
48
|
+
for (const task of tasks) {
|
|
49
|
+
const data = task.data && typeof task.data === 'object' && !Array.isArray(task.data) ? task.data : null;
|
|
50
|
+
const version = data?.agentInfraVersion;
|
|
51
|
+
if (!version || typeof version !== 'object' || Array.isArray(version) || version.state !== 'known') {
|
|
52
|
+
addFinding('schema-difference', 'info', [task.recordId], false, 'Task uses a legacy schema without a known agent_infra_version');
|
|
53
|
+
}
|
|
54
|
+
if (scope !== 'all' || !task.binding)
|
|
55
|
+
continue;
|
|
56
|
+
const identityCandidates = resources.filter((record) => record.sourceIdentity === task.binding);
|
|
57
|
+
if (identityCandidates.length === 0) {
|
|
58
|
+
addFinding('missing-remote', 'error', [task.recordId, task.binding], false, 'Task binding has no captured platform resource');
|
|
59
|
+
}
|
|
60
|
+
if (identityCandidates.length > 1) {
|
|
61
|
+
addFinding('binding-conflict', 'error', [task.recordId, ...identityCandidates.map((record) => record.recordId)], false, 'Multiple platform resources bind to one task');
|
|
62
|
+
}
|
|
63
|
+
for (const remote of identityCandidates) {
|
|
64
|
+
if (remote.binding && remote.binding !== task.sourceIdentity) {
|
|
65
|
+
addFinding('content-mismatch', 'warning', [task.recordId, remote.recordId], false, 'Local and remote binding identities disagree');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const candidates = identityCandidates.filter((record) => record.binding === task.sourceIdentity);
|
|
69
|
+
if (candidates.length === 1 && (byIdentity.get(candidates[0].sourceIdentity)?.length ?? 0) === 1) {
|
|
70
|
+
const target = candidates[0];
|
|
71
|
+
repairs.push({
|
|
72
|
+
repairId: `repair:${sha256(`${task.recordId}:${target.recordId}`).slice(0, 20)}`,
|
|
73
|
+
operation: 'link',
|
|
74
|
+
sourceRecordId: task.recordId,
|
|
75
|
+
targetRecordId: target.recordId,
|
|
76
|
+
preconditionSha256: sha256(`${task.sourceSha256}:${target.sourceSha256}`)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const resource of scope === 'all' ? resources : []) {
|
|
81
|
+
if (resource.binding?.startsWith('TASK-') && !taskByIdentity.has(resource.binding)) {
|
|
82
|
+
addFinding('missing-local', 'error', [resource.recordId, resource.binding], false, 'Platform resource binds to a task that is not captured locally');
|
|
83
|
+
}
|
|
84
|
+
const localClaims = tasks.filter((task) => task.binding === resource.sourceIdentity);
|
|
85
|
+
if (localClaims.length > 1 || (resource.binding && localClaims.length > 0 && !localClaims.some((task) => task.sourceIdentity === resource.binding))) {
|
|
86
|
+
addFinding('binding-conflict', 'error', [resource.recordId, ...localClaims.map((task) => task.recordId)], false, 'Local and remote records claim incompatible bindings');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
findings: findings.sort((left, right) => left.findingId.localeCompare(right.findingId)),
|
|
91
|
+
repairs: repairs.sort((left, right) => left.repairId.localeCompare(right.repairId))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export { reconcileRecords };
|
|
95
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canonicalJsonBytes, sha256, verifySnapshot } from './store.js';
|
|
4
|
+
function readRepairActions(snapshotPath) {
|
|
5
|
+
const filePath = path.join(snapshotPath, 'repair-manifest.jsonl');
|
|
6
|
+
if (!fs.existsSync(filePath))
|
|
7
|
+
return [];
|
|
8
|
+
return fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
9
|
+
const { schema: _schema, ...action } = JSON.parse(line);
|
|
10
|
+
return action;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function repairSnapshot(root, snapshotId, apply) {
|
|
14
|
+
const verified = verifySnapshot(root, snapshotId);
|
|
15
|
+
if (!verified.ok || !verified.snapshotPath)
|
|
16
|
+
throw new Error(`snapshot verification failed: ${verified.errors.join('; ')}`);
|
|
17
|
+
const actions = readRepairActions(verified.snapshotPath);
|
|
18
|
+
const records = new Map(fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
19
|
+
const record = JSON.parse(line);
|
|
20
|
+
return [record.recordId, record];
|
|
21
|
+
}));
|
|
22
|
+
for (const action of actions) {
|
|
23
|
+
const source = records.get(action.sourceRecordId);
|
|
24
|
+
const target = records.get(action.targetRecordId);
|
|
25
|
+
if (!source || !target)
|
|
26
|
+
throw new Error(`repair references a missing record: ${action.repairId}`);
|
|
27
|
+
if (sha256(`${source.sourceSha256}:${target.sourceSha256}`) !== action.preconditionSha256) {
|
|
28
|
+
throw new Error(`repair precondition failed: ${action.repairId}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!apply || actions.length === 0)
|
|
32
|
+
return { applied: false, repairId: null, actionCount: actions.length };
|
|
33
|
+
const digest = sha256(canonicalJsonBytes(actions));
|
|
34
|
+
const repairId = `repair-${digest.slice(0, 16)}`;
|
|
35
|
+
const repairDir = path.join(root, 'repairs', snapshotId, repairId);
|
|
36
|
+
const manifest = `${canonicalJsonBytes({ schema: 'repair-application/v1', snapshotId, repairId, actionCount: actions.length, overlaySha256: digest }).toString('utf8')}\n`;
|
|
37
|
+
const overlay = actions.map((action) => `${canonicalJsonBytes({ schema: 'normalized-overlay/v1', ...action }).toString('utf8')}\n`).join('');
|
|
38
|
+
if (fs.existsSync(repairDir)) {
|
|
39
|
+
if (fs.readFileSync(path.join(repairDir, 'manifest.json'), 'utf8') !== manifest)
|
|
40
|
+
throw new Error(`repair collision: ${repairId}`);
|
|
41
|
+
return { applied: false, repairId, actionCount: actions.length, path: repairDir };
|
|
42
|
+
}
|
|
43
|
+
fs.mkdirSync(repairDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
fs.writeFileSync(path.join(repairDir, 'manifest.json'), manifest, { mode: 0o600, flag: 'wx' });
|
|
45
|
+
fs.writeFileSync(path.join(repairDir, 'overlay.jsonl'), overlay, { mode: 0o600, flag: 'wx' });
|
|
46
|
+
return { applied: true, repairId, actionCount: actions.length, path: repairDir };
|
|
47
|
+
}
|
|
48
|
+
function readAppliedOverlays(root, snapshotId) {
|
|
49
|
+
const base = path.join(root, 'repairs', snapshotId);
|
|
50
|
+
if (!fs.existsSync(base))
|
|
51
|
+
return '';
|
|
52
|
+
return fs.readdirSync(base).sort().map((entry) => {
|
|
53
|
+
const overlay = path.join(base, entry, 'overlay.jsonl');
|
|
54
|
+
return fs.existsSync(overlay) ? fs.readFileSync(overlay, 'utf8') : '';
|
|
55
|
+
}).join('');
|
|
56
|
+
}
|
|
57
|
+
export { readAppliedOverlays, repairSnapshot };
|
|
58
|
+
//# sourceMappingURL=repair.js.map
|