Compare commits

..

9 Commits

20 changed files with 908 additions and 218 deletions
+163
View File
@@ -0,0 +1,163 @@
---
name: tdfddd-seam-review
description: "Evaluates how well a bounded context or seam follows TDFDDD bounded-seam review principles. Use when reviewing a bounded context, workflow seam, policy seam, or public API for intent-first naming, trust boundaries, reviewability, and seam quality."
---
# TDFDDD Seam Review
## Description
Evaluates whether a bounded context exposes strong bounded seams that are easy to review with high confidence.
Use this skill to review an existing bounded context, workflow seam, policy seam, or public API and produce a report about seam quality without changing behavior.
## When to Use This Skill
Activate when the user:
- asks to review a bounded context for clarity or seam quality
- wants to know why a context is hard to review
- wants an evaluation of workflow seams, policy seams, or public APIs
- suspects naming drift, leakage, or tangled responsibilities
- wants guidance before a bounded-seam refactor
## Core Function: The Seam Review Protocol
**Goal:** explain whether the code presents a reviewable bounded seam, why it is or is not working, and what the smallest high-value improvements would be.
**Required input:**
- target bounded context, files, or seam
- optional concern such as naming, trust boundaries, public API sprawl, or review load
If scope is unclear, infer a provisional seam under review and state it explicitly.
## Review Headspace
Treat names as review targets, not as automatically correct just because the initial model exists.
A seam is strong when a reviewer can understand caller intent, authority, invariants, and outcomes without reconstructing internals.
## Instructions
1. Read the relevant code before making any claim.
2. Identify the top bounded seam first.
- usually the context public API or the main workflow entrypoint
- state what command goes in and what event or failure comes out
3. Identify the inner review seams.
- pure policies
- state transitions or model operations
- trust boundaries where tainted input becomes trusted
4. Review the seam using the general bounded-seam questions:
- What caller intent does this seam grant?
- What invariant becomes true after crossing it?
- What tainted input becomes trusted here?
- What can still fail, and how is failure represented?
- What decisions are pure policy versus orchestration versus I/O?
- What internal details are leaking through the public API?
- If this seam were renamed by user goal instead of implementation shape, what would it be called?
5. Apply seam-type-specific questions.
### Workflow seam questions
- Given command X, under what rules does this workflow emit event Y or failure Z?
- Is the workflow mostly assembling decisions and capabilities, or is it hiding business rules inside orchestration?
- Does the workflow expose the business outcome more clearly than the implementation steps?
### Policy seam questions
- What exact business or security decision is being made?
- What inputs are sufficient for that decision?
- Are the reasons for approval or rejection explicit enough to review?
- Is the policy pure and deterministic?
### Public API questions
- Does the public surface expose one intent-first entrypoint or force the reviewer to reconstruct the context from many exports?
- Are internals importing back through the public barrel, making the seam circular or blurry?
- Does the API hide mechanics, or does it leak low-level helpers, transitional state names, or assembly details?
## What to Look For
### Strong seam signals
- intent-first workflow or context entrypoint
- explicit event and failure shapes
- pure policy decisions with explicit reasons
- visible trust boundaries
- narrow public API with clear authority
- internal modules importing direct local dependencies instead of re-entering `index.ts`
### Weak seam signals
- state or type names that describe pipeline progress rather than reviewer intent
- barrels that re-export nearly everything
- internal files importing from the public API of their own context
- mixed concerns inside one module such as parsing, policy, model math, and artifact derivation
- service or workflow code hiding business rules in control flow
- reviewer needing broad context just to answer one contract question
## Output Format
Return a compact report with these sections:
```markdown
## Seam Under Review
- Scope: ...
- Top bounded seam: ...
- Inner review seams: ...
## What Makes This Easy or Hard to Review
- Strengths: ...
- Friction points: ...
- Trust-boundary visibility: ...
- Naming quality: ...
## Seam Evaluation
- Caller intent: ...
- Protected invariant: ...
- Tainted to trusted transition: ...
- Failure contract: ...
- Public API leakage: ...
- Policy / workflow / IO separation: ...
## Judgment
- Seam quality: strong | mixed | weak
- Main issue: ...
- Why review load is high or low: ...
## Smallest High-Value Improvements
- 1. ...
- 2. ...
- 3. ...
```
## Naming Guidance During Review
If naming is part of the problem:
1. state why the current name is weak
2. propose three alternatives when the name is important
3. choose the option that best preserves caller intent across likely model evolution
4. prefer intent-first names such as accepted, confirmed, authorized, selected, or emitted outcomes over vague ready/process labels when those better describe the protected invariant
## Constraints
- Do not write implementation code unless the user explicitly asks for a refactor.
- Do not demand extra layers if the current seam can be clarified with naming or API reduction.
- Do not treat every state-machine name as wrong; flag it only when it increases review load or hides the actual invariant.
- Do not weaken security or trust-boundary checks in the name of simplicity.
## Success Criteria
A good seam review should let a human reviewer answer:
- what the main seam is
- what it promises
- why it is easy or hard to review
- where trust enters and becomes trusted
- which names or exports are increasing review load
- what the smallest justified improvements are
+2 -2
View File
@@ -16,7 +16,7 @@
| Feature Step | Bounded Context | Workflow Slice | Notes |
| :----------- | :-------------- | :------------- | :---- |
| Ingest upstream bundle snapshot into deterministic recovery artifacts | `ingest-snapshot` | `deterministic-bundle-ingest` | Produces the canonical per-run source of truth used by all later slices. |
| Identify vendored package boundaries and confidence decisions | `dependency-recovery` | `identify-vendored-packages` | Consumes ingest artifacts and records accepted, rejected, and unresolved dependency decisions. |
| Identify the next vendored package decision from one source | `dependency-recovery` | `identify-next-vendored-package-decision-from-source` | Consumes ingest artifacts, emits one dependency decision at a time, and signals when no more plausible candidates remain. |
| Replace accepted vendored packages with external dependencies while keeping fallbacks | `dependency-recovery` | `externalize-accepted-dependencies` | Depends on identified package decisions; unresolved packages stay bundled. |
| Extract deterministic context packets for each segment | `static-context-evidence` | `extract-segment-context` | Consumes ingest output after dependency treatment to emit machine-readable evidence. |
| Compare adjacent runs and classify lineage-aware changes | `snapshot-lineage` | `diff-adjacent-runs` | Consumes current and previous run manifests plus Phase 3 context. |
@@ -41,7 +41,7 @@
## Recommended Slice Order
1. `ingest-snapshot/deterministic-bundle-ingest` — all later slices depend on deterministic ingest artifacts and canonical segment boundaries.
2. `dependency-recovery/identify-vendored-packages` — shrinks the app-authored surface before later evidence and naming work.
2. `dependency-recovery/identify-next-vendored-package-decision-from-source` — shrinks the app-authored surface one source decision at a time before later evidence and naming work.
3. `dependency-recovery/externalize-accepted-dependencies` — completes dependency treatment before downstream evidence extraction.
4. `static-context-evidence/extract-segment-context` — provides deterministic evidence used by diffing, summaries, and transform anchoring.
5. `snapshot-lineage/diff-adjacent-runs` — identifies changed/new material and durable lineage needed for iterative naming.
@@ -72,7 +72,7 @@
## 6. Candidate Workflow Slices
- ingest-snapshot/deterministic-bundle-ingest: turn an upstream bundle into deterministic segment records and canonical source projection.
- dependency-recovery/identify-vendored-packages: score dependency candidates and recover package boundaries.
- dependency-recovery/identify-next-vendored-package-decision-from-source: recover the next plausible vendored candidate from one source and record one dependency decision or exhaustion.
- dependency-recovery/externalize-accepted-dependencies: replace accepted vendored code with npm imports while preserving fallbacks.
- static-context-evidence/extract-segment-context: emit canonical context packets and binding/link evidence.
- snapshot-lineage/diff-adjacent-runs: classify changes, mint lineage, and produce relabel queues plus upstream summaries.
+1 -1
View File
@@ -41,7 +41,7 @@
| Bounded Context | Workflow Slice | Slice Discovery | Core Sketch | Blueprint | Design Security | Assembly | Impl Security | Refactor | Notes |
| :-------------- | :------------- | :-------------- | :---------- | :-------- | :-------------- | :------- | :------------ | :------- | :---- |
| `ingest-snapshot` | `deterministic-bundle-ingest` | `Complete` | `Complete` | `Complete` | `Complete` | `Complete` | `Not Started` | `Not Started` | `Foundational source-of-truth slice.` |
| `dependency-recovery` | `identify-vendored-packages` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Shrinks app-authored surface before later phases.` |
| `dependency-recovery` | `identify-next-vendored-package-decision-from-source` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Shrinks app-authored surface one decision at a time.` |
| `dependency-recovery` | `externalize-accepted-dependencies` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Depends on package identification decisions.` |
| `static-context-evidence` | `extract-segment-context` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Produces deterministic evidence for downstream consumers.` |
| `snapshot-lineage` | `diff-adjacent-runs` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Owns lineage and changed/new segment routing.` |
@@ -0,0 +1,38 @@
# Dependency Recovery Context
**Vendored Package**: third-party code embedded inside the upstream bundle and considered for recovery as an external dependency.
_Avoid_: library blob, bundled package blob
**Dependency Decision**: the context-owned determination that a vendored candidate is accepted, rejected, or unresolved with recorded evidence and rationale.
_Avoid_: match result, package guess
**Acceptance Threshold**: the configurable confidence score boundary at or above which a vendored candidate is accepted automatically.
_Avoid_: hard-coded cutoff, fixed confidence bar
**Rejected Candidate**: a vendored candidate whose evidence deterministically supports that it is not a package match worth externalizing.
_Avoid_: low-confidence maybe, unresolved miss
**Unresolved Candidate**: a vendored candidate that remains plausible but is below the acceptance threshold, colliding, ambiguous, or otherwise unsafe to accept.
_Avoid_: rejected maybe, ignored candidate
**Fallback Preservation**: keeping bundled code available when externalization is unsafe or unresolved so later validation can compare behaviors safely.
_Avoid_: leave old code around, dead backup code
**Externalization**: replacing accepted vendored code with an external dependency reference without deleting the original bundled implementation.
_Avoid_: strip dependency, remove vendor code
## Example dialogue
> **Developer:** "If a candidate scores below the configured threshold, do we reject it?"
> **Domain Expert:** "No. If it still looks plausible, it stays unresolved until stronger evidence appears or a reviewer decides otherwise."
>
> **Developer:** "When do we mark a candidate rejected?"
> **Domain Expert:** "Only when the evidence deterministically says it is not a vendored package match worth externalizing."
>
> **Developer:** "Does accepting a vendored package delete the bundled code?"
> **Domain Expert:** "No. Externalization still preserves the bundled implementation as fallback evidence."
## Flagged ambiguities
- "Low confidence" alone does not mean `rejected`; low-confidence but still plausible candidates are `Unresolved Candidates`.
- "Match result" was too scoring-shaped; the preferred term is `Dependency Decision` because this context owns a reviewer-facing decision with rationale.
@@ -1,9 +1,9 @@
# Workflow Decomposition: Identify Vendored Packages
# Workflow Decomposition: Identify Next Vendored Package Decision From Source
- Bounded context: `dependency-recovery`
- Workflow slug: `identify-vendored-packages`
- Trigger: `Identify vendored packages from ingest artifacts`
- Success outcome: Vendored candidates receive accepted, rejected, or unresolved dependency decisions with evidence and rationale.
- Workflow slug: `identify-next-vendored-package-decision-from-source`
- Trigger: `Identify next vendored package decision from source`
- Success outcome: The next plausible vendored candidate from one source yields either a single accepted, rejected, or unresolved dependency decision, or an explicit no-more-candidates outcome.
## Inputs Owned by This Context
@@ -0,0 +1,59 @@
# Slice Discovery: Identify Next Vendored Package Decision From Source
- Bounded context: `dependency-recovery`
- Workflow slug: `identify-next-vendored-package-decision-from-source`
## Happy Path
- The workflow receives deterministic ingest artifacts from `ingest-snapshot`, including the run manifest, segment records, and canonical source projection.
- The workflow inspects the source snapshot and deterministically recovers the next plausible vendored candidate boundary that has not already been decided.
- The workflow computes a deterministic confidence ranking for package matches on that next candidate by combining the configured evidence signals.
- The workflow compares the strongest candidate match against the configurable acceptance threshold.
- If the strongest candidate is at or above the acceptance threshold, the workflow records one `accepted` dependency decision.
- If the strongest candidate remains plausible but is below threshold, colliding, or ambiguous, the workflow records one `unresolved` dependency decision.
- If the candidate evidence deterministically supports that it is not a vendored package match, the workflow records one `rejected` dependency decision.
- The workflow emits one decision record with evidence, rationale, boundary notes, and replacement planning hints.
- When no plausible undecided vendored candidates remain for the source under the current rules, the workflow emits an explicit no-more-candidates outcome.
- Downstream `externalize-accepted-dependencies` consumes the accumulated accepted decisions for externalization while preserving unresolved and rejected records for review and summaries.
## Edge Cases
- A package is split across multiple obfuscated wrappers -> recover one vendored candidate boundary spanning the related segments and record the grouping rationale.
- Multiple package matches compete for the same segment group -> keep the stronger deterministic ranking, but if the competition remains plausible and unsafe to settle automatically, emit `unresolved` rather than forcing rejection.
- A candidate has some evidence but stays below the configured acceptance threshold -> emit `unresolved`.
- A candidate has strong counter-evidence that it is app-authored or otherwise not a vendored package match -> emit `rejected`.
- Runtime traces are unavailable -> continue with static evidence only.
- Runtime traces conflict with static evidence -> record the mixed provenance and prefer `unresolved` unless the conflict is resolved deterministically.
- A candidate package boundary is only partially recoverable -> record the partial boundary notes and keep the decision unresolved unless the recoverable portion still supports a deterministic accepted or rejected decision.
- Two runs use different threshold settings -> preserve the same confidence scores and evidence, but allow the configurable threshold to change which candidates become accepted.
## Business Rules & Invariants
- Rule: Confidence scoring is deterministic for identical ingest artifacts, evidence sources, and configuration.
- Rule: Acceptance uses a configurable threshold rather than a hard-coded cutoff.
- Rule: `Accepted` means the candidate score is at or above the configured acceptance threshold and the match is safe enough to externalize later.
- Rule: `Unresolved` is preferred over `Rejected` when a candidate remains plausible but is below threshold, colliding, ambiguous, or otherwise unsafe to settle automatically.
- Rule: `Rejected` is reserved for candidates whose evidence deterministically supports that they are not vendored package matches worth externalizing.
- Rule: The workflow records auditable evidence and rationale for every dependency decision.
- Rule: One vendored package candidate may map to multiple segments when the boundary recovery rationale supports it.
- Invariant: This slice identifies and records dependency decisions only; it does not externalize code.
- Invariant: Accepted, rejected, and unresolved candidates all remain visible in emitted manifests.
- Invariant: Changing the acceptance threshold must not require redesigning the manifest format.
## Required Decisions Owned by This Context
- Which evidence signals are combined into deterministic vendored-package confidence scoring.
- Which related segments belong to one vendored package candidate boundary.
- Whether a candidate becomes accepted, rejected, or unresolved.
- What evidence, rationale, ambiguity notes, and replacement planning hints must be persisted for downstream use.
## Handoff Assumptions
- `ingest-snapshot` provides deterministic run manifest, segment records, and canonical projection as the source of truth for candidate discovery.
- `externalize-accepted-dependencies` consumes only accepted dependency decisions for replacement work.
- Later review and release-summary seams consume accepted, rejected, and unresolved manifests without reopening this slice's decision logic.
- Optional runtime traces act only as additional evidence inside this context and do not override deterministic decision recording requirements.
## Open Questions
- None currently inside this slice; threshold tuning and evidence-weight configuration remain implementation concerns within this context.
@@ -0,0 +1,105 @@
# Core Sketch: Identify Next Vendored Package Decision From Source
- Bounded context: `dependency-recovery`
- Workflow slug: `identify-next-vendored-package-decision-from-source`
## Command
- `IdentifyNextVendoredPackageDecisionFromSource`
- Meaning: inspect one source snapshot, deterministically recover the next plausible vendored candidate if one remains, and record exactly one `Dependency Decision` or an explicit no-more-candidates outcome for later `Externalization`.
## Required State
State owned by `dependency-recovery` and required to decide this workflow:
- `VendoredCandidateDiscoveryRules`
- allowed evidence signals for vendored-package discovery
- grouping rules for segment and segment-group candidates
- rationale rules for recovered package boundaries
- `ConfidenceScoringRules`
- deterministic scoring weights or combination rules across evidence types
- ranking rules for competing package matches on the same candidate boundary
- tie-break rules when scores or evidence patterns compete
- `AcceptanceThresholdPolicy`
- configurable `Acceptance Threshold`
- rules for when plausible but below-threshold or colliding candidates remain `Unresolved Candidates`
- rules for when counter-evidence is strong enough to produce a `Rejected Candidate`
- `DependencyDecisionRequirements`
- required manifest fields for evidence summary, raw evidence references, confidence score, provenance, recovered boundary notes, ambiguity notes, replacement plan, and fallback reference
- required auditability rules for later review and downstream handoffs
## Observed Inputs
Snapshots or handoffs read but not owned by this context:
- one source snapshot from `ingest-snapshot`, plus the current decision cursor for that source
- the relevant `Run Manifest` facts and canonical source projection from `ingest-snapshot`
- optional runtime traces from that source used only as additional or tie-break evidence
- optional registry, tarball, or CDN package evidence used to compare matches for the next recovered candidate
## Policy Signature (Pseudo)
```text
recoverNextCandidateBoundary :
SourceSnapshot
-> SourceDecisionCursor
-> VendoredCandidateDiscoveryRules
-> Result<NextCandidateBoundary, NoMoreVendoredCandidates>
scoreVendoredCandidate :
NextCandidateBoundary
-> CandidateEvidenceSources
-> ConfidenceScoringRules
-> RankedCandidateMatches
decideDependencyDecision :
NextCandidateBoundary
-> RankedCandidateMatches
-> AcceptanceThresholdPolicy
-> DependencyDecision
validateDecisionRecord :
DependencyDecision
-> DependencyDecisionRequirements
-> Result<DependencyDecisionRecorded, DependencyDecisionRejected>
performNextVendoredPackageDecisionFromSource :
IdentifyNextVendoredPackageDecisionFromSource
-> DependencyRecoveryState
-> Result<NextVendoredPackageDecisionFromSourceCompleted, VendoredPackageIdentificationHardStopped>
```
## Events
### Success Event
- `NextVendoredPackageDecisionFromSourceCompleted`
- run identity reference
- source reference
- next decision cursor
- emitted single dependency decision
- emitted decision record reference
- evidence artifact references for that candidate
### Exhaustion Event
- `NoMoreVendoredCandidatesRemain`
- run identity reference
- source reference
- final decision cursor
### Failure Event
- `VendoredPackageIdentificationHardStopped`
- run identity when available
- failed stage such as missing ingest artifacts, invalid candidate boundary inputs, or invalid decision-manifest requirements
- failure reason
## Boundary Notes
- The `dependency-recovery` context decides the next vendored-package decision for one source per workflow invocation.
- Outer orchestration may repeat the workflow for the same source using the returned decision cursor until the slice emits `NoMoreVendoredCandidatesRemain`.
- This slice does not externalize accepted packages; that belongs to `dependency-recovery/externalize-accepted-dependencies`.
- `ingest-snapshot` remains the source of truth for run manifest, segment boundaries, and canonical projection; this slice must not reopen ingest decisions.
- Optional runtime traces and package-source comparisons act only as evidence inputs here and must not turn this slice into cross-context orchestration.
- Feature-level orchestration decides whether unresolved or review-needed outcomes slow later phases; this slice only records one audit-ready dependency decision at a time.
@@ -0,0 +1,242 @@
module DependencyRecovery.IdentifyNextVendoredPackageDecisionFromSource
open DependencyRecovery.SharedModel
// 1. Primitives
type TaintedRunManifestReference = TaintedRunManifestReference of string
type TaintedSegmentRecordReference = TaintedSegmentRecordReference of string
type TaintedCanonicalProjectionReference = TaintedCanonicalProjectionReference of string
type TaintedRuntimeTraceReference = TaintedRuntimeTraceReference of string
type TrustedRunManifestReference = TrustedRunManifestReference of string
type TrustedSegmentRecordReference = TrustedSegmentRecordReference of string
type TrustedCanonicalProjectionReference = TrustedCanonicalProjectionReference of string
type TrustedRuntimeTraceReference = TrustedRuntimeTraceReference of string
type CandidateGroupingRule =
| GroupAdjacentSegments
| GroupStructurallyLinkedSegments
| GroupSharedLiteralClusters
| GroupSharedExportSurface
type BoundaryRationaleRule =
| RecordAdjacentGroupingRationale
| RecordStructuralLinkRationale
| RecordSharedLiteralRationale
| RecordExportSurfaceRationale
type ScoringRule =
| WeightLicenseBanner
| WeightPreservedPackageName
| WeightSourceMapHint
| WeightPreservedRequireString
| WeightCharacteristicLiteralSet
| WeightHelperSignature
| WeightAstShapeFingerprint
| WeightExportSurfaceSimilarity
| WeightDependencyGraphPosition
| WeightByteSimilarity
| WeightRuntimeExecutionTrace
type RankingRule =
| RankByTotalEvidenceWeight
| RankByEvidenceDiversity
| RankByBoundaryCoverage
| RankByRuntimeSupport
type TieBreakRule =
| PreferMoreSpecificPackageMatch
| PreferBroaderBoundaryCoverage
| PreferStaticEvidenceAgreement
| PreferStablePackageNameOrder
type UnresolvedRule =
| KeepBelowThresholdCandidatesUnresolved
| KeepCollidingCandidatesUnresolved
| KeepAmbiguousCandidatesUnresolved
| KeepConflictingEvidenceCandidatesUnresolved
type RejectedRule =
| RejectDeterministicallyAppAuthoredCandidates
| RejectDeterministicallyNonPackageCandidates
| RejectDeterministicallyContradictedCandidates
type RequiredManifestField =
| CandidatePackageNameField
| DecisionStateField
| ConfidenceScoreField
| EvidenceSummaryField
| RawEvidenceReferencesField
| MatchedSegmentIdsField
| RecoveredBoundaryNotesField
| ReplacementPlanField
| FallbackReferenceField
| EvidenceProvenanceField
| AmbiguityNotesField
type AuditabilityRule =
| RecordDecisionRationale
| RecordThresholdUsed
| RecordScoringInputs
| RecordCompetingMatches
| RecordDecisionTimestampOrder
// 2. Commands (Inputs)
type SourceDecisionCursor = SourceDecisionCursor of string
type TaintedSourceReference = TaintedSourceReference of string
type TrustedSourceReference = TrustedSourceReference of string
type TaintedCandidateBoundaryReference = TaintedCandidateBoundaryReference of string
type TrustedCandidateBoundaryReference = TrustedCandidateBoundaryReference of string
type IdentifyNextVendoredPackageDecisionFromSource = {
runManifest: TaintedRunManifestReference
canonicalProjection: TaintedCanonicalProjectionReference
source: TaintedSourceReference
decisionCursor: SourceDecisionCursor
runtimeTraces: TaintedRuntimeTraceReference option
}
// 3. Observed inputs and owned state
type TrustedSourceInput = {
runIdentity: RunIdentity
runManifest: TrustedRunManifestReference
canonicalProjection: TrustedCanonicalProjectionReference
source: TrustedSourceReference
decisionCursor: SourceDecisionCursor
runtimeTraces: TrustedRuntimeTraceReference option
}
type VendoredCandidateDiscoveryRules = {
allowedSignals: EvidenceSignal list
groupingRules: CandidateGroupingRule list
boundaryRationaleRules: BoundaryRationaleRule list
}
type ConfidenceScoringRules = {
scoringRules: ScoringRule list
rankingRules: RankingRule list
tieBreakRules: TieBreakRule list
}
type AcceptanceThresholdPolicy = {
acceptanceThreshold: ConfidenceScore
unresolvedRules: UnresolvedRule list
rejectedRules: RejectedRule list
}
type DependencyDecisionRequirements = {
requiredManifestFields: RequiredManifestField list
auditabilityRules: AuditabilityRule list
}
type DependencyRecoveryState = {
candidateDiscoveryRules: VendoredCandidateDiscoveryRules
confidenceScoringRules: ConfidenceScoringRules
acceptanceThresholdPolicy: AcceptanceThresholdPolicy
dependencyDecisionRequirements: DependencyDecisionRequirements
}
// 4. Events (Facts)
type NextCandidateBoundary = {
source: TrustedSourceReference
candidateBoundary: TrustedCandidateBoundaryReference
nextCursor: SourceDecisionCursor
}
type DecisionRecordReference = DecisionRecordReference of string
type NextVendoredPackageDecisionFromSourceCompleted = {
runIdentity: RunIdentity
source: TrustedSourceReference
nextCursor: SourceDecisionCursor
dependencyDecision: DependencyDecision
decisionRecord: DecisionRecordReference
evidenceArtifacts: EvidenceReference list
}
type NoMoreVendoredCandidatesRemain = {
runIdentity: RunIdentity
source: TrustedSourceReference
finalCursor: SourceDecisionCursor
}
type VendoredPackageIdentificationStage =
| CandidateInputParsingStage
| CandidateScoringStage
| DependencyDecisionStage
| DecisionRecordValidationStage
type VendoredPackageIdentificationFailureReason =
| MissingIngestArtifacts
| InvalidIngestArtifactReference
| InvalidCandidateBoundaryReference
| InvalidDecisionRecordRequirements
type VendoredPackageIdentificationHardStopped = {
runIdentity: RunIdentity option
failedStage: VendoredPackageIdentificationStage
reason: VendoredPackageIdentificationFailureReason
}
// 5. State (Aggregate)
type DependencyIdentificationState =
| AwaitingVendoredPackageIdentification of DependencyRecoveryState
| NextVendoredPackageDecisionRecorded of NextVendoredPackageDecisionFromSourceCompleted
| VendoredCandidateDiscoveryExhausted of NoMoreVendoredCandidatesRemain
// 6. Parse and decision contracts
val parseSourceInput :
IdentifyNextVendoredPackageDecisionFromSource
-> Result<TrustedSourceInput, VendoredPackageIdentificationHardStopped>
val recoverNextCandidateBoundary :
TrustedSourceInput
-> VendoredCandidateDiscoveryRules
-> Result<NextCandidateBoundary, NoMoreVendoredCandidatesRemain>
val scoreCandidateMatches :
NextCandidateBoundary
-> TrustedSourceInput
-> ConfidenceScoringRules
-> Result<CandidateMatch list, VendoredPackageIdentificationHardStopped>
val decideDependencyDecision :
AcceptanceThresholdPolicy
-> NextCandidateBoundary
-> CandidateMatch list
-> Result<DependencyDecision, VendoredPackageIdentificationHardStopped>
val validateDecisionRecord :
DependencyDecisionRequirements
-> DependencyDecision
-> Result<DecisionRecordReference, VendoredPackageIdentificationHardStopped>
val decide :
DependencyIdentificationState
-> IdentifyNextVendoredPackageDecisionFromSource
-> Result<NextVendoredPackageDecisionFromSourceCompleted, VendoredPackageIdentificationHardStopped>
val apply :
DependencyIdentificationState
-> NextVendoredPackageDecisionFromSourceCompleted
-> DependencyIdentificationState
val workflow :
IdentifyNextVendoredPackageDecisionFromSource
-> Effect.Effect<Result<NextVendoredPackageDecisionFromSourceCompleted, VendoredPackageIdentificationHardStopped>>
@@ -0,0 +1,69 @@
module DependencyRecovery.SharedModel
type RunIdentity = RunIdentity of string
type SegmentId = SegmentId of string
type CandidateBoundaryId = CandidateBoundaryId of string
type PackageName = PackageName of string
type ConfidenceScore = private ConfidenceScore of int
type EvidenceReference = EvidenceReference of string
type BoundaryNote = BoundaryNote of string
type AmbiguityNote = AmbiguityNote of string
type ReplacementPlan = ReplacementPlan of string
type FallbackReference = FallbackReference of string
type Rationale = Rationale of string
type EvidenceProvenance =
| Registry
| Tarball
| Cdn
| Runtime
| Static
| Mixed
type EvidenceSignal =
| LicenseBanner
| PreservedPackageName
| SourceMapHint
| PreservedRequireString
| CharacteristicLiteralSet
| HelperSignature
| AstShapeFingerprint
| ExportSurfaceSimilarity
| DependencyGraphPosition
| ByteSimilarity
| RuntimeExecutionTrace
type CandidateBoundary = {
boundaryId: CandidateBoundaryId
segmentIds: SegmentId list
boundaryNotes: BoundaryNote list
}
type EvidenceSummary = {
signals: EvidenceSignal list
rawEvidence: EvidenceReference list
provenance: EvidenceProvenance
rationale: Rationale
}
type CandidateMatch = {
packageName: PackageName
confidenceScore: ConfidenceScore
evidence: EvidenceSummary
ambiguityNotes: AmbiguityNote list
}
type DependencyDecision =
| AcceptedDecision of CandidateBoundary * CandidateMatch * ReplacementPlan * FallbackReference
| RejectedDecision of CandidateBoundary * CandidateMatch
| UnresolvedDecision of CandidateBoundary * CandidateMatch list
+25
View File
@@ -46,6 +46,31 @@ Use this checklist when reviewing a design produced by a human or an LLM.
- Can service and adapter internals be trusted mostly through seam tests and type constraints rather than line-by-line domain review?
- Do service implementations avoid accumulating hidden business logic?
## General bounded-seam review questions
- What caller intent does this seam grant?
- What invariant becomes true after crossing it?
- What tainted input becomes trusted here?
- What can still fail, and how is failure represented?
- What decisions are pure policy versus orchestration versus I/O?
- What internal details are leaking through the public API?
- If this seam were renamed by user goal instead of implementation shape, what would it be called?
## Applying the questions by seam type
### Workflow seams
- Given command X, under what rules does this workflow emit event Y or failure Z?
- Is the workflow mostly assembling decisions and capabilities, or is it hiding business rules inside orchestration?
- Does the workflow expose the business outcome more clearly than the implementation steps?
### Policy seams
- What exact business or security decision is being made?
- What inputs are sufficient for that decision?
- Are the reasons for approval or rejection explicit enough to review?
- Is the policy pure and deterministic?
## Security review readiness
- Are trust boundaries visible enough for a reviewer to identify where untrusted data enters?
+41 -8
View File
@@ -1,13 +1,46 @@
export * from "./models/shared.js"
export * from "./models/types.js"
export * from "./models/factories.js"
export * from "./models/ops.js"
export * from "./policies/selection.js"
export * from "./policies/segments.js"
export type {
Error,
Event,
IngestUpstreamSnapshot,
State,
UpstreamSnapshotIngested,
SnapshotIngestHardStopped,
AwaitingTrustedBundle,
TrustedSnapshotSelected,
IngestableSnapshot,
} from "./models/types.js"
export type {
RunManifest,
SegmentRecord,
SelectedSnapshot,
SnapshotIdentity,
SnapshotMetadata,
TaintedBundleLocation,
TrustedBundleLocation,
RunIdentity,
} from "./models/shared.js"
export {
makeSnapshotIdentity,
makeTaintedBundleInput,
makeTaintedBundleLocation,
makeVerifiedPreviousRunManifest,
} from "./models/factories.js"
export {
makeAstNodeKind,
makeNormalizedHash,
makeRawHash,
makeRunIdentity,
makeShapeHash,
makeTrustedCanonicalProjectionPath,
makeTrustedManifestPath,
makeTrustedSegmentsPath,
makeTrustedSummaryPath,
} from "./models/ops.js"
export { workflow } from "./workflows/ingestSnapshot.js"
export {
apply,
decide,
makeAwaitingSnapshotSelection,
emitSnapshotIngested,
makeAwaitingTrustedBundle,
validatePreviousRunManifest,
} from "./policies/decideSnapshotIngest.js"
@@ -1,47 +1,19 @@
import type {
DerivedRunIdentity,
Error,
IngestFailureReason,
TaintedBundleInput,
VerifiedPreviousRunManifest,
} from "./types.js"
import type {
AstNodeKind,
NormalizedHash,
RawHash,
RunIdentity,
RunManifest,
ShapeHash,
SnapshotIdentity,
TaintedBundleLocation,
TrustedBundleLocation,
TrustedCanonicalProjectionPath,
TrustedManifestPath,
TrustedSegmentsPath,
TrustedSummaryPath,
} from "./shared.js"
const asBrand = <T>(value: string): T => value as T
export const makeSnapshotIdentity = (value: string): SnapshotIdentity => asBrand(value)
export const makeSnapshotIdentity = (value: string): SnapshotIdentity =>
value as SnapshotIdentity
export const makeTaintedBundleLocation = (value: string): TaintedBundleLocation =>
asBrand(value)
export const makeTrustedBundleLocation = (value: string): TrustedBundleLocation =>
asBrand(value)
export const makeRunIdentity = (value: string): RunIdentity => asBrand(value)
export const makeAstNodeKind = (value: string): AstNodeKind => asBrand(value)
export const makeRawHash = (value: string): RawHash => asBrand(value)
export const makeNormalizedHash = (value: string): NormalizedHash => asBrand(value)
export const makeShapeHash = (value: string): ShapeHash => asBrand(value)
export const makeTrustedManifestPath = (value: string): TrustedManifestPath =>
asBrand(value)
export const makeTrustedSegmentsPath = (value: string): TrustedSegmentsPath =>
asBrand(value)
export const makeTrustedCanonicalProjectionPath = (
value: string,
): TrustedCanonicalProjectionPath => asBrand(value)
export const makeTrustedSummaryPath = (value: string): TrustedSummaryPath =>
asBrand(value)
value as TaintedBundleLocation
export const makeVerifiedPreviousRunManifest = (
manifest: RunManifest,
@@ -51,10 +23,6 @@ export const makeTaintedBundleInput = (
location: TaintedBundleLocation,
): TaintedBundleInput => ({ _tag: "TaintedBundleInput", location })
export const makeDerivedRunIdentity = (
value: RunIdentity,
): DerivedRunIdentity => ({ _tag: "DerivedRunIdentity", value })
export const foldFailure = (
snapshotIdentity: SnapshotIdentity,
reason: IngestFailureReason,
+60 -17
View File
@@ -2,36 +2,48 @@ import { Either } from "effect"
import {
isNonEmptyString,
type AstNodeKind,
type NormalizedHash,
type RawHash,
type RunIdentity,
type SegmentRecord,
type SelectedSnapshot,
type ShapeHash,
type SnapshotIdentity,
type TrustedBundleLocation,
type TrustedCanonicalProjectionPath,
type TrustedManifestPath,
type TrustedSegmentsPath,
type TrustedSummaryPath,
} from "./shared.js"
import {
foldFailure,
makeAstNodeKind,
makeDerivedRunIdentity,
makeNormalizedHash,
makeRawHash,
makeRunIdentity,
makeShapeHash,
makeTrustedBundleLocation,
makeTrustedCanonicalProjectionPath,
makeTrustedManifestPath,
makeTrustedSegmentsPath,
makeTrustedSummaryPath,
} from "./factories.js"
import { foldFailure } from "./factories.js"
import type {
DerivedRunIdentity,
Error,
IngestableSnapshot,
RequiredArtifact,
TaintedBundleInput,
TrustedSnapshotSelected,
} from "./types.js"
export const makeTrustedBundleLocation = (value: string): TrustedBundleLocation =>
value as TrustedBundleLocation
export const makeRunIdentity = (value: string): RunIdentity => value as RunIdentity
export const makeAstNodeKind = (value: string): AstNodeKind => value as AstNodeKind
export const makeRawHash = (value: string): RawHash => value as RawHash
export const makeNormalizedHash = (value: string): NormalizedHash =>
value as NormalizedHash
export const makeShapeHash = (value: string): ShapeHash => value as ShapeHash
export const makeTrustedManifestPath = (value: string): TrustedManifestPath =>
value as TrustedManifestPath
export const makeTrustedSegmentsPath = (value: string): TrustedSegmentsPath =>
value as TrustedSegmentsPath
export const makeTrustedCanonicalProjectionPath = (
value: string,
): TrustedCanonicalProjectionPath => value as TrustedCanonicalProjectionPath
export const makeTrustedSummaryPath = (value: string): TrustedSummaryPath =>
value as TrustedSummaryPath
const parseBundleLocationText = (location: string): string | null => {
const trimmedLocation = location.trim()
return trimmedLocation.length === 0 || !trimmedLocation.includes("/")
@@ -63,19 +75,19 @@ const decideSegmentRecordFailure = (
export const parseBundleLocation = (
snapshotIdentity: SnapshotIdentity,
input: TaintedBundleInput,
): Either.Either<ReturnType<typeof makeTrustedBundleLocation>, Error> => {
): Either.Either<TrustedBundleLocation, Error> => {
const location = parseBundleLocationText(input.location as string)
return location === null
? Either.left(foldFailure(snapshotIdentity, "BundleNotParseable"))
: Either.right(makeTrustedBundleLocation(location))
}
export const applyRunIdentityRules = (
const deriveRunIdentity = (
selectedSnapshot: SelectedSnapshot,
): Either.Either<DerivedRunIdentity, Error> => {
const snapshotIdentity = selectedSnapshot.SnapshotIdentity as string
return isNonEmptyString(snapshotIdentity)
? Either.right(makeDerivedRunIdentity(makeRunIdentity(`run:${snapshotIdentity}`)))
? Either.right({ _tag: "DerivedRunIdentity", value: makeRunIdentity(`run:${snapshotIdentity}`) })
: Either.left(
foldFailure(
selectedSnapshot.SnapshotIdentity,
@@ -136,6 +148,37 @@ export const validateRequiredArtifacts = (
)
}
export const decideIngestableSnapshot = (
trustedSnapshot: TrustedSnapshotSelected,
): Either.Either<IngestableSnapshot, Error> =>
Either.flatMap(deriveRunIdentity(trustedSnapshot.SelectedSnapshot), (derivedRunIdentity) =>
Either.flatMap(validateSegmentRecords(trustedSnapshot.SelectedSnapshot), (segmentRecords) =>
Either.flatMap(
validateBoundaryProofs(
trustedSnapshot.SelectedSnapshot.SnapshotIdentity,
segmentRecords,
),
(boundaryProofs) =>
Either.map(
validateRequiredArtifacts(
trustedSnapshot.SelectedSnapshot.SnapshotIdentity,
trustedSnapshot.RequiredArtifacts,
segmentRecords,
),
() => ({
_tag: "IngestableSnapshot" as const,
RunIdentity: derivedRunIdentity.value,
SelectedSnapshot: trustedSnapshot.SelectedSnapshot,
PreviousRunManifest: trustedSnapshot.PreviousRunManifest,
SegmentRecords: segmentRecords,
BoundaryProofs: boundaryProofs,
RequiredArtifacts: trustedSnapshot.RequiredArtifacts,
}),
),
),
),
)
export const deriveRequiredArtifactPaths = (
runIdentity: RunIdentity,
): {
+9 -9
View File
@@ -68,8 +68,8 @@ export type Error = {
readonly payload: SnapshotIngestHardStopped
}
export type AwaitingSnapshotSelection = {
readonly _tag: "AwaitingSnapshotSelection"
export type AwaitingTrustedBundle = {
readonly _tag: "AwaitingTrustedBundle"
readonly RunIdentityRulesDescription: string
readonly BoundaryRulesDescription: string
readonly RequiredArtifacts: ReadonlyArray<RequiredArtifact>
@@ -77,8 +77,8 @@ export type AwaitingSnapshotSelection = {
readonly ParseBudget: number
}
export type SnapshotReady = {
readonly _tag: "SnapshotReady"
export type TrustedSnapshotSelected = {
readonly _tag: "TrustedSnapshotSelected"
readonly SelectedSnapshot: SelectedSnapshot
readonly PreviousRunManifest: VerifiedPreviousRunManifest | null
readonly RequiredArtifacts: ReadonlyArray<RequiredArtifact>
@@ -86,8 +86,8 @@ export type SnapshotReady = {
readonly ParseBudget: number
}
export type DeterministicSegmentsReady = {
readonly _tag: "DeterministicSegmentsReady"
export type IngestableSnapshot = {
readonly _tag: "IngestableSnapshot"
readonly RunIdentity: RunIdentity
readonly SelectedSnapshot: SelectedSnapshot
readonly PreviousRunManifest: VerifiedPreviousRunManifest | null
@@ -97,7 +97,7 @@ export type DeterministicSegmentsReady = {
}
export type State =
| AwaitingSnapshotSelection
| SnapshotReady
| DeterministicSegmentsReady
| AwaitingTrustedBundle
| TrustedSnapshotSelected
| IngestableSnapshot
| ({ readonly _tag: "SnapshotIngested" } & UpstreamSnapshotIngested)
@@ -1,31 +1,67 @@
import { Either } from "effect"
import { foldFailure, makeVerifiedPreviousRunManifest } from "../models/factories.js"
import {
type AwaitingSnapshotSelection,
type DeterministicSegmentsReady,
type Event,
type IngestUpstreamSnapshot,
type RunManifest,
type State,
type UpstreamSnapshotIngested,
decideIngestableSnapshot,
deriveRequiredArtifactPaths,
} from "../index.js"
import { decideSegmentRecords } from "./segments.js"
import {
validatePreviousRunManifest,
validateSnapshotSelection,
} from "./selection.js"
parseBundleLocation,
} from "../models/ops.js"
import type {
AwaitingTrustedBundle,
Error,
Event,
IngestableSnapshot,
IngestUpstreamSnapshot,
State,
TrustedSnapshotSelected,
UpstreamSnapshotIngested,
} from "../models/types.js"
import type { RunManifest } from "../models/shared.js"
const toEvent = (
deterministicSegmentsReady: DeterministicSegmentsReady,
): Event => {
const artifactPaths = deriveRequiredArtifactPaths(
deterministicSegmentsReady.RunIdentity,
export const validatePreviousRunManifest = (
manifest: RunManifest,
): Either.Either<ReturnType<typeof makeVerifiedPreviousRunManifest>, Error> =>
manifest.ManifestPath && manifest.SegmentsPath && manifest.CanonicalProjectionPath
? Either.right(makeVerifiedPreviousRunManifest(manifest))
: Either.left(
foldFailure(manifest.SnapshotIdentity, "PreviousRunManifestNotVerified"),
)
const selectTrustedSnapshot = (
state: State,
command: IngestUpstreamSnapshot,
): Either.Either<TrustedSnapshotSelected, Error> => {
if (state._tag !== "AwaitingTrustedBundle") {
return Either.left(
foldFailure(command.SnapshotIdentity, "RunIdentityCouldNotBeDerived"),
)
}
return Either.map(
parseBundleLocation(command.SnapshotIdentity, command.BundleInput),
(bundleLocation) => ({
_tag: "TrustedSnapshotSelected" as const,
SelectedSnapshot: {
SnapshotIdentity: command.SnapshotIdentity,
BundleLocation: bundleLocation,
SnapshotMetadata: command.SnapshotMetadata,
},
PreviousRunManifest: command.PreviousRunManifest,
RequiredArtifacts: state.RequiredArtifacts,
MaxBundleBytes: state.MaxBundleBytes,
ParseBudget: state.ParseBudget,
}),
)
}
export const emitSnapshotIngested = (
ingestableSnapshot: IngestableSnapshot,
): Event => {
const artifactPaths = deriveRequiredArtifactPaths(ingestableSnapshot.RunIdentity)
const runManifest: RunManifest = {
RunIdentity: deterministicSegmentsReady.RunIdentity,
SnapshotIdentity: deterministicSegmentsReady.SelectedSnapshot.SnapshotIdentity,
RunIdentity: ingestableSnapshot.RunIdentity,
SnapshotIdentity: ingestableSnapshot.SelectedSnapshot.SnapshotIdentity,
ManifestPath: artifactPaths.ManifestPath,
SegmentsPath: artifactPaths.SegmentsPath,
CanonicalProjectionPath: artifactPaths.CanonicalProjectionPath,
@@ -34,7 +70,7 @@ const toEvent = (
const payload: UpstreamSnapshotIngested = {
RunManifest: runManifest,
SegmentRecords: deterministicSegmentsReady.SegmentRecords,
SegmentRecords: ingestableSnapshot.SegmentRecords,
CanonicalProjectionPath: artifactPaths.CanonicalProjectionPath,
SummaryPath: artifactPaths.SummaryPath,
}
@@ -46,8 +82,8 @@ export const decide = (
state: State,
command: IngestUpstreamSnapshot,
) =>
Either.flatMap(validateSnapshotSelection(state, command), (snapshotReady) =>
Either.map(decideSegmentRecords(snapshotReady), toEvent),
Either.flatMap(selectTrustedSnapshot(state, command), (trustedSnapshot) =>
Either.map(decideIngestableSnapshot(trustedSnapshot), emitSnapshotIngested),
)
export const apply = (_state: State, event: Event): State => {
@@ -57,10 +93,10 @@ export const apply = (_state: State, event: Event): State => {
}
}
export const makeAwaitingSnapshotSelection = (
overrides: Partial<AwaitingSnapshotSelection> = {},
): AwaitingSnapshotSelection => ({
_tag: "AwaitingSnapshotSelection",
export const makeAwaitingTrustedBundle = (
overrides: Partial<AwaitingTrustedBundle> = {},
): AwaitingTrustedBundle => ({
_tag: "AwaitingTrustedBundle",
RunIdentityRulesDescription: "derive from snapshot identity",
BoundaryRulesDescription: "require at least one deterministic boundary proof",
RequiredArtifacts: [
@@ -73,4 +109,4 @@ export const makeAwaitingSnapshotSelection = (
...overrides,
})
export { decideSegmentRecords, validatePreviousRunManifest, validateSnapshotSelection }
export { decideIngestableSnapshot }
@@ -1,42 +0,0 @@
import { Either } from "effect"
import {
type DeterministicSegmentsReady,
type Error,
type SnapshotReady,
applyRunIdentityRules,
validateBoundaryProofs,
validateRequiredArtifacts,
validateSegmentRecords,
} from "../index.js"
export const decideSegmentRecords = (
snapshotReady: SnapshotReady,
): Either.Either<DeterministicSegmentsReady, Error> =>
Either.flatMap(applyRunIdentityRules(snapshotReady.SelectedSnapshot), (derivedRunIdentity) =>
Either.flatMap(validateSegmentRecords(snapshotReady.SelectedSnapshot), (segmentRecords) =>
Either.flatMap(
validateBoundaryProofs(
snapshotReady.SelectedSnapshot.SnapshotIdentity,
segmentRecords,
),
(boundaryProofs) =>
Either.map(
validateRequiredArtifacts(
snapshotReady.SelectedSnapshot.SnapshotIdentity,
snapshotReady.RequiredArtifacts,
segmentRecords,
),
() => ({
_tag: "DeterministicSegmentsReady" as const,
RunIdentity: derivedRunIdentity.value,
SelectedSnapshot: snapshotReady.SelectedSnapshot,
PreviousRunManifest: snapshotReady.PreviousRunManifest,
SegmentRecords: segmentRecords,
BoundaryProofs: boundaryProofs,
RequiredArtifacts: snapshotReady.RequiredArtifacts,
}),
),
),
),
)
@@ -1,48 +0,0 @@
import { Either } from "effect"
import {
type Error,
type IngestUpstreamSnapshot,
type RunManifest,
type SnapshotReady,
type State,
foldFailure,
makeVerifiedPreviousRunManifest,
parseBundleLocation,
} from "../index.js"
export const validatePreviousRunManifest = (
manifest: RunManifest,
): Either.Either<ReturnType<typeof makeVerifiedPreviousRunManifest>, Error> =>
manifest.ManifestPath && manifest.SegmentsPath && manifest.CanonicalProjectionPath
? Either.right(makeVerifiedPreviousRunManifest(manifest))
: Either.left(
foldFailure(manifest.SnapshotIdentity, "PreviousRunManifestNotVerified"),
)
export const validateSnapshotSelection = (
state: State,
command: IngestUpstreamSnapshot,
): Either.Either<SnapshotReady, Error> => {
if (state._tag !== "AwaitingSnapshotSelection") {
return Either.left(
foldFailure(command.SnapshotIdentity, "RunIdentityCouldNotBeDerived"),
)
}
return Either.map(
parseBundleLocation(command.SnapshotIdentity, command.BundleInput),
(bundleLocation) => ({
_tag: "SnapshotReady" as const,
SelectedSnapshot: {
SnapshotIdentity: command.SnapshotIdentity,
BundleLocation: bundleLocation,
SnapshotMetadata: command.SnapshotMetadata,
},
PreviousRunManifest: command.PreviousRunManifest,
RequiredArtifacts: state.RequiredArtifacts,
MaxBundleBytes: state.MaxBundleBytes,
ParseBudget: state.ParseBudget,
}),
)
}
@@ -1,17 +1,19 @@
import { Effect, Either } from "effect"
import type {
Error,
Event,
IngestUpstreamSnapshot,
State,
} from "../models/types.js"
import {
type Error,
type Event,
type IngestUpstreamSnapshot,
type State,
decide,
makeAwaitingSnapshotSelection,
} from "../index.js"
makeAwaitingTrustedBundle,
} from "../policies/decideSnapshotIngest.js"
export const workflow = (
command: IngestUpstreamSnapshot,
state: State = makeAwaitingSnapshotSelection(),
state: State = makeAwaitingTrustedBundle(),
): Effect.Effect<Event, Error> =>
Effect.gen(function* () {
const decision = decide(state, command)
+13 -16
View File
@@ -3,11 +3,7 @@ import { describe, expect, it } from "vitest"
import {
type IngestUpstreamSnapshot,
makeAstNodeKind,
makeNormalizedHash,
makeRawHash,
makeRunIdentity,
makeShapeHash,
makeSnapshotIdentity,
makeTaintedBundleInput,
makeTaintedBundleLocation,
@@ -20,7 +16,7 @@ import {
import {
apply,
decide,
makeAwaitingSnapshotSelection,
makeAwaitingTrustedBundle,
validatePreviousRunManifest,
workflow,
} from "../src/contexts/ingest-snapshot/index.js"
@@ -42,16 +38,16 @@ describe("ingestSnapshot workflow", () => {
const event = await Effect.runPromise(workflow(makeCommand()))
expect(event._tag).toBe("UpstreamSnapshotIngested")
expect(event.payload.RunManifest.RunIdentity).toBe(makeRunIdentity("run:snapshot-001"))
expect(event.payload.RunManifest.RunIdentity).toBe("run:snapshot-001")
expect(event.payload.RunManifest.ManifestPath).toBe(
makeTrustedManifestPath("runs/run:snapshot-001/manifest.json"),
"runs/run:snapshot-001/manifest.json",
)
expect(event.payload.SegmentRecords).toHaveLength(1)
})
it("hard-stops when the bundle location is not parseable", () => {
const result = decide(
makeAwaitingSnapshotSelection(),
makeAwaitingTrustedBundle(),
makeCommand({
BundleInput: makeTaintedBundleInput(makeTaintedBundleLocation("not-a-path")),
}),
@@ -64,17 +60,17 @@ describe("ingestSnapshot workflow", () => {
})
it("applies the ingested event into SnapshotIngested state", () => {
const result = decide(makeAwaitingSnapshotSelection(), makeCommand())
const result = decide(makeAwaitingTrustedBundle(), makeCommand())
expect(Either.isRight(result)).toBe(true)
if (Either.isRight(result)) {
const nextState = apply(makeAwaitingSnapshotSelection(), result.right)
const nextState = apply(makeAwaitingTrustedBundle(), result.right)
expect(nextState._tag).toBe("SnapshotIngested")
if (nextState._tag === "SnapshotIngested") {
expect(nextState.RunManifest.CanonicalProjectionPath).toBe(
makeTrustedCanonicalProjectionPath("runs/run:snapshot-001/canonical.ts"),
"runs/run:snapshot-001/canonical.ts",
)
expect(nextState.SummaryPath).toBe(
makeTrustedSummaryPath("runs/run:snapshot-001/summary.json"),
"runs/run:snapshot-001/summary.json",
)
}
}
@@ -115,12 +111,13 @@ describe("ingestSnapshot workflow", () => {
expect(event.payload.SegmentRecords[0]).toMatchObject({
SegmentId: "snapshot-001:root",
AstNodeKind: makeAstNodeKind("Program"),
AstNodeKind: "Program",
Hashes: {
RawHash: makeRawHash("raw:snapshot-001"),
NormalizedHash: makeNormalizedHash("normalized:snapshot-001"),
ShapeHash: makeShapeHash("shape:snapshot-001"),
RawHash: "raw:snapshot-001",
NormalizedHash: "normalized:snapshot-001",
ShapeHash: "shape:snapshot-001",
},
})
expect(event.payload.SegmentRecords[0]?.Hashes.RawHash).toBe("raw:snapshot-001")
})
})