perttool 0.10.2 → 0.10.4

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 (48) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +12 -8
  3. package/dist/application/target-project-history.d.ts.map +1 -1
  4. package/dist/application/target-project-history.js +42 -1
  5. package/dist/application/target-project-history.js.map +1 -1
  6. package/dist/application/target-velocity-observation.d.ts.map +1 -1
  7. package/dist/application/target-velocity-observation.js +28 -1
  8. package/dist/application/target-velocity-observation.js.map +1 -1
  9. package/dist/cli.js +8 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/command/actuals-discovery.d.ts.map +1 -1
  12. package/dist/command/actuals-discovery.js +8 -0
  13. package/dist/command/actuals-discovery.js.map +1 -1
  14. package/dist/conversion/mermaid.d.ts.map +1 -1
  15. package/dist/conversion/mermaid.js +8 -2
  16. package/dist/conversion/mermaid.js.map +1 -1
  17. package/dist/formatter/source-formatter.js +2 -2
  18. package/dist/formatter/source-formatter.js.map +1 -1
  19. package/dist/help/assurance-guide.js +1 -1
  20. package/dist/help/assurance-guide.js.map +1 -1
  21. package/dist/history/git-probe.d.ts +18 -1
  22. package/dist/history/git-probe.d.ts.map +1 -1
  23. package/dist/history/git-probe.js +233 -68
  24. package/dist/history/git-probe.js.map +1 -1
  25. package/dist/history/project-history.d.ts +16 -1
  26. package/dist/history/project-history.d.ts.map +1 -1
  27. package/dist/history/project-history.js +119 -8
  28. package/dist/history/project-history.js.map +1 -1
  29. package/dist/history/velocity-observation.d.ts.map +1 -1
  30. package/dist/history/velocity-observation.js +3 -5
  31. package/dist/history/velocity-observation.js.map +1 -1
  32. package/dist/model/exact-duration-source.d.ts +7 -0
  33. package/dist/model/exact-duration-source.d.ts.map +1 -1
  34. package/dist/model/exact-duration-source.js +18 -2
  35. package/dist/model/exact-duration-source.js.map +1 -1
  36. package/dist/model/exact-velocity-source.d.ts +13 -0
  37. package/dist/model/exact-velocity-source.d.ts.map +1 -0
  38. package/dist/model/exact-velocity-source.js +38 -0
  39. package/dist/model/exact-velocity-source.js.map +1 -0
  40. package/dist/model/syntax.d.ts +2 -2
  41. package/dist/model/syntax.d.ts.map +1 -1
  42. package/dist/parser/document-parser.d.ts.map +1 -1
  43. package/dist/parser/document-parser.js +12 -29
  44. package/dist/parser/document-parser.js.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +1 -1
  48. package/schemas/Perttool.Common.v1.schema.json +40 -2
@@ -21,9 +21,21 @@ const availabilityOrder = new Map([
21
21
  ["ambiguous_path", 4],
22
22
  ["shallow_boundary", 5],
23
23
  ["unsupported_rename", 6],
24
- ["head_changed", 7],
25
- ["target_changed", 8],
24
+ ["provenance_unavailable", 7],
25
+ ["head_changed", 8],
26
+ ["target_changed", 9],
26
27
  ]);
28
+ function emptyProvenance(requestedMode) {
29
+ return Object.freeze({
30
+ modelVersion: 1,
31
+ requestedMode,
32
+ effectiveMode: "automatic",
33
+ overrideApplied: false,
34
+ rootCommitId: null,
35
+ rootSourceDigest: null,
36
+ excludedPredecessors: Object.freeze([]),
37
+ });
38
+ }
27
39
  function errorCode(error) {
28
40
  return typeof error === "object" && error !== null && "code" in error
29
41
  ? String(error.code)
@@ -200,29 +212,197 @@ function availability(causes) {
200
212
  return (left.commitId ?? "").localeCompare(right.commitId ?? "", "en");
201
213
  }));
202
214
  }
203
- function unavailable(requestedRevision, cause, fields = {}) {
215
+ function unavailable(requestedRevision, cause, fields) {
204
216
  return {
205
217
  ok: true,
206
218
  modelVersion: GIT_HISTORY_PROBE_MODEL_VERSION,
207
219
  status: "unavailable",
208
220
  traversal: "first_parent",
209
- objectFormat: fields.objectFormat ?? null,
210
- repositorySnapshotId: fields.repositorySnapshotId ?? null,
211
- repositoryRelativePath: fields.repositoryRelativePath ?? null,
221
+ objectFormat: nullable(fields.objectFormat),
222
+ repositorySnapshotId: nullable(fields.repositorySnapshotId),
223
+ repositoryRelativePath: nullable(fields.repositoryRelativePath),
212
224
  requestedRevision,
213
- resolvedRevision: fields.resolvedRevision ?? null,
214
- headCommitId: fields.headCommitId ?? null,
215
- currentSourceDigest: fields.currentSourceDigest ?? null,
216
- selectedSourceDigest: fields.selectedSourceDigest ?? null,
217
- inspectedCommitIds: fields.inspectedCommitIds ?? [],
218
- snapshots: fields.snapshots ?? [],
225
+ resolvedRevision: nullable(fields.resolvedRevision),
226
+ headCommitId: nullable(fields.headCommitId),
227
+ currentSourceDigest: nullable(fields.currentSourceDigest),
228
+ selectedSourceDigest: nullable(fields.selectedSourceDigest),
229
+ inspectedCommitIds: arrayValue(fields.inspectedCommitIds),
230
+ snapshots: arrayValue(fields.snapshots),
219
231
  availability: availability([{ cause, commitId: null }]),
232
+ provenance: fields.provenance ?? emptyProvenance("automatic"),
220
233
  };
221
234
  }
235
+ function nullable(value) {
236
+ return value === undefined ? null : value;
237
+ }
238
+ function arrayValue(value) {
239
+ return value === undefined ? [] : value;
240
+ }
222
241
  function commitId(value, objectFormat) {
223
242
  const length = objectFormat === "sha1" ? 40 : 64;
224
243
  return new RegExp(`^[0-9a-f]{${length}}$`).test(value);
225
244
  }
245
+ function decodeGitPath(value) {
246
+ try {
247
+ const path = new TextDecoder("utf-8", { fatal: true }).decode(value);
248
+ return path === "" || /[\u0000\r\n]/.test(path) ? null : path;
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ function renameCandidates(value, selectedPath) {
255
+ if (value.length === 0)
256
+ return Object.freeze([]);
257
+ const fields = value.subarray(0, value.at(-1) === 0 ? -1 : value.length)
258
+ .toString("latin1").split("\u0000").map((field) => Buffer.from(field, "latin1"));
259
+ const result = [];
260
+ for (let index = 0; index < fields.length;) {
261
+ const record = consumeDiffRecord(fields, index, selectedPath);
262
+ if (record === null)
263
+ return null;
264
+ index = record.nextIndex;
265
+ if (record.candidate !== null)
266
+ result.push(record.candidate);
267
+ }
268
+ return Object.freeze(result);
269
+ }
270
+ function consumeDiffRecord(fields, index, selectedPath) {
271
+ const statusField = fields[index];
272
+ if (statusField === undefined)
273
+ return null;
274
+ const status = statusField.toString("ascii");
275
+ if (!/^[A-Z][0-9]*$/.test(status))
276
+ return null;
277
+ if (!status.startsWith("R") && !status.startsWith("C")) {
278
+ const path = fields[index + 1];
279
+ return path !== undefined && decodeGitPath(path) !== null
280
+ ? { nextIndex: index + 2, candidate: null }
281
+ : null;
282
+ }
283
+ const oldPath = fields[index + 1];
284
+ const newPath = fields[index + 2];
285
+ if (oldPath === undefined || newPath === undefined)
286
+ return null;
287
+ const oldText = decodeGitPath(oldPath);
288
+ const newText = decodeGitPath(newPath);
289
+ if (oldText === null || newText === null)
290
+ return null;
291
+ const candidate = status.startsWith("R") && newText === selectedPath
292
+ ? { oldPath: oldText, newPath: newText }
293
+ : null;
294
+ return { nextIndex: index + 3, candidate };
295
+ }
296
+ function collectHistorySnapshots(request) {
297
+ const snapshots = [];
298
+ for (const inspectedCommitId of request.inspectedCommitIds) {
299
+ const metadataCommand = runGit(request.executable, request.repositoryRoot, "commit_metadata", ["show", "-s", "--format=%P%x00%cI", inspectedCommitId]);
300
+ if (!metadataCommand.ok) {
301
+ return metadataCommand.kind === "failure"
302
+ ? metadataCommand.failure
303
+ : commandFailure("commit_metadata");
304
+ }
305
+ const metadataText = stdoutText(metadataCommand, "commit_metadata");
306
+ if (typeof metadataText !== "string")
307
+ return metadataText;
308
+ const metadata = parseGitCommitMetadata(metadataText, request.objectFormat);
309
+ if (metadata === null)
310
+ return malformed("commit_metadata");
311
+ const exists = runGit(request.executable, request.repositoryRoot, "snapshot_exists", ["ls-tree", "-z", "--name-only", inspectedCommitId, "--", request.relativePath]);
312
+ if (!exists.ok) {
313
+ return exists.kind === "failure"
314
+ ? exists.failure
315
+ : commandFailure("snapshot_exists");
316
+ }
317
+ let source = null;
318
+ let sourceDigest = null;
319
+ if (exists.stdout.length > 0) {
320
+ if (!exists.stdout.equals(Buffer.from(`${request.relativePath}\0`, "utf8"))) {
321
+ return malformed("snapshot_exists");
322
+ }
323
+ const sourceCommand = runGit(request.executable, request.repositoryRoot, "snapshot_source", ["cat-file", "blob", `${inspectedCommitId}:${request.relativePath}`]);
324
+ if (!sourceCommand.ok) {
325
+ return sourceCommand.kind === "failure"
326
+ ? sourceCommand.failure
327
+ : commandFailure("snapshot_source");
328
+ }
329
+ source = new Uint8Array(sourceCommand.stdout);
330
+ sourceDigest = digestDocumentBytes(source);
331
+ }
332
+ snapshots.push({
333
+ repositorySnapshotId: request.repositorySnapshotId,
334
+ relativePath: request.relativePath,
335
+ commitId: inspectedCommitId,
336
+ parentCommitIds: metadata.parentCommitIds,
337
+ recordedAt: metadata.recordedAt,
338
+ sourceDigest,
339
+ source,
340
+ });
341
+ }
342
+ return { ok: true, snapshots: Object.freeze(snapshots) };
343
+ }
344
+ function treeBlobId(value, expectedPath, objectFormat) {
345
+ const fields = value.subarray(0, value.at(-1) === 0 ? -1 : value.length)
346
+ .toString("latin1").split("\u0000").map((field) => Buffer.from(field, "latin1"));
347
+ if (fields.length !== 1)
348
+ return null;
349
+ const separator = fields[0].indexOf(0x09);
350
+ if (separator < 0)
351
+ return null;
352
+ const metadata = fields[0].subarray(0, separator).toString("ascii");
353
+ const path = decodeGitPath(fields[0].subarray(separator + 1));
354
+ const match = /^[0-7]{6} blob ([0-9a-f]+)$/.exec(metadata);
355
+ return path === expectedPath && match !== null && commitId(match[1], objectFormat)
356
+ ? match[1]
357
+ : null;
358
+ }
359
+ function probeNewRootProvenance(executable, repositoryRoot, relativePath, objectFormat, snapshots) {
360
+ const root = snapshots[0];
361
+ if (root === undefined || root.source === null || root.sourceDigest === null) {
362
+ return null;
363
+ }
364
+ if (root.parentCommitIds.length !== 1)
365
+ return null;
366
+ const parentCommitId = root.parentCommitIds[0];
367
+ const diff = runGit(executable, repositoryRoot, "provenance_root_diff", [
368
+ "diff-tree", "-r", "--name-status", "-z", "-M",
369
+ parentCommitId, root.commitId,
370
+ ]);
371
+ if (!diff.ok)
372
+ return null;
373
+ const candidates = renameCandidates(diff.stdout, relativePath);
374
+ if (candidates === null || candidates.length !== 1)
375
+ return null;
376
+ const predecessorPath = candidates[0].oldPath;
377
+ const tree = runGit(executable, repositoryRoot, "provenance_predecessor_tree", [
378
+ "ls-tree", "-z", parentCommitId, "--", predecessorPath,
379
+ ]);
380
+ if (!tree.ok)
381
+ return null;
382
+ const blobId = treeBlobId(tree.stdout, predecessorPath, objectFormat);
383
+ if (blobId === null)
384
+ return null;
385
+ const source = runGit(executable, repositoryRoot, "provenance_predecessor_source", [
386
+ "cat-file", "blob", blobId,
387
+ ]);
388
+ if (!source.ok)
389
+ return null;
390
+ const predecessorSource = new Uint8Array(source.stdout);
391
+ return Object.freeze({
392
+ modelVersion: 1,
393
+ requestedMode: "new_root",
394
+ effectiveMode: "new_root",
395
+ overrideApplied: true,
396
+ rootCommitId: root.commitId,
397
+ rootSourceDigest: root.sourceDigest,
398
+ excludedPredecessors: Object.freeze([Object.freeze({
399
+ path: predecessorPath,
400
+ commitId: parentCommitId,
401
+ sourceDigest: digestDocumentBytes(predecessorSource),
402
+ source: predecessorSource,
403
+ })]),
404
+ });
405
+ }
226
406
  export function parseGitCommitMetadata(value, objectFormat) {
227
407
  const separatorIndex = value.indexOf("\0");
228
408
  if (separatorIndex === -1)
@@ -266,12 +446,16 @@ function sameTarget(left, right) {
266
446
  }
267
447
  export async function probeGitHistory(request, dependencies = {}) {
268
448
  const requestedRevision = request.revision ?? "HEAD";
449
+ const requestedProvenanceMode = request.provenanceMode ?? "automatic";
450
+ const initialProvenance = emptyProvenance(requestedProvenanceMode);
269
451
  const executable = dependencies.gitExecutable ?? "git";
270
452
  const targetPath = resolve(request.targetPath);
271
453
  const initialTarget = await captureTarget(targetPath);
272
454
  if (!initialTarget.ok) {
273
455
  if ("ambiguous" in initialTarget) {
274
- return unavailable(requestedRevision, "ambiguous_path");
456
+ return unavailable(requestedRevision, "ambiguous_path", {
457
+ provenance: initialProvenance,
458
+ });
275
459
  }
276
460
  return initialTarget.failure;
277
461
  }
@@ -282,6 +466,7 @@ export async function probeGitHistory(request, dependencies = {}) {
282
466
  }
283
467
  return unavailable(requestedRevision, "no_repository", {
284
468
  currentSourceDigest: initialTarget.capture.digest,
469
+ provenance: initialProvenance,
285
470
  });
286
471
  }
287
472
  const repositoryText = stdoutText(repositoryCommand, "repository_root");
@@ -298,6 +483,7 @@ export async function probeGitHistory(request, dependencies = {}) {
298
483
  if (relativePath === null) {
299
484
  return unavailable(requestedRevision, "ambiguous_path", {
300
485
  currentSourceDigest: initialTarget.capture.digest,
486
+ provenance: initialProvenance,
301
487
  });
302
488
  }
303
489
  const headCommand = runGit(executable, repositoryRoot, "resolve_head", ["rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"]);
@@ -307,6 +493,7 @@ export async function probeGitHistory(request, dependencies = {}) {
307
493
  return unavailable(requestedRevision, "no_head", {
308
494
  repositoryRelativePath: relativePath,
309
495
  currentSourceDigest: initialTarget.capture.digest,
496
+ provenance: initialProvenance,
310
497
  });
311
498
  }
312
499
  const formatCommand = runGit(executable, repositoryRoot, "object_format", ["rev-parse", "--show-object-format=storage"]);
@@ -342,6 +529,7 @@ export async function probeGitHistory(request, dependencies = {}) {
342
529
  repositoryRelativePath: relativePath,
343
530
  headCommitId: initialHead,
344
531
  currentSourceDigest: initialTarget.capture.digest,
532
+ provenance: initialProvenance,
345
533
  });
346
534
  }
347
535
  const revisionText = stdoutText(revisionCommand, "resolve_revision");
@@ -361,6 +549,7 @@ export async function probeGitHistory(request, dependencies = {}) {
361
549
  resolvedRevision,
362
550
  headCommitId: initialHead,
363
551
  currentSourceDigest: initialTarget.capture.digest,
552
+ provenance: initialProvenance,
364
553
  });
365
554
  }
366
555
  const trackedCommand = runGit(executable, repositoryRoot, "selected_target", [
@@ -384,6 +573,7 @@ export async function probeGitHistory(request, dependencies = {}) {
384
573
  resolvedRevision,
385
574
  headCommitId: initialHead,
386
575
  currentSourceDigest: initialTarget.capture.digest,
576
+ provenance: initialProvenance,
387
577
  });
388
578
  }
389
579
  if (!trackedCommand.stdout.equals(Buffer.from(`${relativePath}\0`, "utf8"))) {
@@ -394,6 +584,7 @@ export async function probeGitHistory(request, dependencies = {}) {
394
584
  resolvedRevision,
395
585
  headCommitId: initialHead,
396
586
  currentSourceDigest: initialTarget.capture.digest,
587
+ provenance: initialProvenance,
397
588
  });
398
589
  }
399
590
  const commitsCommand = runGit(executable, repositoryRoot, "first_parent_commits", [
@@ -420,61 +611,32 @@ export async function probeGitHistory(request, dependencies = {}) {
420
611
  new Set(inspectedCommitIds).size !== inspectedCommitIds.length) {
421
612
  return malformed("first_parent_commits");
422
613
  }
423
- const snapshots = [];
424
- for (const inspectedCommitId of inspectedCommitIds) {
425
- const metadataCommand = runGit(executable, repositoryRoot, "commit_metadata", [
426
- "show",
427
- "-s",
428
- "--format=%P%x00%cI",
429
- inspectedCommitId,
430
- ]);
431
- if (!metadataCommand.ok) {
432
- return metadataCommand.kind === "failure"
433
- ? metadataCommand.failure
434
- : commandFailure("commit_metadata");
435
- }
436
- const metadataText = stdoutText(metadataCommand, "commit_metadata");
437
- if (typeof metadataText !== "string")
438
- return metadataText;
439
- const metadata = parseGitCommitMetadata(metadataText, objectFormat);
440
- if (metadata === null)
441
- return malformed("commit_metadata");
442
- const existsCommand = runGit(executable, repositoryRoot, "snapshot_exists", [
443
- "ls-tree",
444
- "-z",
445
- "--name-only",
446
- inspectedCommitId,
447
- "--",
448
- relativePath,
449
- ]);
450
- if (!existsCommand.ok) {
451
- return existsCommand.kind === "failure"
452
- ? existsCommand.failure
453
- : commandFailure("snapshot_exists");
454
- }
455
- let source = null;
456
- let sourceDigest = null;
457
- if (existsCommand.stdout.length > 0) {
458
- if (!existsCommand.stdout.equals(Buffer.from(`${relativePath}\0`, "utf8"))) {
459
- return malformed("snapshot_exists");
460
- }
461
- const sourceCommand = runGit(executable, repositoryRoot, "snapshot_source", ["cat-file", "blob", `${inspectedCommitId}:${relativePath}`]);
462
- if (!sourceCommand.ok) {
463
- return sourceCommand.kind === "failure"
464
- ? sourceCommand.failure
465
- : commandFailure("snapshot_source");
466
- }
467
- source = new Uint8Array(sourceCommand.stdout);
468
- sourceDigest = digestDocumentBytes(source);
469
- }
470
- snapshots.push({
614
+ const snapshotCollection = collectHistorySnapshots({
615
+ executable,
616
+ repositoryRoot,
617
+ repositorySnapshotId,
618
+ relativePath,
619
+ objectFormat,
620
+ inspectedCommitIds,
621
+ });
622
+ if (!snapshotCollection.ok)
623
+ return snapshotCollection;
624
+ const snapshots = snapshotCollection.snapshots;
625
+ const provenance = requestedProvenanceMode === "new_root"
626
+ ? probeNewRootProvenance(executable, repositoryRoot, relativePath, objectFormat, snapshots)
627
+ : initialProvenance;
628
+ if (provenance === null) {
629
+ return unavailable(requestedRevision, "provenance_unavailable", {
630
+ objectFormat,
471
631
  repositorySnapshotId,
472
- relativePath,
473
- commitId: inspectedCommitId,
474
- parentCommitIds: metadata.parentCommitIds,
475
- recordedAt: metadata.recordedAt,
476
- sourceDigest,
477
- source,
632
+ repositoryRelativePath: relativePath,
633
+ resolvedRevision,
634
+ headCommitId: initialHead,
635
+ currentSourceDigest: initialTarget.capture.digest,
636
+ selectedSourceDigest: snapshots.at(-1)?.sourceDigest ?? null,
637
+ inspectedCommitIds,
638
+ snapshots,
639
+ provenance: initialProvenance,
478
640
  });
479
641
  }
480
642
  const renameCommand = runGit(executable, repositoryRoot, "rename_detection", [
@@ -523,6 +685,7 @@ export async function probeGitHistory(request, dependencies = {}) {
523
685
  selectedSourceDigest: snapshots.at(-1)?.sourceDigest ?? null,
524
686
  inspectedCommitIds,
525
687
  snapshots,
688
+ provenance,
526
689
  });
527
690
  }
528
691
  const finalHeadText = stdoutText(finalHeadCommand, "recheck_head");
@@ -547,7 +710,8 @@ export async function probeGitHistory(request, dependencies = {}) {
547
710
  commitId: inspectedCommitIds[0] ?? null,
548
711
  });
549
712
  }
550
- if (/(?:^|\n)R\d+\t/.test(renameText)) {
713
+ if (requestedProvenanceMode === "automatic" &&
714
+ /(?:^|\n)R\d+\t/.test(renameText)) {
551
715
  incompleteCauses.push({
552
716
  cause: "unsupported_rename",
553
717
  commitId: null,
@@ -578,6 +742,7 @@ export async function probeGitHistory(request, dependencies = {}) {
578
742
  inspectedCommitIds: Object.freeze(inspectedCommitIds),
579
743
  snapshots: Object.freeze(snapshots),
580
744
  availability: allCauses,
745
+ provenance,
581
746
  };
582
747
  }
583
748
  function historicalLimits(overrides) {