@fitlab-ai/agent-infra 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -1,24 +1,26 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
4
3
 
5
4
  import { detectRepoRoot } from '../task/resolve-ref.ts';
6
- import { normalizeObjects } from './normalize.ts';
7
- import { reconcileRecords } from './reconcile.ts';
5
+ import { acquireGitHubCheckpoint, commitGitHubCheckpoint, readGitHubCheckpoint, releaseGitHubCheckpoint } from './checkpoint.ts';
6
+ import { materializeJson, materializeSnapshot } from './materialize.ts';
7
+ import { normalizeResources } from './normalize.ts';
8
+ import { reconcileRecords, reconcileResourceRecords } from './reconcile.ts';
8
9
  import { readAppliedOverlays, repairSnapshot } from './repair.ts';
9
- import { collectGitHubObjects, collectLocalObjects } from './sources.ts';
10
- import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.ts';
10
+ import { collectGitHubBoundary, resolveGitHubRepository } from './sources.ts';
11
+ import { canonicalJsonBytes, createObjectStore, findSnapshot, publishSnapshotV2, verifySnapshot } from './store.ts';
12
+ import { createGitHubClient } from '../platform/github-client.ts';
11
13
 
12
- import type { CapturedObject, NormalizedRecord, RestCollectionEvidence } from './types.ts';
14
+ import type { NormalizedRecord } from './types.ts';
13
15
 
14
16
  const USAGE = `Usage: ai data <command> [options]
15
17
 
16
18
  Commands:
17
- capture [--source all|local|github] [--root <dir>] [--include-excerpts]
19
+ capture [--source github] [--root <dir>] [--full-reconcile]
18
20
  verify <snapshot-id> [--root <dir>]
19
21
  audit <snapshot-id> [--root <dir>] [--format json|text]
20
22
  repair <snapshot-id> [--root <dir>] [--apply]
21
- export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
23
+ export <snapshot-id> [--root <dir>] [--repairs none|applied] [--as-of <ISO>] [--output <file|->]
22
24
  `;
23
25
 
24
26
  type ParsedArgs = { positionals: string[]; options: Map<string, string | true> };
@@ -64,14 +66,6 @@ function dataRoot(repoRoot: string, requested?: string): string {
64
66
  return resolved;
65
67
  }
66
68
 
67
- function repositoryIdentity(repoRoot: string): string {
68
- try {
69
- return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
70
- } catch {
71
- return path.basename(repoRoot);
72
- }
73
- }
74
-
75
69
  function writeJson(value: unknown): void {
76
70
  process.stdout.write(`${JSON.stringify(value)}\n`);
77
71
  }
@@ -82,54 +76,106 @@ function optionString(parsed: ParsedArgs, name: string): string | undefined {
82
76
  }
83
77
 
84
78
  function capture(args: string[]): number {
85
- const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
79
+ const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--full-reconcile', '--include-excerpts']));
86
80
  if (parsed.positionals.length > 0) throw new Error('capture does not accept positional arguments');
87
- const scope = optionString(parsed, '--source') ?? 'all';
88
- if (!['all', 'local', 'github'].includes(scope)) throw new Error('--source must be all, local, or github');
81
+ const scope = optionString(parsed, '--source') ?? 'github';
82
+ if (scope !== 'github') throw new Error('new process-data capture only supports --source github; local data is already normalized and --source all is retired');
83
+ if (parsed.options.has('--include-excerpts')) throw new Error('--include-excerpts is not supported for GitHub-only capture');
89
84
  const repoRoot = detectRepoRoot();
90
85
  const root = dataRoot(repoRoot, optionString(parsed, '--root'));
91
- const observedFrom = new Date().toISOString();
92
- const objects: CapturedObject[] = [];
93
- const endpoints: RestCollectionEvidence[] = [];
94
- let repository = repositoryIdentity(repoRoot);
95
-
96
- if (scope === 'all' || scope === 'local') {
97
- const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
98
- if (!local.ok) { writeJson({ ok: false, error: local.error }); return 2; }
99
- objects.push(...local.value);
100
- }
101
- if (scope === 'all' || scope === 'github') {
102
- const github = collectGitHubObjects(repoRoot);
86
+ const client = createGitHubClient();
87
+ const repositoryResult = resolveGitHubRepository(repoRoot, client);
88
+ if (!repositoryResult.ok) { writeJson({ ok: false, error: repositoryResult.error }); return 2; }
89
+ const repository = repositoryResult.value;
90
+ const leaseResult = acquireGitHubCheckpoint(root, repository);
91
+ if (!leaseResult.ok) { writeJson({ ok: false, error: leaseResult.error }); return 2; }
92
+ const lease = leaseResult.value;
93
+ try {
94
+ const checkpointResult = readGitHubCheckpoint(root, repository);
95
+ if (!checkpointResult.ok) { writeJson({ ok: false, error: checkpointResult.error }); return 2; }
96
+ const checkpoint = checkpointResult.value;
97
+ const full = parsed.options.has('--full-reconcile');
98
+ const observedFrom = new Date().toISOString();
99
+ const github = collectGitHubBoundary(repoRoot, {
100
+ client,
101
+ fromInclusive: full ? null : checkpoint?.watermark ?? null,
102
+ reconciliation: full ? 'full' : 'incremental'
103
+ });
103
104
  if (!github.ok) { writeJson({ ok: false, error: github.error }); return 2; }
104
- objects.push(...github.value.objects);
105
- endpoints.push(...github.value.endpoints);
106
- repository = github.value.repository;
107
- }
108
-
109
- const store = createObjectStore(root);
110
- for (const object of objects) {
111
- if (object.content !== undefined && object.disposition?.state === 'included') {
112
- const stored = store.put(Buffer.from(object.content, 'utf8'));
113
- if (stored.sha256 !== object.sha256) throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
105
+ const store = createObjectStore(root);
106
+ for (const object of github.value.objects) {
107
+ if (object.content !== undefined && object.disposition?.state === 'included') {
108
+ const stored = store.put(Buffer.from(object.content, 'utf8'));
109
+ if (stored.sha256 !== object.sha256) throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
110
+ }
111
+ }
112
+ let parent: NormalizedRecord[] = [];
113
+ if (checkpoint) {
114
+ const parentPath = findSnapshot(root, checkpoint.snapshotId);
115
+ if (!parentPath) throw new Error(`checkpoint head snapshot is missing: ${checkpoint.snapshotId}`);
116
+ const materialized = materializeSnapshot(root, checkpoint.snapshotId);
117
+ if (!materialized.manifest || materialized.manifest.manifestSha256 !== checkpoint.manifestSha256 || materialized.manifest.watermark !== checkpoint.watermark) {
118
+ throw new Error('checkpoint does not match its verified snapshot head');
119
+ }
120
+ parent = materialized.records;
121
+ }
122
+ const current = normalizeResources(github.value.objects);
123
+ const reconciliation = reconcileResourceRecords(current, {
124
+ parent,
125
+ full,
126
+ deferred: github.value.deferred,
127
+ unavailable: github.value.unavailable
128
+ });
129
+ const quality = reconcileRecords(reconciliation.records, 'github');
130
+ const fromInclusive = checkpoint?.watermark ?? null;
131
+ const hasStrictSinceEndpoint = github.value.endpoints.some((endpoint) => endpoint.queryMode === 'strict-since');
132
+ const queryAfter = !full && fromInclusive && hasStrictSinceEndpoint
133
+ ? new Date(new Date(fromInclusive).getTime() - 1000).toISOString()
134
+ : null;
135
+ const responseDates = github.value.responseDates;
136
+ const published = publishSnapshotV2(root, {
137
+ scope: 'github',
138
+ repository,
139
+ observedFrom,
140
+ observedTo: github.value.watermark,
141
+ objects: github.value.objects.map(({ content: _content, ...object }) => object),
142
+ endpoints: github.value.endpoints,
143
+ excerptsEnabled: false,
144
+ records: reconciliation.records,
145
+ quality: quality.findings,
146
+ repairs: quality.repairs,
147
+ snapshotKind: checkpoint ? 'delta' : 'base',
148
+ parentSnapshotId: checkpoint?.snapshotId ?? null,
149
+ checkpointBefore: checkpoint?.snapshotId ?? null,
150
+ watermark: github.value.watermark,
151
+ window: { fromInclusive, queryAfter, toExclusive: github.value.watermark, precision: 'second' },
152
+ observation: {
153
+ cutoffSource: 'github-response-date',
154
+ preflightDate: github.value.preflightDate,
155
+ responseDates
156
+ },
157
+ coverage: { mode: 'boundary-reread-with-full-reconcile', absoluteCompleteness: false },
158
+ reconciliation: full ? 'full' : 'incremental',
159
+ operations: reconciliation.operations
160
+ });
161
+ const verified = verifySnapshot(root, published.snapshotId);
162
+ if (!verified.ok || !verified.manifest || verified.manifest.schema !== 'raw-manifest/v2') {
163
+ throw new Error(`published snapshot failed verification: ${verified.errors.join('; ')}`);
114
164
  }
165
+ const committed = commitGitHubCheckpoint(lease, {
166
+ schema: 'github-checkpoint/v1',
167
+ repository,
168
+ snapshotId: published.snapshotId,
169
+ watermark: github.value.watermark,
170
+ manifestSha256: verified.manifest.manifestSha256,
171
+ committedAt: new Date().toISOString()
172
+ });
173
+ if (!committed.ok) { writeJson({ ok: false, error: committed.error }); return 2; }
174
+ writeJson({ ok: true, snapshotId: published.snapshotId, scope, snapshotKind: checkpoint ? 'delta' : 'base', reconciliation: full ? 'full' : 'incremental', created: published.created, objects: github.value.objects.length, records: reconciliation.records.length, watermark: github.value.watermark, deferred: github.value.deferred.length });
175
+ return 0;
176
+ } finally {
177
+ releaseGitHubCheckpoint(lease);
115
178
  }
116
- const records = normalizeObjects(objects);
117
- const reconciliation = reconcileRecords(records, scope as 'all' | 'local' | 'github');
118
- const observedTo = new Date().toISOString();
119
- const published = publishSnapshot(root, {
120
- scope: scope as 'all' | 'local' | 'github',
121
- repository,
122
- observedFrom,
123
- observedTo,
124
- objects: objects.map(({ content: _content, ...object }) => object),
125
- endpoints,
126
- excerptsEnabled: parsed.options.has('--include-excerpts'),
127
- records,
128
- quality: reconciliation.findings,
129
- repairs: reconciliation.repairs
130
- });
131
- writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
132
- return 0;
133
179
  }
134
180
 
135
181
  function verify(args: string[]): number {
@@ -175,7 +221,7 @@ function repair(args: string[]): number {
175
221
  }
176
222
 
177
223
  function exportSnapshot(args: string[]): number {
178
- const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
224
+ const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output', '--as-of']), new Set());
179
225
  if (parsed.positionals.length !== 1) throw new Error('export requires one snapshot id');
180
226
  const repairMode = optionString(parsed, '--repairs') ?? 'none';
181
227
  if (!['none', 'applied'].includes(repairMode)) throw new Error('--repairs must be none or applied');
@@ -184,7 +230,13 @@ function exportSnapshot(args: string[]): number {
184
230
  const root = dataRoot(repoRoot, optionString(parsed, '--root'));
185
231
  const verified = verifySnapshot(root, parsed.positionals[0]!);
186
232
  if (!verified.ok || !verified.snapshotPath) { writeJson(verified); return 1; }
187
- let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
233
+ let content: string;
234
+ if (verified.manifest?.schema === 'raw-manifest/v2') {
235
+ content = materializeJson(root, parsed.positionals[0]!, { asOf: optionString(parsed, '--as-of') });
236
+ } else {
237
+ if (optionString(parsed, '--as-of')) throw new Error('--as-of is only supported for v2 GitHub snapshots');
238
+ content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
239
+ }
188
240
  if (repairMode === 'applied') content += readAppliedOverlays(root, parsed.positionals[0]!);
189
241
  if (output === '-') process.stdout.write(content);
190
242
  else {
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { findSnapshot, verifySnapshot } from './store.ts';
5
+ import type { NormalizedRecord, SnapshotManifestV2 } from './types.ts';
6
+
7
+ type MaterializeOptions = { asOf?: string };
8
+ type MaterializeResult = { snapshotId: string; records: NormalizedRecord[]; manifest: SnapshotManifestV2 | null };
9
+
10
+ function readRecords(snapshotPath: string, schema: 'normalized/v1' | 'normalized/v2'): NormalizedRecord[] {
11
+ const file = path.join(snapshotPath, 'normalized.jsonl');
12
+ if (!fs.existsSync(file)) return [];
13
+ return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
14
+ const value = JSON.parse(line) as { schema?: string } & NormalizedRecord;
15
+ if (value.schema !== schema) return [];
16
+ const { schema: _schema, ...record } = value;
17
+ return [record];
18
+ });
19
+ }
20
+
21
+ function materializeSnapshot(root: string, snapshotId: string, options: MaterializeOptions = {}): MaterializeResult {
22
+ const target = options.asOf ? new Date(options.asOf) : null;
23
+ if (target && !Number.isFinite(target.getTime())) throw new Error('as-of must be an ISO timestamp');
24
+ const chain: Array<{ id: string; path: string; manifest: SnapshotManifestV2 }> = [];
25
+ let currentId: string | null = snapshotId;
26
+ let repository: string | null = null;
27
+ while (currentId) {
28
+ const currentPath = findSnapshot(root, currentId);
29
+ if (!currentPath) throw new Error(`snapshot not found: ${currentId}`);
30
+ const verified = verifySnapshot(root, currentId);
31
+ if (!verified.ok || !verified.manifest) throw new Error(`snapshot is not verified: ${currentId}`);
32
+ if (verified.manifest.schema !== 'raw-manifest/v2') {
33
+ if (chain.length > 0) throw new Error('v1 and v2 snapshots cannot share a lineage');
34
+ return { snapshotId, records: readRecords(currentPath, 'normalized/v1'), manifest: null };
35
+ }
36
+ const manifest = verified.manifest;
37
+ if (repository && manifest.repository !== repository) throw new Error('snapshot lineage repository mismatch');
38
+ repository = manifest.repository;
39
+ chain.push({ id: currentId, path: currentPath, manifest });
40
+ currentId = manifest.parentSnapshotId;
41
+ }
42
+ if (chain.length === 0) throw new Error('snapshot lineage is empty');
43
+ const chronological = [...chain].reverse();
44
+ for (let index = 1; index < chronological.length; index += 1) {
45
+ if (new Date(chronological[index - 1]!.manifest.watermark).getTime() >= new Date(chronological[index]!.manifest.watermark).getTime()) {
46
+ throw new Error('snapshot watermark is not monotonic');
47
+ }
48
+ }
49
+ const view = new Map<string, NormalizedRecord>();
50
+ let appliedManifest: SnapshotManifestV2 | null = null;
51
+ for (const entry of chronological) {
52
+ if (target && new Date(entry.manifest.watermark).getTime() > target.getTime()) continue;
53
+ appliedManifest = entry.manifest;
54
+ for (const record of readRecords(entry.path, 'normalized/v2')) {
55
+ const identity = record.resourceIdentity ?? record.sourceIdentity;
56
+ switch (record.operation) {
57
+ case 'tombstone':
58
+ view.delete(identity);
59
+ break;
60
+ case 'unavailable':
61
+ view.set(identity, record);
62
+ break;
63
+ default:
64
+ view.set(identity, { ...record, operation: undefined });
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ if (target && !appliedManifest) throw new Error('no verified snapshot is available at the requested as-of time');
70
+ return {
71
+ snapshotId,
72
+ records: [...view.values()].sort((left, right) => left.recordId.localeCompare(right.recordId)),
73
+ manifest: appliedManifest
74
+ };
75
+ }
76
+
77
+ function materializeJson(root: string, snapshotId: string, options: MaterializeOptions = {}): string {
78
+ const result = materializeSnapshot(root, snapshotId, options);
79
+ return result.records.map((record) => JSON.stringify({ schema: 'normalized/v2', ...record })).join('\n') + (result.records.length ? '\n' : '');
80
+ }
81
+
82
+ export { materializeJson, materializeSnapshot };
83
+ export type { MaterializeOptions, MaterializeResult };
@@ -199,4 +199,32 @@ function normalizeObjects(objects: CapturedObject[]): NormalizedRecord[] {
199
199
  return records.sort((left, right) => left.recordId.localeCompare(right.recordId));
200
200
  }
201
201
 
202
- export { normalizeObjects };
202
+ function normalizeResources(objects: CapturedObject[]): NormalizedRecord[] {
203
+ const records: NormalizedRecord[] = [];
204
+ for (const object of objects) {
205
+ if (object.role !== 'resource') continue;
206
+ const identity = object.resourceIdentity ?? object.sourceIdentity;
207
+ let data: JsonValue = { availability: 'unknown' };
208
+ try { data = JSON.parse(object.content ?? '{}') as JsonValue; } catch { data = { availability: 'unavailable', reason: 'invalid resource JSON' }; }
209
+ const binding = typeof data === 'object' && data !== null && !Array.isArray(data) && typeof data.body === 'string'
210
+ ? data.body.match(TASK_ID_RE)?.[0]
211
+ : undefined;
212
+ records.push({
213
+ recordId: recordId('platform-resource', identity),
214
+ kind: 'platform-resource',
215
+ sourceIdentity: identity,
216
+ resourceIdentity: identity,
217
+ sourceSha256: object.sha256,
218
+ ...(binding ? { binding } : {}),
219
+ ...(object.eventTime ? { observedAt: { state: 'known', value: object.eventTime } } : {}),
220
+ ...(object.parentIdentity ? { parentIdentity: object.parentIdentity } : {}),
221
+ ...(object.pageSha256 ? {
222
+ evidence: [{ resourceIdentity: identity, resourceSha256: object.sha256, pageSha256: object.pageSha256 }]
223
+ } : {}),
224
+ data
225
+ });
226
+ }
227
+ return records.sort((left, right) => left.recordId.localeCompare(right.recordId));
228
+ }
229
+
230
+ export { normalizeObjects, normalizeResources };
@@ -1,6 +1,6 @@
1
1
  import { sha256 } from './store.ts';
2
2
 
3
- import type { NormalizedRecord, QualityFinding, RepairAction, SnapshotManifest } from './types.ts';
3
+ import type { NormalizedRecord, QualityFinding, RepairAction, SnapshotManifest, SnapshotOperations } from './types.ts';
4
4
 
5
5
  function reconcileRecords(
6
6
  records: NormalizedRecord[],
@@ -107,4 +107,66 @@ function reconcileRecords(
107
107
  };
108
108
  }
109
109
 
110
- export { reconcileRecords };
110
+ type ResourceReconcileOptions = {
111
+ parent?: NormalizedRecord[];
112
+ full: boolean;
113
+ deferred?: Iterable<string>;
114
+ unavailable?: Iterable<string>;
115
+ };
116
+
117
+ function reconcileResourceRecords(
118
+ current: NormalizedRecord[],
119
+ options: ResourceReconcileOptions
120
+ ): { records: NormalizedRecord[]; operations: SnapshotOperations } {
121
+ const parent = new Map((options.parent ?? []).filter((record) => record.resourceIdentity).map((record) => [record.resourceIdentity!, record]));
122
+ const currentMap = new Map(current.filter((record) => record.resourceIdentity).map((record) => [record.resourceIdentity!, record]));
123
+ const deferred = new Set(options.deferred ?? []);
124
+ const unavailable = new Set(options.unavailable ?? []);
125
+ const records: NormalizedRecord[] = [];
126
+ const operations: SnapshotOperations = { upsert: 0, supersede: 0, tombstone: 0, unavailable: 0 };
127
+ for (const record of currentMap.values()) {
128
+ const identity = record.resourceIdentity!;
129
+ const previous = parent.get(identity);
130
+ if (!previous) {
131
+ records.push({ ...record, operation: 'upsert' });
132
+ operations.upsert += 1;
133
+ } else if (previous.sourceSha256 !== record.sourceSha256) {
134
+ records.push({ ...record, operation: 'supersede' });
135
+ operations.supersede += 1;
136
+ }
137
+ }
138
+ if (options.full) {
139
+ for (const [identity, previous] of parent) {
140
+ if (currentMap.has(identity) || deferred.has(identity) || unavailable.has(identity)
141
+ || (previous.parentIdentity && deferred.has(previous.parentIdentity))
142
+ || (previous.parentIdentity && unavailable.has(previous.parentIdentity))) continue;
143
+ records.push({
144
+ recordId: previous.recordId,
145
+ kind: 'platform-resource',
146
+ sourceIdentity: identity,
147
+ resourceIdentity: identity,
148
+ sourceSha256: previous.sourceSha256,
149
+ operation: 'tombstone',
150
+ data: { availability: 'tombstone', previousSha256: previous.sourceSha256 }
151
+ });
152
+ operations.tombstone += 1;
153
+ }
154
+ }
155
+ for (const identity of unavailable) {
156
+ if (!parent.has(identity) && !currentMap.has(identity)) continue;
157
+ const previous = parent.get(identity) ?? currentMap.get(identity)!;
158
+ records.push({
159
+ recordId: previous.recordId,
160
+ kind: 'platform-resource',
161
+ sourceIdentity: identity,
162
+ resourceIdentity: identity,
163
+ sourceSha256: previous.sourceSha256,
164
+ operation: 'unavailable',
165
+ data: { availability: 'unavailable', reason: 'source-unavailable' }
166
+ });
167
+ operations.unavailable += 1;
168
+ }
169
+ return { records: records.sort((left, right) => left.recordId.localeCompare(right.recordId)), operations };
170
+ }
171
+
172
+ export { reconcileRecords, reconcileResourceRecords };