@quolu/lattice 0.57.3 → 0.58.1

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.
@@ -0,0 +1,162 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { gitSpawnSync } from './git-process.mjs';
6
+ import { runSensorCli } from './sensor-cli.mjs';
7
+ import { collectTodoStructureGitProvenance } from './todo-structure-git-adapter.mjs';
8
+ import { collectTodoStructureSourceEvidence } from './todo-structure-source-adapter.mjs';
9
+
10
+ const SHA = /^[0-9a-f]{40}$/u;
11
+
12
+ export class TodoStructureObservationError extends Error {
13
+ constructor(code, reason, detail = {}) {
14
+ super(reason);
15
+ this.name = 'TodoStructureObservationError';
16
+ this.code = code;
17
+ this.detail = { reason, ...detail };
18
+ }
19
+ }
20
+
21
+ function fail(code, reason, detail = {}) {
22
+ throw new TodoStructureObservationError(code, reason, detail);
23
+ }
24
+
25
+ function git({ cwd, args, operation }) {
26
+ const result = gitSpawnSync(args, {
27
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
28
+ });
29
+ if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
30
+ fail('STRUCTURE_OBSERVATION_GIT_FAILED', 'observation_git_command_failed', {
31
+ operation, status: result.status ?? null, signal: result.signal ?? null,
32
+ cause: result.error?.message ?? null,
33
+ });
34
+ }
35
+ return result.stdout.trim();
36
+ }
37
+
38
+ function memoryStream() {
39
+ let value = '';
40
+ return {
41
+ stream: { write: (chunk) => { value += String(chunk); } },
42
+ read: () => value,
43
+ };
44
+ }
45
+
46
+ async function initializeObservationSensor(observationRoot) {
47
+ const stdout = memoryStream();
48
+ const stderr = memoryStream();
49
+ const status = await runSensorCli({
50
+ argv: ['init', observationRoot, '--json'],
51
+ stdout: stdout.stream,
52
+ stderr: stderr.stream,
53
+ });
54
+ if (status !== 0) {
55
+ let diagnostic = null;
56
+ try {
57
+ const parsed = JSON.parse(stderr.read().trim());
58
+ diagnostic = {
59
+ code: typeof parsed?.code === 'string' ? parsed.code : null,
60
+ detail: parsed?.detail ?? null,
61
+ };
62
+ } catch {
63
+ diagnostic = { code: null, detail: null };
64
+ }
65
+ fail('STRUCTURE_OBSERVATION_SENSOR_INIT_FAILED', 'observation_sensor_init_failed', {
66
+ sensor_error: diagnostic,
67
+ });
68
+ }
69
+ }
70
+
71
+ function errorSummary(error) {
72
+ return {
73
+ code: typeof error?.code === 'string' ? error.code : null,
74
+ message: error instanceof Error ? error.message : String(error),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * 管理repoのdirtyなindex/worktreeを一切観測せず、current HEADだけを一時detached worktreeへ
80
+ * 展開してGit来歴とsource graphを収集する。store/planned source/artifactの読書きは呼出側が
81
+ * 管理repoで行うため、ここへは持ち込まない。
82
+ */
83
+ export async function collectTodoStructureAuthoritativeObservation({
84
+ repoRoot, structureSet, effectiveTransforms = null,
85
+ initializeSensor = initializeObservationSensor,
86
+ } = {}) {
87
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0
88
+ || structureSet === null || typeof structureSet !== 'object'
89
+ || typeof initializeSensor !== 'function') {
90
+ fail('STRUCTURE_OBSERVATION_INPUT_INVALID', 'observation_input_invalid');
91
+ }
92
+
93
+ const headSha = git({
94
+ cwd: repoRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'], operation: 'resolve_head',
95
+ });
96
+ if (!SHA.test(headSha)) fail('STRUCTURE_OBSERVATION_HEAD_INVALID', 'observation_head_invalid');
97
+
98
+ const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'lattice-structure-observation-'));
99
+ const observationRoot = path.join(temporaryRoot, 'worktree');
100
+ let registered = false;
101
+ let operationError = null;
102
+ let result;
103
+ try {
104
+ git({
105
+ cwd: repoRoot,
106
+ args: ['worktree', 'add', '--detach', '--quiet', observationRoot, headSha],
107
+ operation: 'worktree_add',
108
+ });
109
+ registered = true;
110
+ const observedHead = git({
111
+ cwd: observationRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'],
112
+ operation: 'verify_observation_head',
113
+ });
114
+ if (observedHead !== headSha) {
115
+ fail('STRUCTURE_OBSERVATION_HEAD_MISMATCH', 'observation_head_mismatch', {
116
+ expected_head_sha: headSha, actual_head_sha: observedHead,
117
+ });
118
+ }
119
+
120
+ // sensor DBを作る前にclean treeをGit provenanceへ束縛する。sensorの一時fileは証拠対象外で、
121
+ // source projection収集後にworktreeごと破棄する。
122
+ const gitProvenance = collectTodoStructureGitProvenance({
123
+ repoRoot: observationRoot, structureSet,
124
+ });
125
+ await initializeSensor(observationRoot);
126
+ const sourceEvidence = await collectTodoStructureSourceEvidence({
127
+ cwd: observationRoot, structureSet, effectiveTransforms,
128
+ observationStage: effectiveTransforms === null ? 'planned' : 'final',
129
+ });
130
+ result = {
131
+ head_sha: headSha,
132
+ git_provenance: gitProvenance,
133
+ source_evidence: sourceEvidence,
134
+ };
135
+ } catch (error) {
136
+ operationError = error;
137
+ }
138
+
139
+ let cleanupError = null;
140
+ try {
141
+ if (registered) {
142
+ git({
143
+ cwd: repoRoot,
144
+ args: ['worktree', 'remove', '--force', observationRoot],
145
+ operation: 'worktree_remove',
146
+ });
147
+ }
148
+ await rm(temporaryRoot, { recursive: true, force: true });
149
+ } catch (error) {
150
+ cleanupError = error;
151
+ }
152
+
153
+ if (cleanupError !== null) {
154
+ fail('STRUCTURE_OBSERVATION_CLEANUP_FAILED', 'observation_cleanup_failed', {
155
+ operation_error: operationError === null ? null : errorSummary(operationError),
156
+ cleanup_error: errorSummary(cleanupError),
157
+ observation_root: observationRoot,
158
+ });
159
+ }
160
+ if (operationError !== null) throw operationError;
161
+ return result;
162
+ }