Compare commits
9 Commits
5ec088c250
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cd0fdadca1 | |||
| a2d191d7a7 | |||
| 8fca79e968 | |||
| 2c4df3998a | |||
| 121336a190 | |||
| 281c2a8a94 | |||
| 57a691236e | |||
| 858bf67043 | |||
| 558f2a0ea9 |
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
+4
-4
@@ -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
|
||||
|
||||
+59
@@ -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.
|
||||
+105
@@ -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.
|
||||
+242
@@ -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
|
||||
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
): {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user