@guanlin4924/devflow-pi 0.1.73 → 0.1.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -9,6 +9,7 @@ import sqlite3
9
9
  import subprocess
10
10
  import sys
11
11
  import tempfile
12
+ import time
12
13
  import uuid
13
14
  from datetime import datetime, timezone
14
15
  from pathlib import Path
@@ -155,6 +156,29 @@ def _hash_lines(values) -> str:
155
156
  return f"uuid5:{digest}"
156
157
 
157
158
 
159
+ class _Progress:
160
+ """Emit human-readable progress immediately and retain a machine-readable trail."""
161
+
162
+ def __init__(self, output: Path):
163
+ self.output = output
164
+ self.output.mkdir(parents=True, exist_ok=True)
165
+ self.path = output / "scan-progress.jsonl"
166
+ self.started = time.monotonic()
167
+
168
+ def emit(self, phase: str, message: str, **details) -> None:
169
+ event = {
170
+ "at": datetime.now(timezone.utc).isoformat(),
171
+ "elapsedSeconds": round(time.monotonic() - self.started, 3),
172
+ "phase": phase,
173
+ "message": message,
174
+ **details,
175
+ }
176
+ with self.path.open("a", encoding="utf-8") as handle:
177
+ handle.write(json.dumps(event, ensure_ascii=False) + "\n")
178
+ handle.flush()
179
+ print(f"[code-index][{phase}] {message}", file=sys.stderr, flush=True)
180
+
181
+
158
182
  def main(argv=None) -> int:
159
183
  parser = argparse.ArgumentParser(description="Build and validate a generic code intelligence index.")
160
184
  sub = parser.add_subparsers(dest="command", required=True)
@@ -215,7 +239,10 @@ def main(argv=None) -> int:
215
239
  def command_scan(args) -> int:
216
240
  root = Path(args.root).resolve()
217
241
  output = (root / args.output).resolve() if not Path(args.output).is_absolute() else Path(args.output).resolve()
242
+ progress = _Progress(output)
243
+ progress.emit("start", "scan started", root=str(root), output=str(output), codebases=args.codebase)
218
244
  with _index_lock(output, exclusive=True):
245
+ progress.emit("lock", "exclusive index lock acquired")
219
246
  codebases = discover_codebases(root, args.codebase)
220
247
  registry = AdapterRegistry()
221
248
  changed = _normalize_changed_files(root, args.changed_files)
@@ -233,12 +260,15 @@ def command_scan(args) -> int:
233
260
  if not codebase.root.exists():
234
261
  diagnostics.append(_diagnostic(codebase.name, "", 1, "warning", "skipped_file", f"codebase root does not exist: {codebase.root}", "discovery@1"))
235
262
  continue
263
+ progress.emit("discover", f"discovering {codebase.name}", codebase=codebase.name, path=str(codebase.root))
236
264
  all_files = scan_files(codebase.root, excludes=args.exclude)
265
+ progress.emit("discover", f"discovered {len(all_files)} files in {codebase.name}", codebase=codebase.name, files=len(all_files))
237
266
  discovered_paths.update(_workspace_rel(root, path) for path in all_files)
238
267
  if changed:
239
268
  all_files = [path for path in all_files if _workspace_rel(root, path) in changed]
240
269
  manifests = [path for path in all_files if path.name in {"package.json", "go.mod", "Cargo.toml", "pyproject.toml", "requirements.txt", "composer.json", "pubspec.yaml"}]
241
270
  adapters = registry.adapters_for(codebase, all_files, manifests)
271
+ parsed = 0
242
272
  for path in all_files:
243
273
  rel_path = _workspace_rel(root, path)
244
274
  if rel_path in indexed_paths:
@@ -266,11 +296,17 @@ def command_scan(args) -> int:
266
296
  diagnostics.extend(result.diagnostics)
267
297
  adapter_entities.extend(getattr(result, "entities", []) or [])
268
298
  adapter_flows.extend(getattr(result, "flows", []) or [])
299
+ parsed += 1
300
+ if parsed == 1 or parsed % 250 == 0 or parsed == len(all_files):
301
+ progress.emit("parse", f"parsed {parsed}/{len(all_files)} files in {codebase.name}", codebase=codebase.name, parsed=parsed, total=len(all_files))
269
302
  except Exception as exc: # keep indexing useful even when one file fails
270
303
  diagnostics.append(_diagnostic(codebase.name, rel_path, 1, "error", "parse_failed", str(exc), adapter.parser_id))
304
+ parsed += 1
271
305
 
306
+ progress.emit("relations", "resolving relations", files=len(files), symbols=len(symbols), relations=len(relations))
272
307
  relations = _resolve_relations(symbols, relations)
273
308
  files, symbols, relations, diagnostics = _dedupe(files, symbols, relations, diagnostics)
309
+ progress.emit("validate", "validating extracted facts", files=len(files), symbols=len(symbols), relations=len(relations), diagnostics=len(diagnostics))
274
310
  errors = validate_index(files, symbols, relations, diagnostics)
275
311
  if errors:
276
312
  for error in errors[:200]:
@@ -285,6 +321,7 @@ def command_scan(args) -> int:
285
321
  workspace_state["generatedAt"] = datetime.now(timezone.utc).isoformat()
286
322
  workspace_state["semanticExtractor"] = SEMANTIC_EXTRACTOR_VERSION
287
323
  workspace_state["extractorSetHash"] = _hash_lines([*workspace_state.get("extractors", []), SEMANTIC_EXTRACTOR_VERSION])
324
+ progress.emit("semantic", "building semantic facts")
288
325
  semantic_entities, semantic_flows, feature_bindings = build_semantic_facts(root, files, symbols, relations)
289
326
  semantic_entities.extend(adapter_entities)
290
327
  semantic_flows.extend(adapter_flows)
@@ -292,6 +329,7 @@ def command_scan(args) -> int:
292
329
  write_jsonl(staging / INDEX_FILES["symbols"], symbols)
293
330
  write_jsonl(staging / INDEX_FILES["relations"], relations)
294
331
  write_jsonl(staging / INDEX_FILES["diagnostics"], diagnostics)
332
+ progress.emit("sqlite", "writing sqlite index")
295
333
  write_sqlite(
296
334
  staging / "index.sqlite", files, symbols, relations, diagnostics,
297
335
  snapshot_id=generation,
@@ -300,6 +338,7 @@ def command_scan(args) -> int:
300
338
  semantic_flows=semantic_flows,
301
339
  feature_bindings=feature_bindings,
302
340
  )
341
+ progress.emit("documents", "writing index projections")
303
342
  write_markdown(staging, files, symbols, relations, diagnostics)
304
343
  staged_errors = validate_index(*_load_tuple(staging))
305
344
  if staged_errors:
@@ -307,6 +346,7 @@ def command_scan(args) -> int:
307
346
  print(error, file=sys.stderr)
308
347
  print("Staged index validation failed; the previous snapshot was preserved.", file=sys.stderr)
309
348
  return 1
349
+ progress.emit("publish", "publishing immutable snapshot", generation=generation)
310
350
  _publish_snapshot(
311
351
  staging,
312
352
  output,
@@ -322,6 +362,7 @@ def command_scan(args) -> int:
322
362
  finally:
323
363
  shutil.rmtree(staging, ignore_errors=True)
324
364
 
365
+ progress.emit("complete", "scan completed", files=len(files), symbols=len(symbols), relations=len(relations), diagnostics=len(diagnostics))
325
366
  print(f"Indexed {len(files)} files, {len(symbols)} symbols, {len(relations)} relations, {len(diagnostics)} diagnostics.")
326
367
  print(f"Output: {output}")
327
368
  return 0
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.73";
2
+ export const PRODUCT_VERSION = "0.1.74";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -383,12 +383,27 @@ export class InitializationStore {
383
383
  return { id: prior.id, leaseToken: prior.leaseToken, reused: true };
384
384
  }
385
385
  }
386
+ const active = this.db.query(`SELECT id FROM initialization_runs
387
+ WHERE workspace_id=? AND config_id=?
388
+ AND status IN ('preparing','collecting','validating')
389
+ AND lease_expires_at>? LIMIT 1`).get(input.workspaceId, input.configId, now()) as { id: string } | null;
390
+ if (active) throw new Error(`Initialization already running for workspace/config: ${active.id}`);
386
391
  const id = randomUUID(); const leaseToken = randomUUID(); const timestamp = now();
387
392
  const expires = new Date(Date.now() + (input.leaseMs ?? 15 * 60_000)).toISOString();
388
393
  this.db.query("INSERT INTO initialization_runs(id,project_id,workspace_id,config_id,input_snapshot_id,request_id,request_hash,status,lease_token,lease_expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)").run(id, this.context.manifest.projectId, input.workspaceId, input.configId, input.inputSnapshotId, input.requestId ?? null, requestHash, "collecting", leaseToken, expires, timestamp, timestamp);
389
394
  return { id, leaseToken, reused: false };
390
395
  }
391
396
 
397
+ renewRunLease(runId: string, leaseToken: string, leaseMs = 15 * 60_000): boolean {
398
+ const timestamp = now();
399
+ const result = this.db.query(`UPDATE initialization_runs
400
+ SET lease_expires_at=?,updated_at=?
401
+ WHERE id=? AND lease_token=? AND status IN ('preparing','collecting','validating')`).run(
402
+ new Date(Date.now() + leaseMs).toISOString(), timestamp, runId, leaseToken,
403
+ );
404
+ return result.changes === 1;
405
+ }
406
+
392
407
  private assertWritableRun(runId: string, leaseToken: string): { status: RunStatus; inputSnapshotId: string; workspaceId: string; configId: string } {
393
408
  const row = this.db.query("SELECT status,input_snapshot_id AS inputSnapshotId,workspace_id AS workspaceId,config_id AS configId,lease_token AS leaseToken,lease_expires_at AS expires FROM initialization_runs WHERE id=?").get(runId) as ({ status: RunStatus; inputSnapshotId: string; workspaceId: string; configId: string; leaseToken: string; expires: string }) | null;
394
409
  if (!row) throw new Error(`Unknown initialization run: ${runId}`);
@@ -193,9 +193,24 @@ function collect(files: SourceFile[], context: ProjectContext, config: ScanConfi
193
193
  return facts;
194
194
  }
195
195
 
196
- async function runProcess(argv: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
196
+ async function runProcess(argv: string[], cwd: string, label = path.basename(argv[1] ?? argv[0])): Promise<{ stdout: string; stderr: string }> {
197
197
  const child = Bun.spawn(argv, { cwd, stdout: "pipe", stderr: "pipe" });
198
- const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
198
+ const readStream = async (stream: ReadableStream<Uint8Array>, emit?: (line: string) => void): Promise<string> => {
199
+ const reader = stream.getReader(); const decoder = new TextDecoder(); let text = ""; const chunks: string[] = [];
200
+ while (true) {
201
+ const chunk = await reader.read(); if (chunk.done) break;
202
+ const decoded = decoder.decode(chunk.value, { stream: true }); chunks.push(decoded); text += decoded;
203
+ const lines = text.split(/\r?\n/); text = lines.pop() ?? "";
204
+ for (const line of lines) if (line) emit?.(line);
205
+ }
206
+ const tail = decoder.decode(); chunks.push(tail); text += tail; if (text) emit?.(text);
207
+ return chunks.join("");
208
+ };
209
+ const [exitCode, stdout, stderr] = await Promise.all([
210
+ child.exited,
211
+ readStream(child.stdout),
212
+ readStream(child.stderr, line => console.error(`[${label}] ${line}`)),
213
+ ]);
199
214
  if (exitCode !== 0) throw new Error(`${path.basename(argv[1] ?? argv[0])} failed (${exitCode}): ${(stderr || stdout).trim()}`);
200
215
  return { stdout: stdout.trim(), stderr: stderr.trim() };
201
216
  }
@@ -204,9 +219,9 @@ async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput):
204
219
  const script = path.resolve(import.meta.dir, "../plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py");
205
220
  const output = `.planning/.code-index-pending-${randomUUID()}`;
206
221
  const codebases = config.repositories.flatMap(repository => ["--codebase", `${repository.name}:${path.resolve(context.root, repository.relativePath)}:${repository.role}`]);
207
- await runProcess(["python3", script, "scan", "--root", context.root, "--output", output, ...codebases], context.root);
208
- await runProcess(["python3", script, "validate", "--index", output], context.root);
209
- await runProcess(["python3", script, "audit", "--root", context.root, "--index", output, "--skill-root", path.resolve(import.meta.dir, "../plugins/devflow-intel/skills")], context.root);
222
+ await runProcess(["python3", script, "scan", "--root", context.root, "--output", output, ...codebases], context.root, "code-index scan");
223
+ await runProcess(["python3", script, "validate", "--index", output], context.root, "code-index validate");
224
+ await runProcess(["python3", script, "audit", "--root", context.root, "--index", output, "--skill-root", path.resolve(import.meta.dir, "../plugins/devflow-intel/skills")], context.root, "code-index audit");
210
225
  const snapshotPath = path.join(context.root, output, "SNAPSHOT.json");
211
226
  const bytes = await readFile(snapshotPath); const snapshot = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
212
227
  if (!snapshot.generation || !snapshot.immutablePath) throw new Error("Code index did not publish an immutable generation");
@@ -239,6 +254,7 @@ export async function runProjectInitialization(context: ProjectContext, input: {
239
254
  const config: ScanConfigInput = input.config ?? { mode: "full", repositories: [{ name: context.manifest.name, relativePath: ".", role: "other" }], requiredKinds: [...FACT_KINDS], adapterVersions: { static: "1" } };
240
255
  const store = await InitializationStore.open(context); const diagnostics: string[] = [];
241
256
  let activeRun: { id: string; leaseToken: string } | undefined;
257
+ let leaseHeartbeat: ReturnType<typeof setInterval> | undefined;
242
258
  try {
243
259
  const workspace = await store.registerWorkspace(); const storedConfig = store.putScanConfig(config);
244
260
  const initialScan = await scan(context, config); const files = initialScan.files;
@@ -255,6 +271,13 @@ export async function runProjectInitialization(context: ProjectContext, input: {
255
271
  workspaceId: context.workspaceId,
256
272
  planningRoot: context.planningRoot,
257
273
  };
274
+ leaseHeartbeat = setInterval(() => {
275
+ try {
276
+ if (!store.renewRunLease(run.id, run.leaseToken)) diagnostics.push("Initialization lease heartbeat could not be renewed");
277
+ } catch (error) {
278
+ diagnostics.push(`Initialization lease heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
279
+ }
280
+ }, 60_000);
258
281
  const grouped = collect(files, context, config); const required = new Set(config.requiredKinds ?? FACT_KINDS);
259
282
  for (const repository of config.repositories) for (const kind of FACT_KINDS) {
260
283
  const repositoryFacts = grouped.get(kind)!.filter(fact => fact.repository === repository.name);
@@ -331,5 +354,8 @@ export async function runProjectInitialization(context: ProjectContext, input: {
331
354
  diagnostics.push(message);
332
355
  if (activeRun) store.failRun(activeRun.id, activeRun.leaseToken, message);
333
356
  throw error;
334
- } finally { store.close(); }
357
+ } finally {
358
+ if (leaseHeartbeat) clearInterval(leaseHeartbeat);
359
+ store.close();
360
+ }
335
361
  }