project-tiny-context-harness 0.8.12 → 0.8.13

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 (158) hide show
  1. package/README.md +43 -19
  2. package/assets/README.md +81 -40
  3. package/assets/README.zh-CN.md +44 -22
  4. package/assets/agents/AGENTS_CORE.md +8 -4
  5. package/assets/skills/context_development_engineer/SKILL.md +9 -1
  6. package/assets/skills/design-resource-authoring/SKILL.md +7 -5
  7. package/assets/skills/design-resource-authoring/references/downstream-handoff.md +2 -2
  8. package/assets/skills/design-resource-authoring/references/open-design-provider.md +22 -2
  9. package/assets/skills/design-resource-authoring/references/recovery-and-writeback.md +199 -0
  10. package/assets/skills/design-resource-authoring/references/resource-selection.md +2 -2
  11. package/assets/skills/long-task-workflow/SKILL.md +6 -6
  12. package/assets/skills/long-task-workflow/references/contract-authoring.md +27 -22
  13. package/assets/skills/long-task-workflow/references/evidence-design.md +49 -32
  14. package/assets/skills/long-task-workflow/references/source-authoring.md +2 -2
  15. package/dist/commands/design-resource-recovery.d.ts +1 -0
  16. package/dist/commands/design-resource-recovery.js +152 -0
  17. package/dist/commands/design-resource.js +20 -1
  18. package/dist/commands/index.js +1 -1
  19. package/dist/lib/design-resource-reconciliation-codec.d.ts +2 -0
  20. package/dist/lib/design-resource-reconciliation-codec.js +174 -0
  21. package/dist/lib/design-resource-reconciliation-types.d.ts +62 -0
  22. package/dist/lib/design-resource-reconciliation-types.js +1 -0
  23. package/dist/lib/design-resource-reconciliation.d.ts +3 -0
  24. package/dist/lib/design-resource-reconciliation.js +224 -0
  25. package/dist/lib/design-resource-recovery-authority-policy.d.ts +8 -0
  26. package/dist/lib/design-resource-recovery-authority-policy.js +123 -0
  27. package/dist/lib/design-resource-recovery-catalog-resources.d.ts +6 -0
  28. package/dist/lib/design-resource-recovery-catalog-resources.js +121 -0
  29. package/dist/lib/design-resource-recovery-catalog-shape.d.ts +3 -0
  30. package/dist/lib/design-resource-recovery-catalog-shape.js +104 -0
  31. package/dist/lib/design-resource-recovery-catalog.d.ts +9 -0
  32. package/dist/lib/design-resource-recovery-catalog.js +103 -0
  33. package/dist/lib/design-resource-recovery-cleanup.d.ts +2 -0
  34. package/dist/lib/design-resource-recovery-cleanup.js +11 -0
  35. package/dist/lib/design-resource-recovery-codec-primitives.d.ts +19 -0
  36. package/dist/lib/design-resource-recovery-codec-primitives.js +92 -0
  37. package/dist/lib/design-resource-recovery-codec.d.ts +4 -0
  38. package/dist/lib/design-resource-recovery-codec.js +93 -0
  39. package/dist/lib/design-resource-recovery-current.d.ts +9 -0
  40. package/dist/lib/design-resource-recovery-current.js +41 -0
  41. package/dist/lib/design-resource-recovery-delta-policy.d.ts +5 -0
  42. package/dist/lib/design-resource-recovery-delta-policy.js +108 -0
  43. package/dist/lib/design-resource-recovery-files.d.ts +37 -0
  44. package/dist/lib/design-resource-recovery-files.js +204 -0
  45. package/dist/lib/design-resource-recovery-final-disposition-shape.d.ts +2 -0
  46. package/dist/lib/design-resource-recovery-final-disposition-shape.js +47 -0
  47. package/dist/lib/design-resource-recovery-patch-types.d.ts +54 -0
  48. package/dist/lib/design-resource-recovery-patch-types.js +1 -0
  49. package/dist/lib/design-resource-recovery-replay.d.ts +7 -0
  50. package/dist/lib/design-resource-recovery-replay.js +112 -0
  51. package/dist/lib/design-resource-recovery-repository-bindings.d.ts +9 -0
  52. package/dist/lib/design-resource-recovery-repository-bindings.js +49 -0
  53. package/dist/lib/design-resource-recovery-schema.d.ts +5 -0
  54. package/dist/lib/design-resource-recovery-schema.js +5 -0
  55. package/dist/lib/design-resource-recovery-shape.d.ts +10 -0
  56. package/dist/lib/design-resource-recovery-shape.js +184 -0
  57. package/dist/lib/design-resource-recovery-source-authority.d.ts +5 -0
  58. package/dist/lib/design-resource-recovery-source-authority.js +219 -0
  59. package/dist/lib/design-resource-recovery-text.d.ts +17 -0
  60. package/dist/lib/design-resource-recovery-text.js +186 -0
  61. package/dist/lib/design-resource-recovery-types.d.ts +197 -0
  62. package/dist/lib/design-resource-recovery-types.js +1 -0
  63. package/dist/lib/design-resource-recovery-writeback-policy.d.ts +5 -0
  64. package/dist/lib/design-resource-recovery-writeback-policy.js +326 -0
  65. package/dist/lib/design-resource-recovery-writeback-shape.d.ts +6 -0
  66. package/dist/lib/design-resource-recovery-writeback-shape.js +155 -0
  67. package/dist/lib/design-resource-recovery.d.ts +44 -0
  68. package/dist/lib/design-resource-recovery.js +216 -0
  69. package/dist/lib/long-task-acceptance-shape.js +8 -2
  70. package/dist/lib/long-task-activation-validation.js +13 -3
  71. package/dist/lib/long-task-admitted-observation-records.d.ts +25 -0
  72. package/dist/lib/long-task-admitted-observation-records.js +40 -0
  73. package/dist/lib/long-task-admitted-observation.d.ts +93 -0
  74. package/dist/lib/long-task-admitted-observation.js +201 -0
  75. package/dist/lib/long-task-artifacts.d.ts +1 -0
  76. package/dist/lib/long-task-artifacts.js +1 -0
  77. package/dist/lib/long-task-authority-material-diff.js +1 -0
  78. package/dist/lib/long-task-authority-policy.d.ts +2 -0
  79. package/dist/lib/long-task-authority-policy.js +2 -0
  80. package/dist/lib/long-task-authority-revision-details.js +3 -1
  81. package/dist/lib/long-task-authority.js +2 -1
  82. package/dist/lib/long-task-check-execution-policy.js +5 -0
  83. package/dist/lib/long-task-check-runner.d.ts +2 -2
  84. package/dist/lib/long-task-check-runner.js +440 -23
  85. package/dist/lib/long-task-claims.d.ts +18 -0
  86. package/dist/lib/long-task-claims.js +35 -0
  87. package/dist/lib/long-task-compact-structure-targets.js +1 -0
  88. package/dist/lib/long-task-conformance-policy.js +3 -1
  89. package/dist/lib/long-task-counterfactual-claim-policy.d.ts +3 -4
  90. package/dist/lib/long-task-counterfactual-claim-policy.js +11 -3
  91. package/dist/lib/long-task-counterfactual-sandbox.js +1 -0
  92. package/dist/lib/long-task-counterfactual-types.d.ts +2 -0
  93. package/dist/lib/long-task-delivery-compiler.js +2 -0
  94. package/dist/lib/long-task-evidence-capability-policy.d.ts +3 -2
  95. package/dist/lib/long-task-evidence-capability-policy.js +114 -8
  96. package/dist/lib/long-task-evidence-capability-runtime.d.ts +5 -2
  97. package/dist/lib/long-task-evidence-capability-runtime.js +149 -5
  98. package/dist/lib/long-task-evidence-sensitivity-policy.d.ts +9 -0
  99. package/dist/lib/long-task-evidence-sensitivity-policy.js +31 -1
  100. package/dist/lib/long-task-evidence-v2.d.ts +4 -4
  101. package/dist/lib/long-task-evidence-v2.js +321 -43
  102. package/dist/lib/long-task-exact-comparison.d.ts +16 -0
  103. package/dist/lib/long-task-exact-comparison.js +28 -0
  104. package/dist/lib/long-task-execution-observation.d.ts +24 -0
  105. package/dist/lib/long-task-execution-observation.js +332 -0
  106. package/dist/lib/long-task-explain-acceptance-link.d.ts +4 -0
  107. package/dist/lib/long-task-explain-acceptance-link.js +4 -0
  108. package/dist/lib/long-task-explain-claim-links.d.ts +2 -0
  109. package/dist/lib/long-task-explain-claim-links.js +2 -0
  110. package/dist/lib/long-task-explain-source-links.d.ts +6 -0
  111. package/dist/lib/long-task-final-v2.js +7 -4
  112. package/dist/lib/long-task-json-pointer-observation.d.ts +59 -0
  113. package/dist/lib/long-task-json-pointer-observation.js +184 -0
  114. package/dist/lib/long-task-observation-artifact.d.ts +8 -0
  115. package/dist/lib/long-task-observation-artifact.js +58 -0
  116. package/dist/lib/long-task-observation-authority.d.ts +17 -0
  117. package/dist/lib/long-task-observation-authority.js +348 -0
  118. package/dist/lib/long-task-paths.js +1 -1
  119. package/dist/lib/long-task-process-observation.d.ts +6 -0
  120. package/dist/lib/long-task-process-observation.js +117 -0
  121. package/dist/lib/long-task-process-runtime-closure.d.ts +13 -0
  122. package/dist/lib/long-task-process-runtime-closure.js +175 -0
  123. package/dist/lib/long-task-progress.js +2 -0
  124. package/dist/lib/long-task-protected-files.d.ts +1 -1
  125. package/dist/lib/long-task-protected-files.js +1 -30
  126. package/dist/lib/long-task-risk.js +8 -3
  127. package/dist/lib/long-task-root-shape.js +10 -1
  128. package/dist/lib/long-task-runner-environment.d.ts +1 -1
  129. package/dist/lib/long-task-runner-environment.js +4 -4
  130. package/dist/lib/long-task-runner-freeze.d.ts +4 -2
  131. package/dist/lib/long-task-runner-freeze.js +128 -28
  132. package/dist/lib/long-task-runtime-types.d.ts +111 -0
  133. package/dist/lib/long-task-scoped-binding.d.ts +13 -0
  134. package/dist/lib/long-task-scoped-binding.js +11 -0
  135. package/dist/lib/long-task-semantic-contract-types.d.ts +1 -0
  136. package/dist/lib/long-task-semantic-fact-contract-closure.js +3 -3
  137. package/dist/lib/long-task-semantic-fact-evidence.d.ts +3 -3
  138. package/dist/lib/long-task-semantic-fact-evidence.js +75 -58
  139. package/dist/lib/long-task-source-claim-validation.js +4 -1
  140. package/dist/lib/long-task-source-target-continuity.d.ts +2 -2
  141. package/dist/lib/long-task-source-target-continuity.js +35 -2
  142. package/dist/lib/long-task-source-target-index.d.ts +3 -0
  143. package/dist/lib/long-task-source-target-index.js +19 -0
  144. package/dist/lib/long-task-stage-policy.js +10 -4
  145. package/dist/lib/long-task-static-observation-freeze.d.ts +83 -0
  146. package/dist/lib/long-task-static-observation-freeze.js +428 -0
  147. package/dist/lib/long-task-status-projection.js +5 -1
  148. package/dist/lib/long-task-target-policy.d.ts +1 -1
  149. package/dist/lib/long-task-target-policy.js +3 -0
  150. package/dist/lib/long-task-verifier-dependency-closure.js +7 -3
  151. package/dist/lib/long-task-verifier-v2.js +70 -9
  152. package/dist/lib/long-task-workspace.d.ts +1 -1
  153. package/dist/lib/long-task-workspace.js +1 -12
  154. package/dist/lib/repository-path-safety.d.ts +14 -0
  155. package/dist/lib/repository-path-safety.js +126 -0
  156. package/dist/schemas/long-task-delivery-v2/long-task-delivery-v2.schema.json +53 -8
  157. package/migrations/README.md +77 -0
  158. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { CompiledSourceItemV2, DeliveryContractV2 } from "./long-task-delivery-types.js";
1
+ import type { CompiledSourceItemV2, DeliveryContractV2, SourceBackedExecutionTargetV2 } from "./long-task-delivery-types.js";
2
2
  type ValidationReporter = (message: string) => void;
3
- export declare function validateSourceTargetContinuity(contract: DeliveryContractV2, items: CompiledSourceItemV2[], report?: ValidationReporter): void;
3
+ export declare function validateSourceTargetContinuity(contract: DeliveryContractV2, items: CompiledSourceItemV2[], report?: ValidationReporter): Map<string, SourceBackedExecutionTargetV2>;
4
4
  export {};
@@ -1,11 +1,14 @@
1
1
  import { resolveAcceptanceAssertion } from "./long-task-acceptance-reference.js";
2
- import { buildCanonicalSourceTargetIndex, sourceKindForTarget, } from "./long-task-source-target-index.js";
2
+ import { buildCanonicalSourceTargetIndex, executionTargetSourceTargetRef, sourceKindForTarget, } from "./long-task-source-target-index.js";
3
+ import { canonicalValueJson, sha256Hex } from "./strict-codec.js";
3
4
  export function validateSourceTargetContinuity(contract, items, report) {
4
5
  const state = sourceContinuityState(contract, items);
5
6
  for (const claim of contract.source_claims)
6
7
  validateSourceClaimContinuity(claim, state, report);
7
8
  validateAcceptanceBindingContinuity(contract, state, report);
8
9
  validateArchitectureContinuity(state, report);
10
+ validateRequiredExecutionTargetContinuity(contract, state, report);
11
+ return state.executionTargetAuthorities;
9
12
  }
10
13
  function sourceContinuityState(contract, items) {
11
14
  return {
@@ -19,6 +22,7 @@ function sourceContinuityState(contract, items) {
19
22
  .filter((item) => item.aspect === "architecture")
20
23
  .map((item) => item.key)),
21
24
  ownedArchitectureItems: new Set(),
25
+ executionTargetAuthorities: new Map(),
22
26
  };
23
27
  }
24
28
  function validateSourceClaimContinuity(claim, state, report) {
@@ -46,7 +50,9 @@ function validateSourceClaimContinuity(claim, state, report) {
46
50
  }
47
51
  if (target.normalized_text !== undefined &&
48
52
  target.normalized_text !== item.normalized_text) {
49
- issue(report, `source_target_statement_mismatch:${claim.key}:${ref}`);
53
+ issue(report, target.execution_target_ref
54
+ ? `process_root_source_identity_mismatch:${target.execution_target_ref}:${claim.key}`
55
+ : `source_target_statement_mismatch:${claim.key}:${ref}`);
50
56
  return;
51
57
  }
52
58
  const owner = state.owners.get(ref);
@@ -73,6 +79,33 @@ function validateSourceClaimContinuity(claim, state, report) {
73
79
  state.sourceBackedGlobalClaims.add(ref);
74
80
  if (target.kind === "acceptance")
75
81
  state.acceptanceBindings.push({ claim, target });
82
+ if (target.execution_target_ref)
83
+ state.executionTargetAuthorities.set(target.execution_target_ref, {
84
+ target_ref: target.execution_target_ref,
85
+ canonical_target_ref: ref,
86
+ source_claim_key: claim.key,
87
+ source_item_key: item.key,
88
+ source_path: item.source_path,
89
+ source_text_sha256: item.text_sha256,
90
+ target_identity: sha256Hex(canonicalValueJson({
91
+ canonical_target_ref: ref,
92
+ source_claim_key: claim.key,
93
+ source_item_key: item.key,
94
+ source_path: item.source_path,
95
+ source_text_sha256: item.text_sha256,
96
+ normalized_text: item.normalized_text,
97
+ })),
98
+ });
99
+ }
100
+ function validateRequiredExecutionTargetContinuity(contract, state, report) {
101
+ for (const targetRef of contract.task.target_profile.required_target_refs) {
102
+ if (state.executionTargetAuthorities.has(targetRef))
103
+ continue;
104
+ const target = contract.task.execution_targets.find((candidate) => candidate.key === targetRef);
105
+ issue(report, target?.runtime_family === "process"
106
+ ? `process_root_source_binding_required:${targetRef}:${executionTargetSourceTargetRef(targetRef)}`
107
+ : `execution_target_source_binding_required:${targetRef}:${executionTargetSourceTargetRef(targetRef)}`);
108
+ }
76
109
  }
77
110
  function validateAcceptanceBindingContinuity(contract, state, report) {
78
111
  for (const { claim, target } of state.acceptanceBindings) {
@@ -7,6 +7,9 @@ export interface CanonicalSourceTarget {
7
7
  outcome_key?: string;
8
8
  risk_fact?: keyof DeliveryContractV2["risk"]["facts"];
9
9
  affected_outcome?: string;
10
+ execution_target_ref?: string;
10
11
  }
11
12
  export declare function buildCanonicalSourceTargetIndex(contract: DeliveryContractV2): Map<string, CanonicalSourceTarget>;
13
+ export declare function executionTargetSourceTargetRef(targetKey: string): string;
14
+ export declare function executionTargetSourceStatement(targetValue: DeliveryContractV2["task"]["execution_targets"][number]): string;
12
15
  export declare function sourceKindForTarget(target: CanonicalSourceTarget): SourceItemKind | null;
@@ -1,7 +1,13 @@
1
1
  import { controlFieldFacts } from "./long-task-control-fields.js";
2
2
  import { normalizeSourceItemText } from "./long-task-source-item-parser.js";
3
+ import { canonicalValueJson } from "./strict-codec.js";
3
4
  export function buildCanonicalSourceTargetIndex(contract) {
4
5
  const targets = [];
6
+ for (const executionTarget of contract.task.execution_targets)
7
+ targets.push({
8
+ ...target(executionTargetSourceTargetRef(executionTarget.key), "technical_obligation", executionTargetSourceStatement(executionTarget)),
9
+ execution_target_ref: executionTarget.key,
10
+ });
5
11
  for (const outcome of contract.outcomes) {
6
12
  targets.push(target(`${outcome.key}.result`, "outcome_result", outcome.product.observable_result, outcome.key));
7
13
  for (const requirement of outcome.product.requirements)
@@ -52,6 +58,19 @@ export function buildCanonicalSourceTargetIndex(contract) {
52
58
  targets.push(target(item.key, "external_confirmation", item.description));
53
59
  return new Map(targets.map((item) => [item.ref, item]));
54
60
  }
61
+ export function executionTargetSourceTargetRef(targetKey) {
62
+ return `execution_target.${targetKey}`;
63
+ }
64
+ export function executionTargetSourceStatement(targetValue) {
65
+ return `Execution target authority: ${canonicalValueJson({
66
+ key: targetValue.key,
67
+ role: targetValue.role,
68
+ runtime_family: targetValue.runtime_family,
69
+ root_entrypoint: targetValue.root_entrypoint,
70
+ root_argv: [...(targetValue.root_argv ?? [])],
71
+ capabilities: [...new Set(targetValue.capabilities)].sort(),
72
+ })}.`;
73
+ }
55
74
  function target(ref, kind, text, outcomeKey, scope) {
56
75
  return {
57
76
  ref,
@@ -1,3 +1,4 @@
1
+ import { outcomeResultExternallyBlocked } from "./long-task-claims.js";
1
2
  export function validateDeliveryStages(contract, report) {
2
3
  unique(contract.stages.map((stage) => stage.key), "stage_key_duplicate", report);
3
4
  const stages = new Map(contract.stages.map((stage) => [stage.key, stage]));
@@ -60,17 +61,22 @@ function validateStageGateClosure(contract, report) {
60
61
  issue(report, "stage_outcome_missing_gate_dependency", `${stage.key}:${outcome.key}:${dependencyGate}`);
61
62
  }
62
63
  const stageGateChecks = gate.acceptance.checks.filter((check) => check.journey_roles.includes("stage_gate"));
63
- if (!stageGateChecks.length)
64
+ const resultExternallyBlocked = outcomeResultExternallyBlocked(contract, gate.key);
65
+ if (!stageGateChecks.length && !resultExternallyBlocked)
64
66
  issue(report, "stage_gate_check_required", `${stage.key}:${gate.key}`);
65
- else if (!stageGateChecks.some((check) => provesGate(check, gate)))
67
+ else if (!resultExternallyBlocked &&
68
+ !stageGateChecks.some((check) => provesGate(check, gate)))
66
69
  issue(report, "stage_gate_target_runtime_result_required", `${stage.key}:${gate.key}`);
67
- for (const targetRef of contract.task.target_profile.required_target_refs)
70
+ for (const targetRef of resultExternallyBlocked
71
+ ? []
72
+ : contract.task.target_profile.required_target_refs)
68
73
  if (!stageGateChecks.some((check) => check.execution_target.target_ref === targetRef &&
69
74
  check.execution_target.entrypoint === "root" &&
70
75
  provesGate(check, gate)))
71
76
  issue(report, "stage_gate_required_target_proof_missing", `${stage.key}:${targetRef}`);
72
77
  const stageOutcomes = contract.outcomes.filter((candidate) => candidate.stage === stage.key);
73
- if (stageOutcomes.length > 1 &&
78
+ if (!resultExternallyBlocked &&
79
+ stageOutcomes.length > 1 &&
74
80
  !stageGateChecks.some((check) => [...check.positive_assertions, ...check.negative_assertions].some((assertion) => assertion.evidence_capabilities.includes("cross_surface_consistency"))))
75
81
  issue(report, "stage_gate_cross_surface_consistency_required", `${stage.key}:${stageOutcomes.map((outcome) => outcome.key).join(",")}`);
76
82
  }
@@ -0,0 +1,83 @@
1
+ import { type JsonPointerExactBudget, type JsonPointerExactLocator, type JsonPointerExactObservation } from "./long-task-json-pointer-observation.js";
2
+ import type { WorkspaceManifestV2 } from "./long-task-workspace-runtime-types.js";
3
+ export type StaticObservationFreezeErrorCode = "static_observation_not_in_pre_run_snapshot" | "static_observation_changed_by_runner" | "static_observation_manifest_invalid" | "static_observation_path_escape" | "static_observation_root_invalid" | "static_observation_parent_not_directory" | "static_observation_symlink_not_allowed" | "static_observation_hardlink_not_allowed" | "static_observation_not_regular_file" | "static_observation_size_limit";
4
+ export declare class StaticObservationFreezeError extends Error {
5
+ readonly code: StaticObservationFreezeErrorCode;
6
+ readonly artifact_path: string;
7
+ readonly cause_code: string | null;
8
+ constructor(code: StaticObservationFreezeErrorCode, artifactPath: string, causeCode?: string | null);
9
+ }
10
+ export interface StaticObservationManifestMembership {
11
+ path: string;
12
+ mode: number;
13
+ declared_size: number;
14
+ /** Git/blob identity from the workspace manifest; never a content digest. */
15
+ manifest_object_identity: string;
16
+ }
17
+ export interface StaticObservationFileIdentity {
18
+ device: string;
19
+ inode: string;
20
+ mode: number;
21
+ links: string;
22
+ size: number;
23
+ modified_time_ns: string;
24
+ changed_time_ns: string;
25
+ birth_time_ns: string;
26
+ content_sha256: string;
27
+ }
28
+ export interface VerifiedObservationInputFile {
29
+ readonly artifact_path: string;
30
+ readonly manifest_membership: StaticObservationManifestMembership;
31
+ readonly pre_run_identity: StaticObservationFileIdentity;
32
+ readonly post_run_identity: StaticObservationFileIdentity;
33
+ }
34
+ export interface VerifiedStaticObservationCarrier extends VerifiedObservationInputFile {
35
+ copyFrozenBytes(): Uint8Array;
36
+ extractJsonPointerExactObservation(input: {
37
+ locator: JsonPointerExactLocator;
38
+ sensitivity: string;
39
+ budget?: JsonPointerExactBudget;
40
+ }): JsonPointerExactObservation;
41
+ extractJsonPointerExactValue(input: {
42
+ locator: JsonPointerExactLocator;
43
+ sensitivity: string;
44
+ budget?: JsonPointerExactBudget;
45
+ }): {
46
+ raw_value: unknown;
47
+ observation: JsonPointerExactObservation;
48
+ };
49
+ }
50
+ export interface FrozenObservationInputFile {
51
+ readonly artifact_path: string;
52
+ readonly manifest_membership: StaticObservationManifestMembership;
53
+ readonly pre_run_identity: StaticObservationFileIdentity;
54
+ verifyPostRun(): Promise<VerifiedObservationInputFile>;
55
+ dispose(): void;
56
+ }
57
+ export interface FrozenStaticObservationCarrier extends FrozenObservationInputFile {
58
+ verifyPostRun(): Promise<VerifiedStaticObservationCarrier>;
59
+ }
60
+ export interface ObservationInputFreezeBudget {
61
+ readonly seen_artifact_paths: Set<string>;
62
+ total_artifact_bytes: number;
63
+ readonly max_artifacts: number;
64
+ readonly max_total_artifact_bytes: number;
65
+ }
66
+ export declare function createObservationInputFreezeBudget(input: {
67
+ max_artifacts: number;
68
+ max_total_artifact_bytes: number;
69
+ }): ObservationInputFreezeBudget;
70
+ export declare function freezeStaticObservationCarrier(input: {
71
+ snapshot_root: string;
72
+ workspace_manifest: WorkspaceManifestV2;
73
+ artifact_path: string;
74
+ budget?: JsonPointerExactBudget;
75
+ input_freeze_budget?: ObservationInputFreezeBudget;
76
+ }): Promise<FrozenStaticObservationCarrier>;
77
+ export declare function freezeObservationInputFile(input: {
78
+ snapshot_root: string;
79
+ workspace_manifest: WorkspaceManifestV2;
80
+ artifact_path: string;
81
+ max_file_bytes: number;
82
+ budget?: ObservationInputFreezeBudget;
83
+ }): Promise<FrozenObservationInputFile>;
@@ -0,0 +1,428 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants, watch } from "node:fs";
3
+ import { lstat, open, realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { JSON_POINTER_EXACT_LIMITS, assertJsonTree, decodeUtf8Json, extractJsonPointerExactObservationFromBytes, normalizeObservationArtifactPath, parseJsonWithoutDuplicateKeys, resolveJsonPointer, } from "./long-task-json-pointer-observation.js";
6
+ export class StaticObservationFreezeError extends Error {
7
+ code;
8
+ artifact_path;
9
+ cause_code;
10
+ constructor(code, artifactPath, causeCode = null) {
11
+ super([code, artifactPath, causeCode]
12
+ .filter((value) => value !== null && value !== "")
13
+ .join(":"));
14
+ this.name = "StaticObservationFreezeError";
15
+ this.code = code;
16
+ this.artifact_path = artifactPath;
17
+ this.cause_code = causeCode;
18
+ }
19
+ }
20
+ export function createObservationInputFreezeBudget(input) {
21
+ if (!Number.isSafeInteger(input.max_artifacts) ||
22
+ input.max_artifacts <= 0 ||
23
+ !Number.isSafeInteger(input.max_total_artifact_bytes) ||
24
+ input.max_total_artifact_bytes <= 0)
25
+ throw new Error("observation_input_freeze_budget_invalid");
26
+ return {
27
+ seen_artifact_paths: new Set(),
28
+ total_artifact_bytes: 0,
29
+ max_artifacts: input.max_artifacts,
30
+ max_total_artifact_bytes: input.max_total_artifact_bytes,
31
+ };
32
+ }
33
+ export async function freezeStaticObservationCarrier(input) {
34
+ const artifactPath = normalizeStaticArtifactPath(input.artifact_path);
35
+ assertArtifactCount(input.budget, artifactPath);
36
+ const prepared = await prepareFrozenObservationInput({
37
+ snapshot_root: input.snapshot_root,
38
+ workspace_manifest: input.workspace_manifest,
39
+ artifact_path: artifactPath,
40
+ max_file_bytes: JSON_POINTER_EXACT_LIMITS.max_file_bytes,
41
+ budget: input.input_freeze_budget,
42
+ retain_bytes: true,
43
+ });
44
+ accountForArtifact(input.budget, artifactPath, prepared.preRunIdentity.size);
45
+ const frozenBytes = prepared.frozenBytes;
46
+ if (!frozenBytes)
47
+ throw new Error("static_observation_frozen_bytes_missing");
48
+ return Object.freeze({
49
+ artifact_path: artifactPath,
50
+ manifest_membership: prepared.manifest,
51
+ pre_run_identity: prepared.preRunIdentity,
52
+ verifyPostRun: async () => {
53
+ try {
54
+ const postRunIdentity = await prepared.verifyPostRunIdentity();
55
+ return Object.freeze({
56
+ artifact_path: artifactPath,
57
+ manifest_membership: prepared.manifest,
58
+ pre_run_identity: prepared.preRunIdentity,
59
+ post_run_identity: postRunIdentity,
60
+ copyFrozenBytes: () => Uint8Array.from(frozenBytes),
61
+ extractJsonPointerExactObservation: (input) => extractJsonPointerExactObservationFromBytes({
62
+ artifact_path: artifactPath,
63
+ bytes: frozenBytes,
64
+ locator: input.locator,
65
+ sensitivity: input.sensitivity,
66
+ budget: input.budget,
67
+ }),
68
+ extractJsonPointerExactValue: (input) => {
69
+ const observation = extractJsonPointerExactObservationFromBytes({
70
+ artifact_path: artifactPath,
71
+ bytes: frozenBytes,
72
+ locator: input.locator,
73
+ sensitivity: input.sensitivity,
74
+ budget: input.budget,
75
+ });
76
+ const parsed = parseJsonWithoutDuplicateKeys(decodeUtf8Json(frozenBytes));
77
+ assertJsonTree(parsed, 0);
78
+ return {
79
+ raw_value: resolveJsonPointer(parsed, observation.locator.value),
80
+ observation,
81
+ };
82
+ },
83
+ });
84
+ }
85
+ finally {
86
+ prepared.dispose();
87
+ }
88
+ },
89
+ dispose: prepared.dispose,
90
+ });
91
+ }
92
+ export async function freezeObservationInputFile(input) {
93
+ const prepared = await prepareFrozenObservationInput({
94
+ ...input,
95
+ retain_bytes: false,
96
+ });
97
+ return Object.freeze({
98
+ artifact_path: prepared.artifactPath,
99
+ manifest_membership: prepared.manifest,
100
+ pre_run_identity: prepared.preRunIdentity,
101
+ verifyPostRun: async () => {
102
+ try {
103
+ const postRunIdentity = await prepared.verifyPostRunIdentity();
104
+ return Object.freeze({
105
+ artifact_path: prepared.artifactPath,
106
+ manifest_membership: prepared.manifest,
107
+ pre_run_identity: prepared.preRunIdentity,
108
+ post_run_identity: postRunIdentity,
109
+ });
110
+ }
111
+ finally {
112
+ prepared.dispose();
113
+ }
114
+ },
115
+ dispose: prepared.dispose,
116
+ });
117
+ }
118
+ async function prepareFrozenObservationInput(input) {
119
+ const artifactPath = normalizeStaticArtifactPath(input.artifact_path);
120
+ const maxFileBytes = observationInputFileLimit(input.max_file_bytes);
121
+ const manifestEntry = manifestMembership(input.workspace_manifest, artifactPath);
122
+ assertObservationInputCount(input.budget, artifactPath);
123
+ const root = path.resolve(input.snapshot_root);
124
+ const captured = await captureStaticObservationFile(root, artifactPath, maxFileBytes, input.retain_bytes);
125
+ accountForObservationInput(input.budget, artifactPath, captured.identity.size);
126
+ const manifest = freezeManifestMembership(manifestEntry);
127
+ const preRunIdentity = freezeFileIdentity(captured.identity);
128
+ const frozenBytes = captured.bytes ? Buffer.from(captured.bytes) : null;
129
+ const mutationMonitor = monitorObservationInput(root, artifactPath);
130
+ return {
131
+ artifactPath,
132
+ manifest,
133
+ preRunIdentity,
134
+ frozenBytes,
135
+ verifyPostRunIdentity: async () => {
136
+ let post;
137
+ try {
138
+ await mutationMonitor.assertUnchanged();
139
+ post = await captureStaticObservationFile(root, artifactPath, maxFileBytes, false, preRunIdentity);
140
+ if (post.identity.content_sha256 !== preRunIdentity.content_sha256)
141
+ throw freezeError("static_observation_changed_by_runner", artifactPath, "content_sha256");
142
+ await mutationMonitor.assertUnchanged();
143
+ }
144
+ catch (error) {
145
+ if (error instanceof StaticObservationFreezeError &&
146
+ error.code === "static_observation_changed_by_runner")
147
+ throw error;
148
+ throw freezeError("static_observation_changed_by_runner", artifactPath, freezeCause(error));
149
+ }
150
+ return freezeFileIdentity(post.identity);
151
+ },
152
+ dispose: mutationMonitor.dispose,
153
+ };
154
+ }
155
+ function monitorObservationInput(root, artifactPath) {
156
+ const absolute = path.resolve(root, ...artifactPath.split("/"));
157
+ let changedCause = null;
158
+ let disposed = false;
159
+ let watcher;
160
+ try {
161
+ watcher = watch(absolute, { persistent: false }, (eventType) => {
162
+ changedCause ??= `filesystem_event.${eventType}`;
163
+ });
164
+ }
165
+ catch (error) {
166
+ throw freezeError("static_observation_changed_by_runner", artifactPath, `monitor_unavailable.${nodeErrorCode(error)}`);
167
+ }
168
+ watcher.on("error", (error) => {
169
+ changedCause ??= `filesystem_monitor_error.${nodeErrorCode(error)}`;
170
+ });
171
+ return {
172
+ assertUnchanged: async () => {
173
+ await settleFilesystemEvents();
174
+ if (changedCause)
175
+ throw freezeError("static_observation_changed_by_runner", artifactPath, changedCause);
176
+ },
177
+ dispose: () => {
178
+ if (disposed)
179
+ return;
180
+ disposed = true;
181
+ watcher.close();
182
+ },
183
+ };
184
+ }
185
+ async function settleFilesystemEvents() {
186
+ await new Promise((resolve) => setImmediate(resolve));
187
+ await new Promise((resolve) => setImmediate(resolve));
188
+ }
189
+ const STATIC_OBSERVATION_METADATA_KEYS = Object.freeze([
190
+ "device",
191
+ "inode",
192
+ "mode",
193
+ "links",
194
+ "size",
195
+ "modified_time_ns",
196
+ "changed_time_ns",
197
+ "birth_time_ns",
198
+ ]);
199
+ async function captureStaticObservationFile(root, artifactPath, maxFileBytes, retainBytes, expected) {
200
+ const rootInfo = await lstat(root, { bigint: true }).catch((error) => {
201
+ throw freezeError("static_observation_root_invalid", artifactPath, nodeErrorCode(error));
202
+ });
203
+ if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())
204
+ throw freezeError("static_observation_root_invalid", artifactPath);
205
+ const canonicalRoot = await realpath(root);
206
+ const absolute = path.resolve(root, ...artifactPath.split("/"));
207
+ assertContained(root, absolute, artifactPath);
208
+ const pathInfo = await inspectNoFollowPath(root, artifactPath, expected);
209
+ const canonicalFile = await realpath(absolute).catch((error) => {
210
+ throw freezeError(expected
211
+ ? "static_observation_changed_by_runner"
212
+ : "static_observation_not_in_pre_run_snapshot", artifactPath, nodeErrorCode(error));
213
+ });
214
+ assertContained(canonicalRoot, canonicalFile, artifactPath);
215
+ const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
216
+ const handle = await open(absolute, constants.O_RDONLY | noFollow).catch((error) => {
217
+ throw freezeError(expected
218
+ ? "static_observation_changed_by_runner"
219
+ : openFailureCode(error), artifactPath, nodeErrorCode(error));
220
+ });
221
+ try {
222
+ const opened = await handle.stat({ bigint: true });
223
+ assertRegularFile(opened, artifactPath);
224
+ const openedMetadata = fileMetadata(opened);
225
+ assertMetadataEqual(pathInfo, openedMetadata, artifactPath, expected);
226
+ if (expected)
227
+ assertMetadataEqual(expected, openedMetadata, artifactPath, expected);
228
+ if (openedMetadata.size > maxFileBytes)
229
+ throw freezeError("static_observation_size_limit", artifactPath);
230
+ const content = await readAndHashBounded(handle, artifactPath, maxFileBytes, retainBytes);
231
+ const afterRead = fileMetadata(await handle.stat({ bigint: true }));
232
+ assertMetadataEqual(openedMetadata, afterRead, artifactPath, expected);
233
+ const pathAfter = fileMetadata(await lstat(absolute, { bigint: true }).catch((error) => {
234
+ throw freezeError(expected
235
+ ? "static_observation_changed_by_runner"
236
+ : "static_observation_not_in_pre_run_snapshot", artifactPath, nodeErrorCode(error));
237
+ }));
238
+ assertMetadataEqual(afterRead, pathAfter, artifactPath, expected);
239
+ const canonicalAfter = await realpath(absolute);
240
+ assertContained(canonicalRoot, canonicalAfter, artifactPath);
241
+ if (canonicalAfter !== canonicalFile)
242
+ throw freezeError(expected
243
+ ? "static_observation_changed_by_runner"
244
+ : "static_observation_symlink_not_allowed", artifactPath, "realpath_changed");
245
+ return {
246
+ identity: {
247
+ ...openedMetadata,
248
+ content_sha256: content.sha256,
249
+ },
250
+ bytes: content.bytes,
251
+ };
252
+ }
253
+ finally {
254
+ await handle.close();
255
+ }
256
+ }
257
+ async function inspectNoFollowPath(root, artifactPath, expected) {
258
+ let current = root;
259
+ const segments = artifactPath.split("/");
260
+ for (const [index, segment] of segments.entries()) {
261
+ current = path.join(current, segment);
262
+ const info = await lstat(current, { bigint: true }).catch((error) => {
263
+ throw freezeError(expected
264
+ ? "static_observation_changed_by_runner"
265
+ : "static_observation_not_in_pre_run_snapshot", artifactPath, nodeErrorCode(error));
266
+ });
267
+ if (info.isSymbolicLink())
268
+ throw freezeError(expected
269
+ ? "static_observation_changed_by_runner"
270
+ : "static_observation_symlink_not_allowed", artifactPath, "symbolic_link");
271
+ if (index < segments.length - 1) {
272
+ if (!info.isDirectory())
273
+ throw freezeError(expected
274
+ ? "static_observation_changed_by_runner"
275
+ : "static_observation_parent_not_directory", artifactPath);
276
+ continue;
277
+ }
278
+ assertRegularFile(info, artifactPath);
279
+ return fileMetadata(info);
280
+ }
281
+ throw freezeError("static_observation_path_escape", artifactPath);
282
+ }
283
+ function manifestMembership(manifest, artifactPath) {
284
+ const matches = manifest.files.filter((entry) => entry.path === artifactPath);
285
+ if (!matches.length)
286
+ throw freezeError("static_observation_not_in_pre_run_snapshot", artifactPath);
287
+ if (matches.length !== 1)
288
+ throw freezeError("static_observation_manifest_invalid", artifactPath);
289
+ const entry = matches[0];
290
+ if (!isManifestRegularFile(entry))
291
+ throw freezeError("static_observation_not_regular_file", artifactPath);
292
+ return {
293
+ path: entry.path,
294
+ mode: entry.mode,
295
+ declared_size: entry.size,
296
+ manifest_object_identity: entry.sha256,
297
+ };
298
+ }
299
+ function isManifestRegularFile(entry) {
300
+ return (entry.mode & 0o170000) === 0o100000;
301
+ }
302
+ function normalizeStaticArtifactPath(value) {
303
+ try {
304
+ return normalizeObservationArtifactPath(value);
305
+ }
306
+ catch {
307
+ throw freezeError("static_observation_path_escape", value);
308
+ }
309
+ }
310
+ function assertRegularFile(info, artifactPath) {
311
+ if (info.isSymbolicLink())
312
+ throw freezeError("static_observation_symlink_not_allowed", artifactPath);
313
+ if (!info.isFile())
314
+ throw freezeError("static_observation_not_regular_file", artifactPath);
315
+ if (info.nlink > 1n)
316
+ throw freezeError("static_observation_hardlink_not_allowed", artifactPath);
317
+ }
318
+ function fileMetadata(info) {
319
+ return {
320
+ device: info.dev.toString(),
321
+ inode: info.ino.toString(),
322
+ mode: Number(info.mode),
323
+ links: info.nlink.toString(),
324
+ size: Number(info.size),
325
+ modified_time_ns: info.mtimeNs.toString(),
326
+ changed_time_ns: info.ctimeNs.toString(),
327
+ birth_time_ns: info.birthtimeNs.toString(),
328
+ };
329
+ }
330
+ function assertMetadataEqual(left, right, artifactPath, postRunExpected) {
331
+ const mismatch = STATIC_OBSERVATION_METADATA_KEYS.find((key) => left[key] !== right[key]);
332
+ if (!mismatch)
333
+ return;
334
+ throw freezeError(postRunExpected
335
+ ? "static_observation_changed_by_runner"
336
+ : "static_observation_not_in_pre_run_snapshot", artifactPath, `file_identity.${mismatch}`);
337
+ }
338
+ async function readAndHashBounded(handle, artifactPath, limit, retainBytes) {
339
+ const buffer = Buffer.allocUnsafe(Math.min(65_536, limit + 1));
340
+ const chunks = [];
341
+ const hash = createHash("sha256");
342
+ let offset = 0;
343
+ while (true) {
344
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, offset);
345
+ if (!bytesRead)
346
+ break;
347
+ offset += bytesRead;
348
+ if (offset > limit)
349
+ throw freezeError("static_observation_size_limit", artifactPath);
350
+ const chunk = buffer.subarray(0, bytesRead);
351
+ hash.update(chunk);
352
+ if (retainBytes)
353
+ chunks.push(Buffer.from(chunk));
354
+ }
355
+ return {
356
+ bytes: retainBytes ? Uint8Array.from(Buffer.concat(chunks, offset)) : null,
357
+ sha256: hash.digest("hex"),
358
+ };
359
+ }
360
+ function observationInputFileLimit(value) {
361
+ const limit = value;
362
+ if (!Number.isSafeInteger(limit) || limit <= 0)
363
+ throw new Error("observation_input_freeze_file_limit_invalid");
364
+ return limit;
365
+ }
366
+ function assertContained(root, target, artifactPath) {
367
+ const relative = path.relative(root, target);
368
+ if (relative === "" ||
369
+ relative === ".." ||
370
+ relative.startsWith(`..${path.sep}`) ||
371
+ path.isAbsolute(relative))
372
+ throw freezeError("static_observation_path_escape", artifactPath);
373
+ }
374
+ function assertArtifactCount(budget, artifactPath) {
375
+ if (budget &&
376
+ !budget.seen_artifact_paths.has(artifactPath) &&
377
+ budget.seen_artifact_paths.size >=
378
+ JSON_POINTER_EXACT_LIMITS.max_artifacts_per_check)
379
+ throw new Error("observation_artifact_count_limit");
380
+ }
381
+ function assertObservationInputCount(budget, artifactPath) {
382
+ if (budget &&
383
+ !budget.seen_artifact_paths.has(artifactPath) &&
384
+ budget.seen_artifact_paths.size >= budget.max_artifacts)
385
+ throw new Error("observation_input_freeze_artifact_count_limit");
386
+ }
387
+ function accountForObservationInput(budget, artifactPath, bytes) {
388
+ if (!budget || budget.seen_artifact_paths.has(artifactPath))
389
+ return;
390
+ if (budget.total_artifact_bytes + bytes > budget.max_total_artifact_bytes)
391
+ throw new Error("observation_input_freeze_total_size_limit");
392
+ budget.seen_artifact_paths.add(artifactPath);
393
+ budget.total_artifact_bytes += bytes;
394
+ }
395
+ function accountForArtifact(budget, artifactPath, bytes) {
396
+ if (!budget || budget.seen_artifact_paths.has(artifactPath))
397
+ return;
398
+ if (budget.total_artifact_bytes + bytes >
399
+ JSON_POINTER_EXACT_LIMITS.max_total_artifact_bytes)
400
+ throw new Error("observation_artifact_total_size_limit");
401
+ budget.seen_artifact_paths.add(artifactPath);
402
+ budget.total_artifact_bytes += bytes;
403
+ }
404
+ function freezeManifestMembership(value) {
405
+ return Object.freeze({ ...value });
406
+ }
407
+ function freezeFileIdentity(value) {
408
+ return Object.freeze({ ...value });
409
+ }
410
+ function freezeError(code, artifactPath, causeCode = null) {
411
+ return new StaticObservationFreezeError(code, artifactPath, causeCode);
412
+ }
413
+ function openFailureCode(error) {
414
+ return nodeErrorCode(error) === "ENOENT"
415
+ ? "static_observation_not_in_pre_run_snapshot"
416
+ : nodeErrorCode(error) === "ELOOP"
417
+ ? "static_observation_symlink_not_allowed"
418
+ : "static_observation_not_regular_file";
419
+ }
420
+ function freezeCause(error) {
421
+ if (error instanceof StaticObservationFreezeError)
422
+ return error.cause_code ?? error.code;
423
+ return nodeErrorCode(error);
424
+ }
425
+ function nodeErrorCode(error) {
426
+ return (error?.code ??
427
+ (error instanceof Error ? error.message : String(error)));
428
+ }