diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22c83d1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +output/pdf/*.pdf binary diff --git a/.github/workflows/type-design-research.yml b/.github/workflows/type-design-research.yml new file mode 100644 index 0000000..30325fd --- /dev/null +++ b/.github/workflows/type-design-research.yml @@ -0,0 +1,44 @@ +name: Type design research + +on: + pull_request: + paths: + - research/type-driven-design/** + - .github/workflows/type-design-research.yml + push: + branches: [docs/type-driven-design] + paths: + - research/type-driven-design/** + - .github/workflows/type-design-research.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + research-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 26 + - uses: actions/checkout@v4 + with: + repository: specdog/dotdog + ref: 773dc265ecc30f1ec3cb417d1eb051719ea78ba9 + path: _research/dotdog + - uses: actions/checkout@v4 + with: + repository: sindresorhus/type-fest + ref: c3e825ffc1e687dd13a0003b771802d95964aaf9 + path: _research/type-fest + - run: npm install --omit=dev --ignore-scripts --no-audit --no-fund --package-lock=false + working-directory: _research/type-fest + - run: npm ci --ignore-scripts --no-audit --no-fund + working-directory: research/type-driven-design + - run: npm test + working-directory: research/type-driven-design + env: + DOTDOG_RESEARCH_ROOT: ${{ github.workspace }}/_research/dotdog + TYPE_FEST_RESEARCH_ROOT: ${{ github.workspace }}/_research/type-fest diff --git a/README.md b/README.md index 4ede8cd..5d6ecbb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ > Feed the dog. Ship with specs. -**[dotdog](https://specdog.github.io/dotdog)** — structured, AI-queryable specs. [npm](https://npmjs.com/package/dotdog) | [GitHub](https://github.com/specdog/dotdog) +[Guide](https://specdog.github.io/handbook) — spec-driven development with [type-driven design](https://specdog.github.io/handbook#type-driven-design): domain types, valid states, operation contracts, and checked runtime boundaries. + +**[dotdog 1.0](https://specdog.github.io/dotdog)** — local-first spec/code graphs, saved REA/Ghidra call-graph traversal, and optional TypeScript contract inspection. [npm](https://npmjs.com/package/dotdog) | [Release notes](https://github.com/specdog/dotdog/releases/tag/v1.0.0) | [Migration](https://specdog.github.io/dotdog/v1-migration) + +Run from your repository with npm: `npx dotdog@1.0.0 refresh --json`, or Bun: `bunx --bun dotdog@1.0.0 refresh --json`. Node.js 22.12+ or Bun 1.3+; the optional type adapter additionally requires Node and a trusted project-installed TypeScript 7.0.2 compiler. **[dogbench](https://specdog.github.io/dogbench)** — token-savings benchmark for Collar and dotdog. [GitHub](https://github.com/specdog/dogbench) diff --git a/about.html b/about.html index b873341..84fe084 100644 --- a/about.html +++ b/about.html @@ -58,10 +58,11 @@

About specdog

specdog is a toolkit for making software specs easier for both humans and AI agents to read. The goal is simple: keep intent, implementation rules, and agent instructions in structured files that can be validated and queried.

The project focuses on spec-driven development, agent discovery, and small runtime surfaces that reduce guesswork when an agent enters a codebase.

+

The type-driven design workflow connects spec rules to domain types, state transitions, and explicit failures in application code, with runtime parsing at external boundaries.

What we build

-
dotdog

CLI and MCP tooling for writing, validating, and querying structured project specs.

+
dotdog

Dotdog 1.0 provides local-first spec/code graphs, saved REA/Ghidra call-graph traversal, and optional TypeScript contract inspection. Release notes.

collar

An agent harness that loads graph-backed behavior instead of relying on loose prompt text.

leash

A robotics-oriented harness for simulation-first agent control and capability gating.

diff --git a/agents.html b/agents.html index 1dfda37..423fcef 100644 --- a/agents.html +++ b/agents.html @@ -24,12 +24,13 @@ a:hover { text-decoration: underline; } ol, ul { margin: 0 0 16px 26px; color: #374151; } li { margin-bottom: 6px; } + .code-wrap { position: relative; } .copy-btn { position: absolute; top: 8px; right: 8px; background: none; border: none; color: #94a3b8; cursor: pointer; padding: 4px 6px; border-radius: 4px; line-height: 1; } .copy-btn:hover { color: #e2e8f0; background: rgba(255,255,255,0.08); } .copy-btn.copied { color: #4ade80; } .copy-btn svg { width: 16px; height: 16px; } table { width: 100%; border-collapse: collapse; margin: 16px 0; } - th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #e5e7eb; font-size: .9em; } + th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #e5e7eb; font-size: .9em; overflow-wrap: anywhere; } th { font-weight: 600; color: #1a1a2e; } td { color: #374151; } footer { margin-top: 48px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: .8em; } @@ -72,28 +73,27 @@

For AI Agents

If you are an AI coding agent reading this page: follow these rules and you will not hallucinate on dotdog projects.

-

The agent entry point: CLAUDE.md

-

When an AI agent enters a dotdog project, it reads CLAUDE.md first — before any code, before any docs, before AGENTS.md. This is the Anthropic convention, adopted by TypeScript, React, and Jest.

-

CLAUDE.md tells the agent one thing: load the .dag. It is the shortest path from "I just arrived" to "I have the full entity graph."

-

If a project has CLAUDE.md and *.dag files committed to git, it is agent-ready.

+

Repository-local agent setup

+

Follow the host's project instructions: AGENTS.md for Codex and CLAUDE.md for Claude Code. Install Dotdog's repository-local skills with dotdog skills install --agent both, or configure dotdog serve as a local stdio MCP server.

+

Generated repository metadata belongs under ignored .doghouse paths by default. Decide explicitly which human-owned semantic/overlay inputs to track; do not commit generated graphs merely to enable a host.

Rule zero: load the DAG first

-

Every dotdog project has a compiled .dag file. This is your ground truth. It is 94% smaller than the source .dog files (740 tokens vs 8,100). You load it once. You get the full entity graph.

-

If you ignore the DAG and read .dog files directly, you waste 8,015 tokens scanning 600 lines of prose to find which 4 lines matter. The DAG is a compiled index: it does not replace reading, it replaces scanning.

+

Use compiled graphs for spec structure and targeted context. Keep intended specs, observed code and imported evidence distinct. Check freshness before relying on a generated graph; coverage may be partial.

+

Run refresh from the repository, then query actual labels or IDs. Token costs vary with the source and requested context; loading a graph is not proof that an agent will use it correctly.

-
find . -name "*.dag" -not -path "*/node_modules/*"
-cat projects/spec-platform/spec-platform.dag
+
npx dotdog@1.0.0 refresh --json
+npx dotdog@1.0.0 query "authentication"
+

Hard rules

    -
  1. Load the DAG before reading any code file.
  2. -
  3. Do not grep for structure. The DAG has the entity graph.
  4. -
  5. If the DAG does not say it, you do not know it. Say unverified.
  6. -
  7. If code contradicts DAG, DAG wins. Fix code toward DAG.
  8. -
  9. Run dotdog validate before committing. Score must not decrease.
  10. +
  11. Query the graph for modeled structure; respect the repository's agent instructions.
  12. +
  13. Retain provenance, coverage and truncation when using graph results.
  14. +
  15. Missing or stale evidence is unverified, not proof of absence.
  16. +
  17. Resolve spec/code disagreements through review; do not silently change requirements.
  18. +
  19. Run spec/graph checks and the project's compiler and tests before committing.

What the DAG gives you

@@ -112,52 +112,60 @@

MCP: live queries

ToolWhat it does getEntityGet entity with properties, states, lifecycle traverseFollow edges from a node, return subgraph +pathFind a bounded connecting path with supplied graph scope searchFind entities by name or type schemaGet full property schema for an entity summaryProject stats: node count, edge count, token savings listProjectsList all spec projects in the repo +workspace.listList workspace repositories and groups +contextRetrieve a token-bounded neighborhood +impactRetrieve bounded reverse dependencies +infraVerifyExplicitly verify configured infrastructure; may contact providers -
npx dotdog serve
+
npx dotdog@1.0.0 serve
+
+ +

Type-driven design

+

Use the type-driven design workflow when implementing modeled entities and lifecycles. Define domain types, state-specific data, and operation signatures before function bodies. Use explicit failure types and exhaustive handling where the language supports them.

+

Parse external data into checked domain values; do not treat a type assertion as validation. Run the project's type checker, compile-failure checks for forbidden usage, and runtime tests. Spec completeness and graph compilation do not prove application type safety or runtime correctness.

Commands reference

- - - + + + +
CommandWhen to use
dotdog validateBefore committing spec changes
dotdog compileAfter editing .dog files
dotdog stalenessCheck if spec and code have drifted
dotdog verifyCheck spec references match code files
dotdog verify --initAuto-generate verify section from codebase
dotdog refreshUpdate generated spec/code graphs
dotdog statusRead-only freshness check, not behavior verification
dotdog reference-checkCheck explicit lexical source mappings
dotdog types inspectInspect a selected supported TypeScript contract
dotdog analyzeGap detection and contradiction checking

Common failure modes

How to detect agent-ready projects

- + diff --git a/docs/dotdog-type-driven-design-thesis.md b/docs/dotdog-type-driven-design-thesis.md new file mode 100644 index 0000000..5929430 --- /dev/null +++ b/docs/dotdog-type-driven-design-thesis.md @@ -0,0 +1,378 @@ +# Type-Driven Design as Evidence-Linked Engineering + +## A practical thesis for Dotdog + +Prepared for Specdog. Research cutoff: 31 October 2026. Applied technical thesis, not a claim of a released feature or a peer-reviewed result. + +## Abstract + +Type-driven design uses representations and operation signatures to guide implementation. Specifications express intended behavior, while software graphs connect engineering artifacts. These practices can cooperate, but none substitutes for the others. This thesis argues that Dotdog's defensible opportunity is not another compiler, schema language, or code graph. It is a small evidence-linking layer that helps a developer identify which stated rules have relevant, current checks and which remain unsupported. + +The investigation combines primary-source literature, inspection of Dotdog, a selected-symbol extraction prototype, runtime boundary experiments, and a graph-evidence bookkeeping experiment. The earlier prototype passed 47 automated checks, including 2,000 malformed-input trials, 500 generated valid manifests, and targeted extraction from two public codebases on two compiler versions. All 16 selected symbol/compiler combinations resolved, but ten contexts remained explicitly partial. The experiments reproduced production validator defects and an optional-property compatibility gap in the parser prototype. Fourteen additional graph-evidence cases test missing, stale, unreviewed, ambiguous, and incorrectly classified evidence. + +The proposed integration keeps the chosen contract source authoritative, uses existing compilers and validators, and makes graph links inspectable rather than treating them as proof. Its strongest immediate use is a bounded Dotdog dogfood pilot around workspace input and command-selection rules. Improved review speed, better agent output, and standalone-product demand remain hypotheses requiring separate evaluation. The conclusion is therefore conditional: integrate a narrow, optional evidence workflow before considering a new tool or broad correctness claims. + +## 1. The problem and the thesis + +### 1.1 What a Dotdog user actually needs + +A developer maintaining several repositories rarely needs another list of interfaces. The difficult questions are operational: which consumer is affected by this change; why does this field exist; where is this input checked; which test demonstrates that a forbidden operation is rejected; and whether last week's result still applies to today's source. + +Dotdog already positions itself around structured intent, repository mapping, and agent-readable graphs. Its inspected source includes workspace configuration types, manifest validation, repository selection, and graph-query infrastructure. The current data-design audit examines modeling gaps, not application type safety. The selected checkout must therefore be distinguished from the proposed addition: type-context extraction and evidence-aware rule links are research prototypes, not released Dotdog capabilities. [20,21] + +The practical deficiency is not that compilers lack graphs. It is that intended rules, language-level representations, runtime boundaries, and verification results are usually maintained in different places. A green compiler result may not address the rule under review. A spec-to-file link may locate relevant code without establishing that the code enforces anything. A previously passing test may be stale. + +### 1.2 Central proposition + +**Dotdog should help users navigate from a reviewed rule to its implementation obligations and current verification evidence, while preserving the distinctions between intent, representation, behavior, and evidence freshness.** + +This is an application and integration thesis, not a claim that type-driven design or traceability is new. The value proposition is testable: reduce the work needed to locate relevant enforcement and expose gaps that a plain compiler run or file graph does not explain. Whether that benefit exceeds maintenance cost has not yet been demonstrated in a user study. + +### 1.3 Research questions + +1. Where can type-driven design improve an actual Dotdog workflow rather than add terminology? +2. Which obligations can types express, and which require runtime checks or other reasoning? +3. What must graph evidence record before a user can safely interpret a coverage claim? +4. Which existing tools should be reused, and what additional dependency burden is justified? +5. What evidence would justify a standalone tool rather than a module inside Dotdog? + +The thesis answers the first four with an architecture, worked use cases, and bounded experiments. It provides decision criteria rather than an empirical answer to the fifth. + +## 2. Research method and evidence boundaries + +### 2.1 Method + +This is an exploratory design investigation. Sources were selected for direct relevance to types-first implementation, compiler-backed context, traceability, contract generation, and compatibility. Preference was given to original papers and official documentation. This is not an exhaustive systematic review, and first-party product documentation establishes documented capability, not comparative effectiveness. + +The empirical work has three parts. Phase A tests selected-symbol extraction, type contracts, boundary parsing, and narrow compiler-version behavior. Phase B evaluates evidence-state logic using explicitly synthetic graph fixtures. A proposed Phase C would measure real developer and coding-agent outcomes; it has not been conducted. + +### 2.2 Inspected artifacts + +The Dotdog corpus is pinned to commit `773dc265ecc30f1ec3cb417d1eb051719ea78ba9`. The independent Type-Fest corpus is pinned to `c3e825ffc1e687dd13a0003b771802d95964aaf9`. Phase A results and retained experiment notes are versioned in the research harness. The baseline harness was committed as `38e2b7e`; subsequent graph-evidence work is separately identified by source hashes in the thesis test record. [20-22] + +No production Dotdog source was changed by these experiments. Targeted research configurations were used rather than either repository's complete build. Reference builds require their own treatment rather than an equivalent-looking single-program compilation. [17] The corpus was chosen for engineering relevance and type complexity, not sampled to represent all software projects. Selection therefore limits generalization. + +### 2.3 Claim classes + +This thesis distinguishes four kinds of statement. An **observation** reports inspected source or executed behavior. A **documented capability** reports what a primary source specifies. A **proposal** describes a future Dotdog behavior. A **hypothesis** predicts a user or performance benefit that still requires measurement. These categories must not be collapsed into a single product promise. + +Passing tests establish only their asserted conditions. In particular, tests that reproduce a known crash or compiler incompatibility can pass while the underlying defect remains unfixed. That convention is explicit in the retained results. [21] + +## 3. Foundations and related work + +### 3.1 Type-driven design is a construction method + +In a types-first workflow, the programmer chooses representations and signatures before completing implementations, then revises both as understanding improves. Idris demonstrates this construction-oriented use of types through interactive editing, case splitting, and inspection of unfinished definitions. This is stronger than adding annotations after writing an algorithm. [1] + +Ordinary domain modeling can use the same direction without adopting dependent types. Instead of independent flags such as `paid`, `shipped`, and `cancelled`, a developer can model alternatives whose required data varies by state. Wlaschin's worked examples illustrate how representation choices can exclude combinations that a looser record permits. The relevant lesson is to encode the rule where feasible, not to maximize type-level sophistication. [2] + +### 3.2 Parsing preserves established information + +A runtime predicate can confirm an input property and then discard that knowledge. A parser can instead return a value whose type retains the established structure. King's argument for parsing into stronger values motivates a boundary between untrusted input and domain operations. It does not remove the runtime check; it changes what the successful check returns. [3] + +This distinction matters for Dotdog's workspace manifests. A function returning `{ valid: true }` does not itself provide a correctly shaped configuration value to its caller. A discriminated parse result can make successful data available only in the success branch. That design is useful even without a graph. The graph becomes useful when a maintainer needs to locate the rule, parser, consumers, and relevant checks together. + +### 3.3 Type guarantees depend on language and configuration + +TypeScript uses structural compatibility and permits some intentionally unsound behavior. Different string-alias names do not establish nominal identity. Assertions and `any` can bypass ordinary checking. Compiler settings also change which programs are accepted. Consequently, a Dotdog integration cannot describe a TypeScript check as an unconditional guarantee. [4] + +Rust typestate can associate operations with particular state types and consume a value during a transition. This can prevent reuse of that value, unlike a typical TypeScript union. It still does not prove that two values do not represent the same external order or that a remote side effect occurs only once. Language-specific adapters must preserve such differences. [5] + +### 3.4 Traceability is established engineering work + +The traceability literature treats links as relationships among engineering artifacts that must be created, maintained, and used. The 2014 research roadmap provides a basis for understanding trace creation and maintenance as a lifecycle problem, not a diagram-generation task. [6] + +Research on live traceability deployment further emphasizes the practical difficulty of maintaining useful links and the role of domain knowledge and available data. This cautions against assuming that generating more edges automatically produces more useful engineering knowledge. For Dotdog, reviewed links and visible uncertainty are therefore preferable to presenting all inferred relationships as equally authoritative. [7] + +### 3.5 Existing navigation and contract tools + +SCIP already provides a language-independent indexing protocol for source navigation, including definitions and references. LSP standardizes editor-facing language interactions. These are relevant integration surfaces, not evidence that Dotdog needs to replace language servers or build a universal semantic indexer. [8,9] + +TypeSpec supports designing API contracts and emitting several representations. Buf distinguishes source-code and wire-compatibility categories for Protobuf changes. These systems address important parts of contract authoring and evolution that should not be rebuilt casually inside Dotdog. [10,11] + +Schema export is not a universal bridge between type systems. Zod documents constructs that cannot be faithfully represented as JSON Schema. JSON Schema also has its own required-property and additional-property semantics. An exported shape is not necessarily a lossless representation of an application type, validator transform, or lifecycle policy. [12,19] + +### 3.6 Type context for coding agents + +The ChatLSP work investigates supplying type and binding context to language models. Its TypeScript implementation used a proof-of-concept approach, including manually simulated relevant-header retrieval, and its threats-to-validity discussion limits broad conclusions. It supports investigating semantic context, not claiming that this Dotdog prototype improves arbitrary agent tasks. [13] + +Type-constrained generation is a different mechanism. The PLDI 2025 work constrains generation through a specialized incremental engine over a supported language subset. Ordinary post-generation compiler feedback should not be described as equivalent token-level enforcement. [14] + +TypePilot provides another distinct approach: type-focused revision of generated Scala code. Its small examples and verification difficulties make it useful motivation for structured workflows, but not proof of whole-project reliability. The thesis accordingly avoids transferring published benchmark improvements to Dotdog. [15] + +## 4. An evidence-linked model for Dotdog + +### 4.1 Separate the authorities + +Four responsibilities must remain distinct. A specification records intended behavior and rationale. A canonical contract source defines the chosen machine-readable representation. An implementation performs operations. A check produces evidence under stated conditions. + +The canonical contract source is a project decision. In a code-first project it may be application types and parsers. In a schema-first project it may be TypeSpec, Protobuf, or another maintained contract. Generated code must not silently become a second editable authority. Dotdog's graph should point to the configured source rather than create a third copy of the contract in `.dog` files. + +Specs and graphs are optional for type-driven design itself. Their justification inside Dotdog is traceability and coordinated change, not making a compiler function. + +### 4.2 Graph structure + +Let the evidence graph contain requirements, obligations, contract symbols, operations, boundaries, checks, runs, and input snapshots. Edges record relationships such as a requirement explaining an obligation, an operation using a type, a check exercising an obligation, and a run observing a check under a snapshot. + +The essential path is: + +```mermaid +flowchart LR + R[Requirement] --> O[Reviewed obligation] + O --> A[Contract / operation] + A --> C[Check] + C --> E[Run record] + E --> S[Input snapshot] +``` + +This graph is an index of engineering claims and observations. It is not a theorem prover. An `implements` edge means that someone or some process associated artifacts; it does not establish that the implementation satisfies the requirement. + +Identity must include repository and symbol context. A type named `State` in one package is not interchangeable with another package's `State`. Recursive types and legal state cycles also mean traversal cannot assume that all underlying relationships are acyclic merely because an artifact uses the `.dag` extension. + +### 4.3 Obligations prevent false completeness + +A rule should be decomposed into obligations that are meaningful for that rule. A manifest-input rule might require compile-time narrowing, runtime shape rejection, and semantic cross-reference checks. A payment rule may require runtime authorization and idempotency even when a state type restricts which operation can be called. + +The obligation list is reviewed engineering input, not automatically inferred truth. A type checker result cannot be relabeled as runtime evidence. A runtime example test cannot establish that every forbidden type-level call is rejected. A generic graph path cannot substitute for either. + +This approach intentionally avoids a single completeness percentage. A rule can have one current compile check, a stale runtime result, and an unresolved semantic obligation. Collapsing those states into one number would hide the distinction most relevant to the reviewer. + +### 4.4 Freshness is necessary, not sufficient + +For a particular check, a current-evidence label requires a reviewed association, the expected check kind, a passing result, and matching tracked inputs. The record must include the relevant rule revision, contract and implementation hashes, check-definition hash, compiler identity, and configuration identity. Ambiguous histories must be resolved explicitly rather than selecting an old pass because it appears first. + +This is an operational evidence predicate, not semantic entailment. Correct hashes cannot rescue an irrelevant test or a wrong rule. A complete list of tracked inputs is itself an assumption. When an adapter reports partial dependency context, the system must not present the result as a complete-evidence claim. + +Whole-file hashes are conservative but can invalidate evidence after irrelevant edits. Signature-only hashes are cheaper but may miss behavior changes. The first pilot should prefer conservative invalidation, expose its cause, and measure the review burden before attempting more aggressive semantic caching. + +### 4.5 Proposed user-visible answers + +The useful output is not simply a picture. A future Dotdog view could answer: this rule has a current compile check; its runtime check predates the parser change; these consumers reference the changed contract; and this inferred link has not been reviewed. + +The existing research evaluator only tests a small part of that behavior using explicit fixtures. It does not automatically create reviewed links from natural language, discover all dependencies, or ingest arbitrary CI logs. Those are separate integration tasks. + +## 5. Real-life uses and adoption order + +### 5.1 Workspace manifest parsing: the first dogfood case + +The strongest immediate case is Dotdog's own manifest boundary. The inspected validator accepts `unknown` but throws on malformed nested values such as a null repository entry. It also accepts an incomplete group lacking its repository list. These behaviors were executed against the pinned checkout, not inferred from a hypothetical customer story. [20,21] + +The type-driven intervention is to parse raw data into a discriminated result before domain operations use it. A developer first defines what a valid workspace contains, then checks that success exposes that data while failure exposes diagnostics. Runtime checks retain the cross-reference rules already present in the validator. + +The graph's added value would be coordination: link the manifest rule to the schema, the parser, `WorkspaceConfig`, its consumers, and the exact compile/runtime checks. When an optional field changes, Dotdog could identify the relevant obligations and invalidate affected evidence. The immediate benefit is a clearer failure and review path, not a new type system. + +The current Zod-based prototype is not ready to replace the production validator. [18] Its optional-property inference conflicts with the existing interfaces under exact optional-property checking, and its default object parsing strips unknown fields. Both policies require explicit review before adoption. + +### 5.2 Explicit multi-repository command selection + +The inspected selection code uses a discriminated `WorkspaceSelection` and rejects conflicting CLI selectors at runtime. A related guard rejects implicit selection when multiple repositories would be affected. This is already a practical separation between raw options, domain selection, and operational policy. [20] + +Type-driven design can keep internal callers from constructing incomplete selection variants. Runtime parsing remains necessary for command-line input, and the multi-repository guard remains necessary for the chosen policy. The graph could connect the policy to the parser, selection type, guard, command consumers, and tests. + +The user benefit would be targeted maintenance: a new selector should lead the reviewer to every handling branch and policy check, not merely every text occurrence of the word `repo`. This benefit is proposed; the experiments did not measure review time or complete command-level integration. + +### 5.3 Lifecycle changes in an application + +Consider a worked scenario in which an application adds a refunded state to an order lifecycle. The scenario is illustrative, not an observed Dotdog customer deployment. A type-first workflow changes the state representation and operation signatures, lets exhaustive handlers reveal omissions, and then adds runtime checks for amounts, authorization, and external side effects. + +A graph can connect the intended change to the state type, transition functions, API responses, rendering branches, and tests. Its contribution is explaining the affected surface and the remaining obligations. It cannot turn a type label into evidence that a payment was captured or a shipment occurred. + +The earlier contract experiments tested the smaller mechanism: adding a variant produces a diagnostic when an explicit exhaustive handler is left unchanged; weakening the handler removes that rejection. They did not test a deployed payment or fulfillment system. + +### 5.4 Contract evolution across frontend and backend + +In another worked scenario, an API renames a field or changes its optionality. Type-driven work should start from the project's chosen contract authority, then regenerate or update consumers and check actual serialization behavior. TypeSpec or Protobuf tooling may already own this part of the workflow. [10,11] + +Dotdog should add an impact and evidence layer: identify producers and consumers, the compatibility policy each check applies, and which contract version each result covers. Source compatibility, JSON compatibility, and binary wire compatibility are different questions. A graph may connect them, but must not collapse them into one generic breaking-change label. + +Cross-language nominal identity is particularly limited. A TypeScript brand is not automatically transmitted in JSON or recreated by another language's generated model. Validation and version policy must remain explicit at the boundary. + +### 5.5 Focused context for a coding agent + +For a selected operation, the prototype can retrieve its signature and relevant named type declarations. The real-code experiment resolved selected symbols through two compiler versions without exporting function bodies as the default context. It also exposed partial closure for complex library types. [21,22] + +A future Dotdog task workflow could combine a spec rule, selected type context, reviewed links, and check commands. The agent would receive bounded context and explicit gaps rather than the entire repository graph. However, the current adapter requires a selected file and export; it does not independently choose the right operation from a user task. + +The expected benefit is less rediscovery and more relevant constraints. That remains a hypothesis until tested against ordinary repository context and existing spec/graph context under the same coding tasks, budgets, and behavioral checks. + +### 5.6 Priority + +| Candidate | Why investigate it | Evidence today | Main unresolved question | +| --- | --- | --- | --- | +| Manifest parsing and its rule links | Concrete local defects and a small boundary | Runtime and compile experiments | Optional-field and unknown-field policy | +| Explicit workspace selection | Existing domain type and operational guard | Source inspection and type extraction | Command-level trace completeness | +| Contract-change review | Clear multi-repo coordination problem | Worked scenario and existing external tools | Consumer coverage and review cost | +| Agent task context | Plausible fit with graph-query workflows | Selected-symbol feasibility | Measured task improvement | + +This ordering is an engineering judgment based on scope and available evidence, not a market-demand ranking. + +## 6. Architecture and dependency policy + +### 6.1 A small integration surface + +The proposed module has three responsibilities: read contract and symbol information from supported tools; associate reviewed obligations with source and checks; and report evidence state with provenance. It should reuse existing Dotdog graph components where their semantics fit. A new graph database, language, or hosted service is not required for the first pilot. + +An extraction adapter should identify its compiler and configuration, resolve imported symbols, state what it omitted, and invalidate results when relevant inputs change. A runner adapter should distinguish intended negative diagnostics from environmental failure. A graph query should preserve those distinctions rather than manufacture an overall green result. + +### 6.2 What not to rebuild + +Do not rebuild compiler type inference, schema validation, API generation, or Protobuf compatibility checking. Do not fork existing contract definitions into a parallel Dotdog-only type DSL. Do not equate an automatically inferred association with a reviewed claim of enforcement. + +SCIP and language-server interfaces are candidates for broader indexing and navigation integration. TypeSpec and Buf are candidates for contract-aware metadata. These are architectural options supported by documented capabilities, not integrations implemented by this thesis. [8-11] + +### 6.3 Additional dependencies + +No dependency was added to production Dotdog. The research harness has five direct development dependencies: three aliased TypeScript versions, Zod, and fast-check. The new graph-evidence evaluator uses only ordinary JavaScript. PDF-rendering packages are document-production tools, not Dotdog runtime requirements. [21] + +A production pilot should use the project's supported compiler and existing parser where possible. Compiler matrices and property-testing packages belong to development checks. The research harness's tested Node version must not silently become a new requirement for every Dotdog user. + +TypeScript 7.1 beta has a replacement API with different entry points and explicit object lifetimes. The separate snapshot smoke test confirms a narrow diagnostic/repair path, not compatibility of the older extractor. A production adapter therefore needs an explicit version-support policy rather than a universal `typescript` import assumption. [16] + +## 7. Evaluation and results + +### 7.1 Phase A: extraction, contracts, and boundaries + +The retained local suite passed 47 checks with zero failures or skips. A clean lockfile-based installation also passed 47. The prior Linux CI runs passed. The environment and package versions are recorded with the results. [21] + +The generated runtime trials used fixed seeds: 1,000 arbitrary JSON values, 1,000 mutations of nested manifest fields, and 500 generated valid manifests. The valid-input control matters: a parser that rejects everything would look robust on malformed inputs while being useless. The prototype returned structured outcomes for the malformed trials and preserved the generated valid configurations. + +Four deliberate contract weakenings were exercised on both older compiler versions: removing distinct ID brands, widening an accepted state, making required state data optional, and weakening an exhaustive handler. Original negative cases produced their intended diagnostics; the weakened contracts stopped rejecting those cases. This is a bounded mutation-style experiment, not a full mutation score from Stryker. [23] + +### 7.2 Phase A: real-code context + +Four selected targets in each corpus were evaluated with TypeScript 5.9.3 and 6.0.3. All 16 target/compiler combinations resolved. Six returned checked context and ten returned partial context. The per-target classifications agreed between these two compiler versions. + +| Target | Context bytes | Classification | +| --- | ---: | --- | +| Dotdog selectionFromOptions | 360 | checked | +| Dotdog WorkspaceConfig | 907 | checked | +| Dotdog RepositoryWorldModel | 1,783 | partial | +| Dotdog auditDesign | 646 | checked | +| Type-Fest Simplify | 664 | partial | +| Type-Fest Merge | 5,724 | partial | +| Type-Fest Jsonify | 7,523 | partial | +| Type-Fest Tagged | 348 | partial | + +The extractor respected its 16 KB output budget. Complex utilities reached declaration limits or referenced external definitions. Byte sizes are not model-token measurements, and smaller context is not automatically better context. These results do not establish token savings, successful task completion, or complete semantic closure. + +Initial failures were retained. A generic-parameter traversal mistake was corrected. An inappropriate module-resolution configuration and stricter optional-property policy produced diagnostics in the first Dotdog corpus run; the final targeted configuration records the chosen policy explicitly. No production source was modified to manufacture a pass. + +### 7.3 Phase B: graph-evidence behavior + +Fourteen additional tests evaluate the research evidence-state function. Their data are synthetic and explicitly reviewed fixtures, not automatically recovered production trace links. The tests cover a valid evidence checklist and thirteen situations that must not produce the same result. + +Those situations include a missing runtime check, an unreviewed link, changed parser content, changed compiler or configuration, a missing symbol, a skipped or failed check, a changed requirement, a mismatched check kind, ambiguous run history, partial context, and unknown completeness. The purpose is to test bookkeeping distinctions that a future Dotdog graph must preserve. + +All fourteen targeted cases passed locally. Both combined-suite runs passed 61 checks with zero failures or skips, including the clean lockfile-based environment. The associated thesis test record retains counts, timings, and source hashes. Passing these fixtures establishes neither the relevance of real links nor the completeness of actual dependency capture. + +### 7.4 What is and is not supported + +The experiments support selected-symbol extraction on the tested configurations, discriminated boundary-result design under the compatible optional-field policy, and explicit evidence-state handling in the fixture model. They also demonstrate useful failure modes rather than merely collecting green checks. + +They do not establish a production graph integration, universal compiler compatibility, complete repository verification, a security guarantee, reduced developer effort, improved agent output, or customer demand. The primary product hypothesis remains open. + +## 8. Limitations, counterarguments, and alternatives + +### 8.1 Trace maintenance may cost more than it saves + +The strongest objection is that users can already run their compiler and tests. If Dotdog requires substantial manual linking and produces only another checklist, the addition may make work slower. The pilot must therefore measure link-maintenance time alongside retrieval or review benefits. Traceability deployment research makes this a substantive concern rather than an incidental UX issue. [7] + +### 8.2 A typed model can faithfully encode the wrong rule + +Types constrain programs according to the chosen representation. They cannot establish that the representation matches the user's intent. Similarly, a graph may accurately connect a requirement to an irrelevant test. Review remains necessary at the intent-to-obligation boundary, and experimental evaluation must include incorrect but plausible links. + +### 8.3 Partial information is normal + +Recursive utility types, external dependencies, generated contracts, dynamic configuration, and cross-repository deployments complicate evidence capture. The real-code experiment's ten partial contexts should therefore shape the interface, not be hidden as exceptional errors. A partial report can still be useful if its scope is clear; it is unsuitable as an unrestricted completeness claim. + +### 8.4 Temporal and operational rules exceed ordinary shape types + +Authorization, idempotency, actual external side effects, and many numeric relationships remain runtime concerns in ordinary application type systems. More expressive verification systems can address some such obligations, but adopting them would be a separate product and engineering decision. This thesis does not propose that Dotdog become a general verification language. + +### 8.5 A graph may be unnecessary + +A small repository with a clear contract module and a handful of tests may gain little from graph bookkeeping. A plain reviewed mapping file and existing check commands may be sufficient. The graph is justified when relationships are reused for change impact, cross-repository coordination, onboarding, or bounded agent context. Visualization is optional; useful queries are the primary benefit. + +### 8.6 Stronger incumbent tooling may be the right answer + +If the user's main problem is API-contract generation, TypeSpec may be the more direct investment. If it is Protobuf compatibility, use the existing Buf policy. If it is source navigation, existing language tools or SCIP indexes may suffice. Dotdog should add context and evidence relationships around such tools only when that combination reduces real work. [8-11] + +## 9. Validation plan and decision gates + +### 9.1 First implementation experiment + +Start with one reviewed rule family around workspace input, not every entity in every graph. Resolve the optional-field and unknown-field policies. Associate the existing and prototype checks with explicit obligations. Run both successful and malformed input cases, change a contract and a parser body, and verify that the graph reports the right evidence as stale or incomplete. + +A second bounded case can address workspace selection and its explicit multi-repository policy. Its purpose is to test a different obligation mix, not to inflate a count of mapped symbols. + +### 9.2 Developer evaluation + +Use concrete maintenance tasks: add a manifest field, add a selection variant, and review a producer/consumer contract change. Compare existing compiler/test tools, ordinary Dotdog graph context, and the proposed evidence-linked view. Counterbalance task order and preserve the same acceptance criteria. Measure time to find the relevant checks, missed obligations, incorrect links accepted, review decisions, and time spent repairing the links themselves. + +These measures should be interpreted together. A faster answer with a missed runtime obligation is not an improvement. A larger graph is not success. No numeric benefit target is justified by the present data. + +### 9.3 Coding-agent evaluation + +Compare the same coding tasks under ordinary repository context, spec/graph context, and added semantic type/evidence context. Keep model, budget, instructions, and behavioral checks constant. Repeat runs where generation varies and retain unsuccessful outputs. Measure functional acceptance, relevant diagnostic handling, repair attempts, actual prompt tokens, latency, and harmful changes outside task scope. + +This is a distinct experiment from the current compiler probes. It is required before advertising improved coding-agent performance or token efficiency. + +### 9.4 Split into a standalone tool only on evidence + +A standalone tool becomes plausible if users repeatedly obtain value from contract/evidence inspection without adopting Dotdog specs or graphs, and if maintaining an independent installation does not erase that benefit. Until then, a separable module inside Dotdog limits coordination and dependency cost without pretending to settle the product question. + +## 10. Conclusion + +Type-driven design can work with specs and graphs, but not because a graph grants types new enforcement power. Types shape the implementation; parsers and tests check additional obligations; specs express intent; graphs connect those artifacts and their changing evidence. + +For Dotdog, the immediate opportunity is concrete: make it easier to identify the current enforcement and remaining gaps for a reviewed rule. Workspace parsing supplies a real dogfood problem, while command selection and contract-change review supply bounded follow-up cases. The existing prototypes establish parts of the mechanism and expose limits that must remain visible. + +The research therefore recommends an optional, narrow integration rather than a new compiler, type DSL, or standalone product. Its next success criterion is not another feature list. It is a demonstrated improvement in a user's maintenance task without hiding stale, partial, or irrelevant evidence. + +## References + +[1] Idris contributors. Interactive Editing. Official Idris 2 documentation. https://idris2.readthedocs.io/en/latest/tutorial/interactive.html + +[2] Scott Wlaschin. Designing with Types: Making Illegal States Unrepresentable. 2013. https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/ + +[3] Alexis King. Parse, Don't Validate. 2019. https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ + +[4] Microsoft. Type Compatibility. TypeScript Handbook. https://www.typescriptlang.org/docs/handbook/type-compatibility.html + +[5] Rust Embedded Working Group. Typestate Programming. The Embedded Rust Book. https://doc.rust-lang.org/embedded-book/static-guarantees/typestate-programming.html + +[6] Jane Cleland-Huang, Olly Gotel, Jane Huffman Hayes, Patrick Mäder, and Andrea Zisman. Software Traceability: Trends and Future Directions. ICSE/FOSE, 2014. https://selab.netlab.uky.edu/homepage/publications/2014-ICSE-FOSE.pdf + +[7] Alberto D. Rodriguez, Katherine R. Dearstyne, and Jane Cleland-Huang. Understanding the Challenges of Deploying Live-Traceability Solutions. 2023. https://arxiv.org/abs/2306.10972 + +[8] SCIP contributors. SCIP Code Intelligence Protocol. https://github.com/scip-code/scip + +[9] Microsoft. Language Server Protocol, version 3.17. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ + +[10] Microsoft and TypeSpec contributors. Emitters. TypeSpec documentation. https://typespec.io/docs/extending-typespec/emitters-basics/ + +[11] Buf. Breaking Change Rules and Categories. https://buf.build/docs/breaking/rules/ + +[12] Zod contributors. JSON Schema. https://zod.dev/json-schema + +[13] Andrew Blinn, Xiang Li, June Hyung Kim, and Cyrus Omar. Statically Contextualizing Large Language Models with Typed Holes. OOPSLA, 2024. https://arxiv.org/abs/2409.00921 + +[14] Niels Mündler, Jingxuan He, Hao Wang, Koushik Sen, Dawn Song, and Martin Vechev. Type-Constrained Code Generation with Language Models. PLDI, 2025. https://arxiv.org/abs/2504.09246 + +[15] Alexander Sternfeld, Andrei Kucharavy, and Ljiljana Dolamic. TypePilot: Leveraging the Scala Type System for Secure LLM-Generated Code. 2025. https://arxiv.org/abs/2510.11151 + +[16] Daniel Rosenwasser / Microsoft. Announcing TypeScript 7.1 Beta. 20 October 2026. https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/ + +[17] Microsoft. Project References. TypeScript Handbook. https://www.typescriptlang.org/docs/handbook/project-references + +[18] Zod contributors. Defining Schemas. https://zod.dev/api + +[19] JSON Schema contributors. Object Reference. https://json-schema.org/understanding-json-schema/reference/object + +[20] Specdog. Dotdog source, commit 773dc265ecc30f1ec3cb417d1eb051719ea78ba9. https://github.com/specdog/dotdog/tree/773dc265ecc30f1ec3cb417d1eb051719ea78ba9 + +[21] Specdog. Type-Driven Design Research Harness and Results. Baseline commit 38e2b7e; thesis follow-up in PR 66. https://github.com/specdog/specdog.github.io/pull/66 + +[22] Type-Fest contributors. Source, commit c3e825ffc1e687dd13a0003b771802d95964aaf9. https://github.com/sindresorhus/type-fest/tree/c3e825ffc1e687dd13a0003b771802d95964aaf9 + +[23] Stryker contributors. Mutant States and Metrics. https://stryker-mutator.io/docs/mutation-testing-elements/mutant-states-and-metrics/ diff --git a/docs/type-driven-design-evidence.json b/docs/type-driven-design-evidence.json new file mode 100644 index 0000000..f373589 --- /dev/null +++ b/docs/type-driven-design-evidence.json @@ -0,0 +1,134 @@ +{ + "scope": "Small local compiler probes; not a Dotdog integration or agent benchmark", + "typescript": { + "compiler": "6.0.3", + "options": { + "strict": true, + "noEmit": true, + "types": [], + "skipLibCheck": true, + "target": "ES2022" + }, + "results": [ + { + "name": "valid transition", + "accepted": true, + "diagnostics": [], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nship(paid, 'tracking');" + }, + { + "name": "draft transition", + "accepted": false, + "diagnostics": [ + 2322 + ], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nship({ state: 'draft', id: '1' }, 'tracking');" + }, + { + "name": "missing tracking", + "accepted": false, + "diagnostics": [ + 2741 + ], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nconst shipped: Shipped = { state: 'shipped', id: '1', receipt: 'receipt' };" + }, + { + "name": "missing union branch", + "accepted": false, + "diagnostics": [ + 2322 + ], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nfunction label(order: Order): string { switch(order.state) { case 'draft': return 'draft'; case 'paid': return 'paid'; default: const exhaustive: never = order; return exhaustive; } }" + }, + { + "name": "plain ID aliases can mix", + "accepted": true, + "diagnostics": [], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\ntype UserId = string; type OrderId = string; const user: UserId = 'user'; const order: OrderId = user;" + }, + { + "name": "distinct ID brands cannot mix", + "accepted": false, + "diagnostics": [ + 2322 + ], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\ndeclare const brand: unique symbol; type UserId = string & { readonly [brand]: 'UserId' }; type OrderId = string & { readonly [brand]: 'OrderId' }; declare const user: UserId; const order: OrderId = user;" + }, + { + "name": "unchecked assertion bypasses shape", + "accepted": true, + "diagnostics": [], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nconst order = {} as Paid; ship(order, 'tracking');" + }, + { + "name": "repeat transition remains allowed", + "accepted": true, + "diagnostics": [], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nship(paid, 'first'); ship(paid, 'second');" + }, + { + "name": "empty tracking remains allowed", + "accepted": true, + "diagnostics": [], + "source": "type Draft = { state: 'draft'; id: string };\ntype Paid = { state: 'paid'; id: string; receipt: string };\ntype Shipped = { state: 'shipped'; id: string; receipt: string; tracking: string };\ntype Order = Draft | Paid | Shipped;\nfunction ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\nconst paid: Paid = { state: 'paid', id: '1', receipt: 'receipt' };\nship(paid, '');" + } + ] + }, + "rust": { + "compiler": "rustc 1.96.0 (ac68faa20 2026-05-25)", + "edition": "2024", + "results": [ + { + "name": "valid_transition", + "accepted": true, + "diagnostics": [], + "source": "struct Draft;\nstruct Paid;\nstruct Shipped;\nimpl Paid { fn ship(self) -> Shipped { Shipped } }\nfn main() { let paid = Paid; let _shipped = paid.ship(); }\n" + }, + { + "name": "draft_transition", + "accepted": false, + "diagnostics": [ + "E0599" + ], + "source": "struct Draft;\nstruct Paid;\nstruct Shipped;\nimpl Paid { fn ship(self) -> Shipped { Shipped } }\nfn main() { let draft = Draft; let _shipped = draft.ship(); }\n" + }, + { + "name": "reuse_consumed_state", + "accepted": false, + "diagnostics": [ + "E0382" + ], + "source": "struct Draft;\nstruct Paid;\nstruct Shipped;\nimpl Paid { fn ship(self) -> Shipped { Shipped } }\nfn main() { let paid = Paid; let _first = paid.ship(); let _second = paid.ship(); }\n" + }, + { + "name": "separate_values_same_state", + "accepted": true, + "diagnostics": [], + "source": "struct Draft;\nstruct Paid;\nstruct Shipped;\nimpl Paid { fn ship(self) -> Shipped { Shipped } }\nfn main() { let first = Paid; let second = Paid; let _a = first.ship(); let _b = second.ship(); }\n" + } + ] + }, + "semanticContext": { + "importedName": "dispatch", + "resolvedName": "ship", + "file": "domain.ts", + "line": 3, + "signature": "(order: Paid, tracking: string): Shipped", + "parameters": [ + { + "name": "order", + "type": "Paid" + }, + { + "name": "tracking", + "type": "string" + } + ], + "returnType": "Shipped", + "sources": { + "app.ts": "import { dispatch } from './index';\nexport const deliver = dispatch;\n", + "domain.ts": "export type Paid = { state: 'paid'; receipt: string };\nexport type Shipped = { state: 'shipped'; receipt: string; tracking: string };\nexport function ship(order: Paid, tracking: string): Shipped { return { ...order, state: 'shipped', tracking }; }\n", + "index.ts": "export { ship as dispatch } from './domain';\n" + } + } +} diff --git a/docs/type-driven-design-research.md b/docs/type-driven-design-research.md new file mode 100644 index 0000000..07915ef --- /dev/null +++ b/docs/type-driven-design-research.md @@ -0,0 +1,124 @@ +# Type-driven design: research and Dotdog direction + +Research date: 2026-10-31. This is a proposal, not a shipped capability or a market-validation result. + +Follow-up: [tested findings and reproducible prototype](type-driven-design-tested-findings.md). That report includes real-code corpus checks, runtime properties, mutation-style contract checks, and updated compiler-API research. + +## Recommendation + +Start with a distinct type-first design workflow in Dotdog. Prototype a small, read-only TypeScript adapter that supplies relevant type context and collects compiler evidence. Do not build a new type system, universal code generator, or token-level decoder. + +Keep the adapter independent of `.dog` parsing so it can eventually be reused without Dotdog. A separate product is justified only after testing whether users want that independent workflow. The sources below establish technical feasibility, not demand or competitive uniqueness. + +## Three different practices + +| Practice | Primary job | Does not establish | +| --- | --- | --- | +| Spec-driven design | State intended behavior and constraints | That application code enforces the rules | +| Type-driven design | Shape representations and operations using types, then refine implementation with type feedback | That every business rule is encoded or every external input is trustworthy | +| Graph mapping | Record relationships and expose relevant artifacts | Type safety or behavioral correctness | + +Type-driven design is not merely annotating existing code or generating interfaces from schemas. The type-first loop is: understand a domain rule, choose representations and operation signatures, use compiler feedback to refine them, implement, and test what types cannot express. [Idris interactive editing](https://idris2.readthedocs.io/en/latest/tutorial/interactive.html) demonstrates types guiding construction through hole inspection and case splitting. [Scott Wlaschin](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/) demonstrates changing representations to exclude invalid domain combinations. + +## Existing tools: integrate, do not duplicate + +| Layer | Existing capability | Implication for Dotdog | +| --- | --- | --- | +| Semantic type information | [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) exposes symbols, types, and diagnostics; [ts-morph](https://ts-morph.com/) wraps the API | Use semantic resolution, not regex matching of type names. An additional wrapper is optional, not required. | +| Boundary parsing | [Zod](https://zod.dev/api) supports schemas and branded types; [Effect Schema](https://effect.website/docs/v3/schema/introduction) models decoded values, encoded values, and requirements | Reuse a project's existing parser. Do not introduce a second schema authority. | +| Type regression checks | [tsd](https://github.com/tsdjs/tsd) checks declaration contracts; [Vitest](https://vitest.dev/guide/testing-types) delegates type tests to a compiler | Record and run existing checks; ordinary runtime tests do not substitute for them. | +| Ownership-sensitive state transitions | [Rust typestate](https://doc.rust-lang.org/embedded-book/static-guarantees/typestate-programming.html) changes state types and can consume the old value; [trybuild](https://docs.rs/trybuild/latest/trybuild/) tests forbidden API usage | Language adapters must preserve differences in guarantees. A TypeScript union is not Rust ownership. | +| Executable state machines | [XState TypeScript support](https://stately.ai/docs/typescript) types machine-related data and operations | Import explicit machine definitions if the project already has them; do not infer full transition enforcement from a state enum. | +| Runtime properties | [fast-check](https://fast-check.dev/docs/introduction/) generates inputs and shrinks counterexamples | Useful for parser and transition invariants left to runtime. Generated cases are not a proof. | + +[Alexis King's parsing principle](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) concerns preserving information established by validation in stronger returned values. It does not mean eliminating runtime input checks. + +[Zod's JSON Schema documentation](https://zod.dev/json-schema) identifies representations that cannot be exported faithfully, including transforms and custom schemas. Therefore a graph or JSON Schema export must not be treated as a lossless replacement for the source type/parser system. + +The older TypeScript Compiler API documentation targets 6.0 and earlier. [TypeScript 7.1 beta now documents the replacement API](https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/); a separate native-API smoke test is included in the follow-up. Support and test explicit compiler versions; report unsupported versions instead of silently falling back to syntactic guesses. + +## Research on type-guided AI coding + +| Work | Evidence | Limit for this decision | +| --- | --- | --- | +| [Statically Contextualizing Large Language Models with Typed Holes, OOPSLA 2024](https://arxiv.org/abs/2409.00921) | Uses type and binding context through language-server integration; evaluates Hazel and a TypeScript port of MVUBench. Type definitions are particularly useful context. | Supports a type-context prototype, not a claim that Dotdog already reduces tokens or improves coding success. ChatLSP is a proposed extension, not assumed standard support. | +| [Type-Constrained Code Generation with Language Models, PLDI 2025](https://arxiv.org/abs/2504.09246) and its [implementation](https://github.com/eth-sri/type-constrained-code-generation) | Uses an incremental completion engine to constrain generation; evaluates TypeScript versions of HumanEval and MBPP. | Models a TypeScript subset, not the full compiler. Its reported evaluation also accepts already-compiling unconstrained samples and otherwise uses constrained resampling. A normal compiler feedback loop is not this decoding algorithm. | +| [TypePilot, 2025](https://arxiv.org/html/2510.11151v1) | Studies Scala code generation with a separate vulnerability-analysis and type-focused revision stage. Reports improvements on input and injection tasks. | Small examples and manual evaluations do not establish whole-repository effectiveness. Its Stainless experiments did not consistently produce compiling verification code. Figure 5 exposes a `ValidAge` case-class constructor; given [Scala's construction behavior](https://docs.scala-lang.org/tour/case-classes.html), the displayed smart constructor alone does not establish an unbypassable invariant. This is source analysis, not a local Scala execution. | +| [ChopChop, POPL 2026](https://arxiv.org/abs/2509.00360) | Studies semantic constrained decoding for type safety and equivalence to a reference program. | A research alternative, not evidence that Dotdog should own model inference or that ordinary type checking proves equivalence. | + +Inference: type-aware context plus an ordinary compiler-feedback loop is the most practical initial experiment. Token-level constrained decoding is a separate research track and would require access to generation controls; it cannot be promised for an arbitrary hosted coding assistant. + +## What the current Dotdog checkout provides + +Inspected commit: `773dc265ecc30f1ec3cb417d1eb051719ea78ba9`. These are source observations, not verification of the latest published release. + +- [`design/audit.ts`](https://github.com/specdog/dotdog/blob/773dc265ecc30f1ec3cb417d1eb051719ea78ba9/packages/dotdog/src/design/audit.ts) examines compiled entities for identifiers, lifecycle states, ownership, access patterns, sensitivity, and connectivity. It does not invoke an application type checker. +- [`map/repoMapper.ts`](https://github.com/specdog/dotdog/blob/773dc265ecc30f1ec3cb417d1eb051719ea78ba9/packages/dotdog/src/map/repoMapper.ts) uses file/path conventions and manifest facts. Its inspected implementation does not resolve TypeScript symbols or callable signatures. +- [`dag/schema.ts`](https://github.com/specdog/dotdog/blob/773dc265ecc30f1ec3cb417d1eb051719ea78ba9/packages/dotdog/src/dag/schema.ts) already has symbol, spec, test, and command nodes, plus `defines`, `implements`, `tests`, and `depends_on` edges. Reuse those before expanding the graph vocabulary. + +The gap is semantic type context and evidence-backed links to domain rules, not another generic graph format. + +## Local feasibility probes + +These deliberately small probes test mechanics and limits. They are not a benchmark of coding agents, a security evaluation, or an integration test of Dotdog. + +### TypeScript 6.0.3 + +Used the installed compiler API with `strict`, `noEmit`, no ambient type packages, `skipLibCheck`, and ES2022. Each case was compiled separately using a draft/paid/shipped union and a `ship(Paid, string): Shipped` function. + +| Probe | Compiler result | Meaning | +| --- | --- | --- | +| Paid to shipped | Accepted | Valid typed usage works | +| Draft passed to shipping | Rejected, TS2322 | Input-state mismatch detected | +| Shipped value missing tracking | Rejected, TS2741 | State-specific required field enforced | +| Exhaustiveness check missing shipped branch | Rejected, TS2322 | Explicit `never` check detects omission | +| Two plain string ID aliases mixed | Accepted | Different alias names do not make nominal types | +| Two distinctly branded string IDs mixed | Rejected, TS2322 | Brands distinguish these statically typed values | +| Empty object asserted as paid | Accepted | Assertion bypass remains possible | +| Same paid value shipped twice | Accepted | This representation does not enforce consumption | +| Empty tracking string | Accepted | `string` does not encode nonempty data | + +### Rust 1.96.0, edition 2024 + +Used `rustc --emit=metadata` on four separate programs. A non-`Copy`, non-`Clone` paid-state value exposed `ship(self)`. + +| Probe | Compiler result | +| --- | --- | +| Paid value shipped | Accepted | +| Shipping method called on draft | Rejected, E0599 | +| Same paid value consumed twice | Rejected, E0382 | +| Two independently constructed paid values shipped | Accepted | + +Ownership prevents reuse of this value, not duplicate business records or repeated external side effects. Idempotency, transactions, and authoritative state checks remain runtime concerns. + +### Semantic context extraction + +A three-file TypeScript probe imported `dispatch` through a re-export of `ship`. The compiler resolved it to `domain.ts:3`, recovered `(order: Paid, tracking: string): Shipped`, and identified parameter and return types. All source files type-checked. This verifies a narrow extraction path, not complete support for generics, overloads, project references, or monorepos. + +Machine-readable results are in [type-driven-design-evidence.json](type-driven-design-evidence.json). + +## Proposed first product slice + +1. **Design before implementation.** Capture domain values, invalid combinations, allowed transitions, expected failures, and boundary assumptions. Let the user revise types as understanding changes; do not freeze a generated interface as the design. +2. **Read actual type context.** For a selected operation, resolve its symbol, input/output types, relevant variant definitions, and source locations using the project's supported compiler configuration. Keep raw type declarations in the language, not copied into a competing DSL. +3. **Connect explicit intent.** Link reviewed domain-rule IDs to actual symbols and checks. File references or matching names alone are not proof of enforcement. Record inferred links as inferred. +4. **Collect separate evidence.** Run configured type checks, expected compile failures, and runtime tests. Include compiler/configuration version, source revision, diagnostics, and check status. Missing dependencies, unsupported syntax, and skipped checks must remain unverified. +5. **Deliver bounded agent context.** Supply the selected operation, necessary reachable types, constraints, and unresolved questions. Avoid dumping the entire type graph into every prompt. Generated context is an export, not a new authority. + +No new command names are committed by this proposal. Keep type-design output visibly separate from the current data-model completeness audit. Do not claim that a clean audit proves domain correctness. + +## Evaluation before expanding scope + +- Compare the same tasks with three conditions: ordinary repository context, spec/graph context, and spec/graph plus semantic type context. Hold model, task, budget, and checks constant; retain failures and use repeated trials where generation varies. +- Include wrong IDs/units, omitted variants, wrong-state operations, malformed external inputs, duplicate transitions, and a requirement change introducing a new state. +- Measure behavioral test success, intended compile-failure diagnostics, false positives, unresolved cases, repair iterations, actual prompt tokens, and wall-clock time. Do not optimize token count alone. +- Introduce deliberate weakening mutations: replace a branded ID with `string`, make a required field optional, widen a state union, or remove an exhaustiveness guard. The relevant verification should fail for the intended reason. +- Check valid use before negative use. An unrelated import error must not count as proof that a prohibited transition is rejected. +- Exercise imported aliases, re-exports, generics, overloads, project references, configuration changes, and unsupported compiler versions before describing the adapter as repository-wide. +- Review whether the types encode the correct rule. A compiler can faithfully enforce the wrong design. + +## Add-on versus separate tool + +Current recommendation: a separable adapter inside Dotdog, paired with a type-first workflow. Promote it to a standalone tool only if independent users get value from type-context extraction or contract verification without adopting `.dog` or `.dag`, and measured results justify the extra installation and maintenance surface. + +Open questions: which language users need first; whether authoring assistance or existing-code analysis saves more work; acceptable configuration burden; and whether the selected real tasks improve with type context. This research does not settle those product questions. diff --git a/docs/type-driven-design-tested-findings.md b/docs/type-driven-design-tested-findings.md new file mode 100644 index 0000000..d4482f9 --- /dev/null +++ b/docs/type-driven-design-tested-findings.md @@ -0,0 +1,131 @@ +# Type-driven design: tested findings + +This extends the [initial research](type-driven-design-research.md) with a runnable [research harness](../research/type-driven-design/README.md). The initial harness did not modify production Dotdog source; later candidate repairs are tracked separately below. + +The later [technical thesis](dotdog-type-driven-design-thesis.md) adds real-life use cases, a deeper traceability/tooling review, and fourteen synthetic graph-evidence cases. Its combined 61-check record is preserved separately from the original experiment below. + +## Decision after testing + +Keep the work inside Dotdog rather than creating a separate tool. The research remains distinct from the bounded, unmerged 1.0 implementation described below. Prioritize checked boundary values and selected-symbol type context. Do not market it as a universal type-design tool, full semantic indexer, or type-safe coding agent. + +The tests establish narrow engineering feasibility and expose production and prototype gaps. They do not establish user demand, better agent output, or token savings. + +## What the deeper literature changes + +1. **The new compiler API is available to test.** Microsoft's [TypeScript 7.1 beta announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/) documents separate synchronous/asynchronous APIs and disposable programs/snapshots. The earlier research's future-tense API warning is outdated. A real 7.1 beta snapshot test now detects TS2322, verifies a repaired snapshot, and confirms the old snapshot still contains its error. This does not make the 5/6 extractor compatible with 7.1. +2. **The agent-context evidence is narrower than an integration claim.** [ChatLSP's TypeScript methodology and threats to validity](https://arxiv.org/html/2409.00921v1) say relevant-header retrieval was manually simulated in that implementation. The paper also acknowledges idealized baselines, a closely translated benchmark, and cases where irrelevant headers hurt results. Therefore, selecting useful context still needs a separate measured experiment; extracting types does not establish that advantage. +3. **Test the contracts themselves.** The [TypeScript 3.9 documentation](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#ts-expect-error-comments) describes expected-error comments, but such a comment does not identify the intended diagnostic. This harness checks diagnostic code and file after establishing valid baseline usage. Its deliberately weakened contracts exercise a principle related to [mutation testing](https://stryker-mutator.io/docs/mutation-testing-elements/mutant-states-and-metrics/); Stryker itself was not run. +4. **Configuration is part of the guarantee.** [Project-reference documentation](https://www.typescriptlang.org/docs/handbook/project-references) explains why a reference build is not equivalent to compiling one file set. The prototype rejects reference-based projects rather than reporting a misleading pass. Null checks and exact optional-property rules are tested separately. + +## Executed checks + +Environment: Node 26.8.1; TypeScript 5.9.3, 6.0.3, and 7.1.0-beta; Zod 4.6.7; fast-check 4.10.2. Versions are pinned in the research package lockfile. + +The local suite passed **47 tests, zero failures, zero skips**. A clean installation from the lockfile was tested separately; machine-readable counts and timings are in [the test results](../research/type-driven-design/results/tests.json). CI results are separate from local results and remain whatever the linked PR actually reports. + +| Area | What ran | What it establishes | +| --- | --- | --- | +| Type context | Both older compilers; alias/re-export resolution, overloads, recursive/generic types, computed brand keys, inherited config, and path aliases | Selected declarations can be recovered without dumping function bodies | +| Contract weakening | Wrong-ID, wrong-state, required-field, and exhaustive-handler mutations on both compilers | Each original check rejects its intended misuse, and deliberately weakening its contract removes that rejection | +| Configuration and bypasses | Disabled null checks; `unknown` versus `any` and assertions; strict optional-property mismatch | A compiler pass is conditional on configuration and can be bypassed | +| Runtime boundary | 1,000 arbitrary JSON values and 1,000 nested-manifest mutations, with fixed seeds | The parser prototype returned structured results without crashes for these cases | +| Positive runtime control | 500 generated valid manifests, plus explicit examples | The parser is not merely rejecting every input | +| Real source corpora | Four selected symbols in each of Dotdog and Type-Fest, on two compilers | Targeted extraction and incomplete-context reporting work on these selections | +| New native API | 7.1 beta diagnostic and immutable-snapshot repair | The installed new API works for this narrow workflow | + +The generated-input trials are not 2,500 independent tests, exhaustive fuzzing, or a security evaluation. Their seeds and generators are retained in the harness. + +## Real-code results + +Dotdog revision: `773dc265ecc30f1ec3cb417d1eb051719ea78ba9`. Type-Fest revision: `c3e825ffc1e687dd13a0003b771802d95964aaf9`. + +Both targeted programs produced zero diagnostics on both compilers after their research configurations were corrected. These configurations are not substitutes for either repository's full build/test suite. Dotdog uses an explicitly selected Bundler-resolution research configuration because this checkout has no application `tsconfig`; Type-Fest uses NodeNext resolution and its required exact optional-property behavior. + +All 16 selected target/compiler combinations resolved. Six contexts were `checked`; ten were `partial`. The per-target results below were identical in status, declaration count, and byte size on 5.9 and 6.0: + +| Target | Declarations | Context bytes | Status | +| --- | ---: | ---: | --- | +| Dotdog `selectionFromOptions` | 2 | 360 | checked | +| Dotdog `WorkspaceConfig` | 6 | 907 | checked | +| Dotdog `RepositoryWorldModel` | 8 | 1,783 | partial: external utility type | +| Dotdog `auditDesign` | 4 | 646 | checked | +| Type-Fest `Simplify` | 3 | 664 | partial: external utility type | +| Type-Fest `Merge` | 24 | 5,724 | partial: budget and external definitions | +| Type-Fest `Jsonify` | 24 | 7,523 | partial: budget and external definitions | +| Type-Fest `Tagged` | 3 | 348 | partial: external definitions, including the brand symbol | + +The 16 KB output cap was respected. These are UTF-8 bytes, not model tokens or demonstrated savings. Program setup/checking cost hundreds of milliseconds in these local runs; extracting already-loaded selected symbols was much cheaper. See [the measured corpus results](../research/type-driven-design/results/corpora.json) for timings; these single-run timings are not a performance benchmark. + +## Concrete defects and limits found + +### Original boundary defects and the unmerged candidate repair + +At the pinned `773dc26` research baseline, `validateWorkspaceConfig(unknown)` throws a `TypeError` for `repos: [null]` and for a non-array `groups` value. It also reports an incomplete group with no `repos` array as valid. The historical research tests retain those reproductions against that checkout; they are not tests of the later repaired 1.0 candidate. + +The parser prototype performs shape parsing before calling the existing semantic validator. It returns a discriminated success/error result with typed data and retains duplicate-alias and unknown-edge checks. It is not deployed, and it is not a complete production fix. + +The released [1.0 implementation](https://github.com/specdog/dotdog/pull/332) instead repairs the existing validator with manual shape checks and a typed success/error parser result, without adding Zod as a runtime dependency. Its real compiler regression uses `strict` and `exactOptionalPropertyTypes`, accepts validated success data as `WorkspaceConfig`, and checks that accessing failure-result data produces TS2339. This avoids adopting the prototype's optional-field incompatibility; it does not claim that the separate Zod prototype has been fixed. + +### Prototype compatibility gap retained + +Under ordinary strict checking, success-result data is assignable to Dotdog's real `WorkspaceConfig` without an assertion; error results do not expose `data`. With `exactOptionalPropertyTypes: true`, the assignment fails with TS2379 because Zod optional properties can include explicit `undefined` whereas the existing interfaces distinguish omission from that value. Two tests intentionally reproduce this incompatibility. Normalization or a reviewed optional-value policy is required before adoption. + +### Context extraction is not yet a general semantic closure + +- Inferred signatures, classes, and project-reference builds are not fully supported. The prototype labels these cases partial or unsupported. +- Budgeted traversal of Type-Fest's complex utilities omits definitions. Built-in utility types and dependencies outside the configured source root are explicitly reported rather than expanded. +- Computed unique-symbol keys initially lost their marker declaration; a regression now exercises marker retrieval. +- The adapter requires a selected file/export. It does not find the right operation from a natural-language task or rank all potentially useful headers. +- Source/config fingerprints attach evidence to inputs; they do not prove that a declared rule is the correct rule. + +## Failures during the experiment + +The first context-only run passed 14 of 16 tests: it incorrectly emitted generic parameter binders as independent declarations. That was corrected and tested. A later ESM fixture initially produced unrelated module-format diagnostics; the fixture was corrected instead of accepting those diagnostics as a negative-contract success. + +The initial Dotdog corpus experiment also produced TS2835 under inappropriate NodeNext resolution and TS2375 with a stricter optional-property policy. Those results are retained in [the experiment notes](../research/type-driven-design/results/experiment-notes.json). The final targeted configuration is explicit about those changes; no production source was altered to make the research pass. + +## What remains before a product claim + +1. Complete current-head compatibility and release review of the bounded candidate adapter before promising broader support. If adopting the Zod prototype, resolve its optional-field incompatibility; the existing manual production parser does not require that prototype. A beta native API is not mandatory. +2. Add actual rule-to-symbol/test links, with reviewed intent and evidence provenance; names alone do not prove enforcement. +3. Run a controlled coding-task comparison with ordinary repository context, spec/graph context, and added type context. Measure behavioral success, repair iterations, actual tokens, time, and failures under the same conditions. +4. Validate whether users want this workflow independently of Dotdog before splitting it into a standalone tool. + +No coding-agent A/B study, model-weight training, whole-repository verification, or production rollout was performed in this experiment. + +## Scoped 1.0 gap review + +The combined research suite now passes **62 checks with zero failures or skips**, including a second native TypeScript 7.1 beta test. It resolves `dispatch` through a re-export to `ship`, obtains the original declaration path and `(order: Paid) => Paid` signature, then changes the imported contract and observes TS2322 in the new snapshot while the old one stays valid. This uses explicit virtual files/compiler options. Full project configuration, reference builds and selected-dependency extraction still need an adapter; the 5/6 extractor is not silently treated as compatible with 7.0 or 7.1. + +The same review found that the installed REA 6.8.0 call-graph body omits traversal direction even though the producer accepts forward/backward requests. Treating every returned neighbor as a callee can therefore reverse call meaning. The candidate import now requires the declared producer direction: + +```sh +dotdog graph import native-calls.json --format rea-callgraph --project native --artifact-sha256 --rea-direction forward +``` + +Use `backward` for caller traversal exports. The declaration is recorded, not independently verified from the directionless body. Existing graph traversal/context/impact remain the query engine; there is no new Ghidra provider or semantic-indexing framework. Candidate fixes also preserve graph coverage/node provenance, disclose bounded negative searches, enforce token limits on empty/error responses, and apply repository boundaries to path reads. Real forward/backward Ghidra fixture checks and 116 local candidate tests passed at that checkpoint, before the type module implementation below. No simulator or cloud service is required. + +The original 47-test and 61-check result files and thesis remain historical checkpoints. This follow-up does not replace their measurements with new productivity or token-saving claims. + +## Additional boundary review + +The focused [candidate follow-up, PR #337](https://github.com/specdog/dotdog/pull/337), found and repaired two more spec-check gaps: direct graph reads bypassed the shared input-size guard, and explicit reference mappings accepted ambiguous entity labels. It reuses the existing reader, prioritizes exact IDs and adds no dependency or framework. Both regressions failed before repair; 118 local candidate tests passed afterward. An internal built-CLI fixture also checked failure exits, exact-ID acceptance and unchanged plan bytes. The broader audit remains open in [#334](https://github.com/specdog/dotdog/issues/334). + +Documentation now distinguishes freshness inspection from lexical diagnostics: scenario/reference checks do not independently establish graph freshness. Compile-time byte estimates are also distinct from context budgets using the declared tokenizer. Neither kind of token figure proves billing savings. + +A separate declaration-enabled TypeScript 7.0.2 CLI fixture exposed two requirements investigated before the [type adapter](https://github.com/specdog/dotdog/issues/333): + +- An inherited or explicit [`declarationDir`](https://www.typescriptlang.org/tsconfig/#declarationDir) can direct declarations outside an `outDir`-only temporary destination. Contain every output explicitly and record inspection-only overrides. +- [`stripInternal`](https://www.typescriptlang.org/tsconfig/#stripInternal) can remove a contract still referenced by an emitted public signature. Successful emit is not proof of complete dependency context; retain the contract through an explicit inspection policy or report the omission. + +The probe reproduced both cases and checked contained/full declaration output with explicit inspection settings. This supports a bounded CLI-adapter investigation, not a production integration claim or a new compiler framework. Saved diagnostics/maps also need repository-relative paths and untrusted-path handling; no automatic URL fetching or raw absolute-path evidence export is planned. + +## Released 1.0 implementation + +[Dotdog 1.0.0](https://github.com/specdog/dotdog/releases/tag/v1.0.0), source commit `1d6ebeed3f390ce7dbd8408bed8177114489ea3c`, includes the selected contract adapter from [PR #339](https://github.com/specdog/dotdog/pull/339). It uses the actual TypeScript **7.0.2** `unstable/sync` snapshot/checker API, separate from the historical 5/6 extractor and 7.1 beta experiments. A trusted project-installed compiler runs in a bounded Node worker; no compiler is auto-installed and no beta, service or mandatory runtime npm dependency is added. Other compiler versions and project-reference/composite/noCheck configurations fail explicitly. + +The original project configuration and diagnostics are preserved without declaration emit, configuration edits or build output. The adapter resolves supported aliases/re-exports, retains full selected signatures and bounded declared-type references, and imports them through existing graph workflows. Compiler/config/input evidence survives graph import and MCP retrieval. Coverage remains partial; recorded-input audits do not discover new files, authenticate a compiler or prove runtime behavior. See the [contract and limits](https://specdog.github.io/dotdog/type-contracts). + +The final suite passed **133 tests, 2,124 assertions and one snapshot**, including real valid → TS2322 → repaired code, changed-input audit, overloads/unions, inherited JSONC configuration/path aliases, Unicode signatures, no emit/build metadata, timeout/process-tree cleanup and output/input limits. [Merged-source CI](https://github.com/specdog/dotdog/actions/runs/41291852617) passed Linux Node 22/24/26, macOS/Windows Node 24 and Bun 1.3.14/1.4.4. The [publisher](https://github.com/specdog/dotdog/actions/runs/41292307924) checked one archive across supported runtime versions and published it with npm provenance. Fresh public-registry npm and Bun consumers passed spec refresh, context/impact, selected-type invalid/repair, recorded-evidence tamper, REA import/path and real stdio MCP checks. These checks are not a coding-agent productivity study; authenticated model-driven Claude behavior remains unverified. + +Historical bundle trials measured a development archive of 1,219,546 bytes and a narrow warm-cache startup improvement; those are not the published archive. The retained 1.0.0 archive is **1,222,323 bytes**, SHA-256 `ec66261ab2127225ebe0167ff88d661d566291eb577a6ae0931fab128093da97`. Its registry integrity and provenance subject match the publisher receipt and tagged source commit. [Measured optimization scope](https://specdog.github.io/dotdog/path-performance) preserves same-source workflow measurements and unchanged-result checks without claiming general cold-cache or coding-agent speedups. diff --git a/handbook.html b/handbook.html index 5e17815..11fe38f 100644 --- a/handbook.html +++ b/handbook.html @@ -4,7 +4,7 @@ Spec-Driven Development | specdog - + - + + +
+
+
+ Dotdog 1.0 Available now · Read the release notes +

Good specs.
Less guesswork.

+

Local-first specs, code graphs, and type contracts for coding agents. Keep what you intend separate from what the code shows — and give your agent the context to work with both.

+ + +
+
+

Feed the dog. Ship with specs.

+
-
🐕 Spec
-
- - - -
- -

specdog

-

Specs that ship. Agents that don't hallucinate.

-

specdog is a small family of tools for spec-driven development: write structured specs, map repos into agent-readable graphs, describe multi-repo workspaces, benchmark token savings, and run agent harnesses from the same source of truth.

+ +

Spec is on the case.

+
+ + + +
+ +
-
- specs - graphs - agents - benchmarks -
+
+
+

Start in your repository.

+

Refresh the local graph, then ask focused questions about context, impact, and paths.

+
+
+
+
+
+ + +
+ +
+
npx dotdog@1.0.0 refresh --json
+

+
+

Node.js 22.12+ or Bun 1.3+. Optional type inspection also requires Node and a trusted project-installed TypeScript 7.0.2 compiler.

+
+
-
-
npm install -g dotdog
-dotdog workspace init --id example-product
-dotdog workspace validate
-dotdog workspace graph --json
- -
+
+
+

Three perspectives. One workflow.

+

Intent, implementation, and evidence stay connected — not confused.

+
+
+
+

01 / Specs

+

Make intent queryable.

+

Write structured specs, check completeness, and keep human-owned rules separate from generated code observations.

+ Explore spec-driven design +
+
+

02 / Graphs

+

Follow the evidence.

+

Query context, impact, and paths. Import saved REA/Ghidra call graphs with artifact identity, coverage, and limits intact.

+ Explore graph workflows +
+ +
+
- +
+
+

New to 1.0? Start here.

+

What's changed, how to upgrade, and what the tools actually check.

+
+ +
-
-
dotdog

CLI and MCP tooling for `.dog` specs, `.dag` graphs, repo mapping, and multi-repo workspace manifests.

-
collar

DAG-first agent harness for loading graph-backed behavior.

-
leash

Robotics harness for simulation-safe agent control.

+
+

More from the pack

+
+
collar

An agent harness for graph-backed behavior.

+
leash

A separate robotics harness for simulation-first agent control.

+
dogbench

Retrieval and token-cost benchmarks for the tooling.

+
-
dotdog@0.5.0 · MIT
+ diff --git a/output/pdf/dotdog-type-driven-design-thesis.pdf b/output/pdf/dotdog-type-driven-design-thesis.pdf new file mode 100644 index 0000000..3b6c934 Binary files /dev/null and b/output/pdf/dotdog-type-driven-design-thesis.pdf differ diff --git a/research/type-driven-design/.gitignore b/research/type-driven-design/.gitignore new file mode 100644 index 0000000..e6cde66 --- /dev/null +++ b/research/type-driven-design/.gitignore @@ -0,0 +1,3 @@ +node_modules +.corpus/ +.pdf-venv/ diff --git a/research/type-driven-design/README.md b/research/type-driven-design/README.md new file mode 100644 index 0000000..2147a15 --- /dev/null +++ b/research/type-driven-design/README.md @@ -0,0 +1,55 @@ +# Type-driven design research harness + +Experimental code, not a shipped Dotdog command. Tested with Node 26.8.1. + +## Run + +```sh +npm ci --ignore-scripts --no-audit --no-fund +``` + +Set `DOTDOG_RESEARCH_ROOT` and `TYPE_FEST_RESEARCH_ROOT` to existing checkouts. The Type-Fest checkout needs its production dependency installed: + +```sh +npm --prefix "$TYPE_FEST_RESEARCH_ROOT" install --omit=dev --ignore-scripts --no-audit --no-fund --package-lock=false +npm test +node evaluate-repositories.mjs +``` + +The [CI workflow](../../.github/workflows/type-design-research.yml) checks out pinned revisions and runs these checks on Linux. The repository tests inspect their source inputs without editing them. The Type-Fest dependency-install step writes only the temporary checkout's ignored dependency directory. + +| Corpus | Revision | +| --- | --- | +| Dotdog | `773dc265ecc30f1ec3cb417d1eb051719ea78ba9` | +| Type-Fest | `c3e825ffc1e687dd13a0003b771802d95964aaf9` | + +## What is tested + +- TypeScript 5.9.3 and 6.0.3 context extraction, re-exports, overloads, generic bounds, recursive types, computed brand symbols, inherited configuration, path aliases, and bounded output. +- Four deliberate contract weakenings per compiler, null-check configuration changes, and assertions/`any` that bypass checks. +- A parser prototype around Dotdog's real workspace validator, including 2,000 seeded malformed-input trials and 500 generated valid manifests. +- Selected symbols in two real codebases, with missing dependencies and bounded context reported explicitly. +- A separate TypeScript 7.1 beta native-API snapshot/repair smoke test. The context adapter does not implement the 7.1 API. +- Fourteen synthetic graph-evidence cases distinguish current records from missing, stale, partial, unreviewed, ambiguous, or incorrectly classified evidence. These test bookkeeping, not semantic satisfaction of real requirements. + +## Reading the results + +`checked` means that the targeted program has no compiler diagnostics and this extractor reported no omitted references. It is not proof of domain correctness. `partial` and `unverified` must not be promoted to success by a caller. A passing negative test means that the intended diagnostic appeared; an unrelated build failure is not accepted as evidence. + +Some passing tests reproduce known defects or compatibility limits. In particular, the production validator still crashes for malformed nested values, and the parser prototype's optional-field inference is incompatible with the existing domain interfaces under `exactOptionalPropertyTypes: true`. The prototype strips unknown object fields through Zod's default object behavior. It is not a drop-in production replacement. + +[Research and measured findings](../../docs/type-driven-design-tested-findings.md) distinguish these checks from unperformed agent-performance experiments. + +## Thesis + +The [technical thesis source](../../docs/dotdog-type-driven-design-thesis.md) develops the real-life Dotdog use cases and the proposed evidence model. The [combined test record](results/thesis-tests.json) retains the 61-check local and clean-replay results; the earlier 47-check experiment remains separately preserved. + +Render the PDF from this directory: + +```sh +python3 -m venv .pdf-venv +.pdf-venv/bin/python -m pip install -r requirements-pdf.txt +.pdf-venv/bin/python render-thesis.py +``` + +PDF dependencies are for document production only. The renderer uses system Arial/Times fonts when available and base PDF fonts otherwise. diff --git a/research/type-driven-design/context.mjs b/research/type-driven-design/context.mjs new file mode 100644 index 0000000..89c0ff8 --- /dev/null +++ b/research/type-driven-design/context.mjs @@ -0,0 +1,113 @@ +import { createHash } from 'node:crypto'; +import { relative, resolve, dirname, isAbsolute } from 'node:path'; + +export function openProject(ts, configPath, { sourceRoot } = {}) { + if (![5, 6].includes(Number(ts.version?.split('.')[0]))) { + return { status: 'unsupported', reason: 'This research adapter implements only the TypeScript 5/6 API.' }; + } + const config = ts.readConfigFile(configPath, ts.sys.readFile); + if (config.error) return { status: 'invalid-config', diagnostics: [config.error.code] }; + const configRoot = dirname(resolve(configPath)); + const root = resolve(sourceRoot || configRoot); + const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, configRoot, undefined, configPath); + if (parsed.errors.length) return { status: 'invalid-config', diagnostics: parsed.errors.map(d => d.code) }; + if (parsed.projectReferences?.length) return { status: 'unsupported', reason: 'Project-reference builds need a separate adapter.' }; + const program = ts.createProgram({ rootNames: parsed.fileNames, options: { ...parsed.options, noEmit: true } }); + const checker = program.getTypeChecker(); + const diagnostics = ts.getPreEmitDiagnostics(program).map(d => ({ + code: d.code, + file: d.file ? relative(root, d.file.fileName).replaceAll('\\', '/') : null, + line: d.file && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null, + })); + const configHash = createHash('sha256').update(JSON.stringify(parsed.options)).digest('hex'); + const sourceHash = createHash('sha256'); + for (const file of program.getSourceFiles().slice().sort((a, b) => a.fileName.localeCompare(b.fileName))) { + sourceHash.update(file.fileName).update('\0').update(file.text).update('\0'); + } + const fingerprint = sourceHash.digest('hex'); + + function extract(filePath, name, { maxDeclarations = 24, maxBytes = 24000, maxDepth = 8 } = {}) { + const file = program.getSourceFile(resolve(filePath)); + if (!file) return { status: 'missing-file', name, diagnostics }; + const module = checker.getSymbolAtLocation(file); + const target = module && checker.getExportsOfModule(module).find(symbol => symbol.name === name); + if (!target) return { status: 'missing-export', name, diagnostics }; + const unalias = symbol => symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol; + const queue = [{ symbol: unalias(target), depth: 0 }]; + const seen = new Set(); + const declarations = []; + const omitted = []; + const external = new Set(); + let bytes = 0; + for (let index = 0; index < queue.length; index++) { + const { symbol, depth } = queue[index]; + const nodes = symbol.declarations || []; + if (nodes.every(node => ts.isTypeParameterDeclaration(node))) continue; + const key = nodes.map(node => `${node.getSourceFile().fileName}:${node.pos}`).join('|'); + if (!key || seen.has(key)) continue; + seen.add(key); + const node = nodes[0]; + const source = node.getSourceFile(); + const fileName = relative(root, source.fileName).replaceAll('\\', '/'); + if (fileName.startsWith('../') || isAbsolute(fileName) || fileName.startsWith('node_modules/') || fileName.includes('/node_modules/')) { + external.add(symbol.name); + continue; + } + if (!nodes.every(n => ts.isFunctionDeclaration(n) || ts.isTypeAliasDeclaration(n) || ts.isInterfaceDeclaration(n) || (ts.isVariableDeclaration(n) && n.type && source.isDeclarationFile))) { + omitted.push({ name: symbol.name, reason: 'unsupported-declaration' }); + continue; + } + const type = checker.getTypeOfSymbolAtLocation(symbol, node); + const signatures = type.getCallSignatures().map(signature => checker.signatureToString(signature, node, ts.TypeFormatFlags.NoTruncation)); + const text = nodes.every(n => ts.isFunctionDeclaration(n)) + ? signatures.map(signature => `function ${symbol.name}${signature};`).join('\n') + : nodes.map(n => ts.isVariableDeclaration(n) ? `declare const ${n.name.getText()}: ${n.type.getText()};` : n.getText()).join('\n'); + const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1; + const size = Buffer.byteLength(`${declarations.length ? '\n\n' : ''}${fileName}:${line}\n${text}`); + if (depth > maxDepth || declarations.length >= maxDeclarations || bytes + size > maxBytes) { + omitted.push({ name: symbol.name, reason: depth > maxDepth ? 'depth' : 'budget' }); + continue; + } + bytes += size; + declarations.push({ + name: symbol.name, + file: fileName, + line, + text, + signatures, + }); + if (nodes.some(n => ts.isFunctionDeclaration(n) && (!n.type || n.parameters.some(p => !p.type)))) { + omitted.push({ name: symbol.name, reason: 'inferred-signature-dependencies' }); + } + const visit = n => { + if (ts.isTypeReferenceNode(n) || ts.isExpressionWithTypeArguments(n) || ts.isTypeQueryNode(n) || ts.isComputedPropertyName(n)) { + const reference = checker.getSymbolAtLocation(n.typeName || n.expression || n.exprName); + if (reference) queue.push({ symbol: unalias(reference), depth: depth + 1 }); + } + if (ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n)) { + for (const parameter of n.typeParameters || []) visit(parameter); + for (const parameter of n.parameters) if (parameter.type) visit(parameter.type); + if (n.type) visit(n.type); + return; + } + ts.forEachChild(n, visit); + }; + for (const declaration of nodes) visit(declaration); + } + return { + status: diagnostics.length ? 'unverified' : omitted.length || external.size ? 'partial' : 'checked', + compiler: ts.version, + strict: Boolean(parsed.options.strict), + configHash, + sourceHash: fingerprint, + target: name, + declarations, + omitted, + external: [...external].sort(), + diagnostics, + bytes, + text: declarations.map(d => `${d.file}:${d.line}\n${d.text}`).join('\n\n'), + }; + } + return { status: diagnostics.length ? 'unverified' : 'checked', diagnostics, program, checker, extract }; +} diff --git a/research/type-driven-design/dependencies.mjs b/research/type-driven-design/dependencies.mjs new file mode 100644 index 0000000..8cbc22b --- /dev/null +++ b/research/type-driven-design/dependencies.mjs @@ -0,0 +1,7 @@ +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +export function dependency(name) { + return require(name); +} diff --git a/research/type-driven-design/evaluate-repositories.mjs b/research/type-driven-design/evaluate-repositories.mjs new file mode 100644 index 0000000..e522f03 --- /dev/null +++ b/research/type-driven-design/evaluate-repositories.mjs @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, relative, resolve } from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { pathToFileURL } from 'node:url'; +import { dependency } from './dependencies.mjs'; +import { openProject } from './context.mjs'; + +export function evaluate(dotdog, typeFest) { + const temp = mkdtempSync(join(tmpdir(), 'specdog-type-corpus-')); + const corpora = [ + { + name: 'specdog/dotdog', root: resolve(dotdog), + options: { module: 'ESNext', moduleResolution: 'Bundler', exactOptionalPropertyTypes: false }, + targets: [ + ['packages/dotdog/src/workspace/selection.ts', 'selectionFromOptions'], + ['packages/dotdog/src/workspace/types.ts', 'WorkspaceConfig'], + ['packages/dotdog/src/dag/schema.ts', 'RepositoryWorldModel'], + ['packages/dotdog/src/design/audit.ts', 'auditDesign'], + ], + }, + { + name: 'sindresorhus/type-fest', root: resolve(typeFest), + options: { module: 'NodeNext', moduleResolution: 'NodeNext', exactOptionalPropertyTypes: true }, + targets: [ + ['source/simplify.d.ts', 'Simplify'], + ['source/merge.d.ts', 'Merge'], + ['source/jsonify.d.ts', 'Jsonify'], + ['source/tagged.d.ts', 'Tagged'], + ], + }, + ]; + const results = []; + try { + for (const compilerName of ['typescript59', 'typescript60']) { + const ts = dependency(compilerName); + for (const corpus of corpora) { + const configFile = join(temp, `${compilerName}-${results.length}.json`); + writeFileSync(configFile, JSON.stringify({ compilerOptions: { + strict: true, noEmit: true, target: 'ESNext', types: [], skipLibCheck: false, ...corpus.options, + }, files: corpus.targets.map(([file]) => join(corpus.root, file)) })); + const started = performance.now(); + const session = openProject(ts, configFile, { sourceRoot: corpus.root }); + assert.ok(session.extract, `${corpus.name}: no project context`); + const setupMs = performance.now() - started; + const localFiles = session.program.getSourceFiles().filter(file => { + const name = relative(corpus.root, file.fileName); + return !name.startsWith('..') && !name.startsWith('node_modules'); + }); + const targets = corpus.targets.map(([file, name]) => { + const start = performance.now(); + const context = session.extract(join(corpus.root, file), name, { maxDeclarations: 24, maxBytes: 16000, maxDepth: 8 }); + assert.ok(context.declarations?.some(d => d.name === name), `${corpus.name}: failed to resolve ${name}`); + assert.ok(Buffer.byteLength(context.text) <= 16000); + if (session.diagnostics.length) assert.equal(context.status, 'unverified'); + if (context.omitted.length || context.external.length) assert.notEqual(context.status, 'checked'); + return { + name, file, status: context.status, declarationCount: context.declarations.length, + names: context.declarations.map(d => d.name), omitted: context.omitted, external: context.external, + contextBytes: Buffer.byteLength(context.text), + targetFileBytes: Buffer.byteLength(session.program.getSourceFile(join(corpus.root, file)).text), + extractionMs: Math.round((performance.now() - start) * 100) / 100, + }; + }); + results.push({ + repository: corpus.name, + revision: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: corpus.root, encoding: 'utf8' }).trim(), + compiler: ts.version, + scope: 'Targeted research configuration, not the repository full build or test suite', + options: { strict: true, skipLibCheck: false, target: 'ESNext', ...corpus.options }, + programSourceFiles: session.program.getSourceFiles().length, + localSourceFiles: localFiles.length, + localSourceBytes: localFiles.reduce((sum, file) => sum + Buffer.byteLength(file.text), 0), + diagnostics: session.diagnostics, + setupMs: Math.round(setupMs * 100) / 100, + targets, + }); + } + } + return { node: process.version, measurement: 'One local run; bytes are not model tokens; timings are not a benchmark', results }; + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + if (!process.env.DOTDOG_RESEARCH_ROOT || !process.env.TYPE_FEST_RESEARCH_ROOT) throw new Error('Set DOTDOG_RESEARCH_ROOT and TYPE_FEST_RESEARCH_ROOT to read-only checkouts.'); + const result = evaluate(process.env.DOTDOG_RESEARCH_ROOT, process.env.TYPE_FEST_RESEARCH_ROOT); + if (process.argv[2]) writeFileSync(process.argv[2], JSON.stringify(result, null, 2) + '\n'); + console.log(JSON.stringify(result.results.map(r => ({ repository: r.repository, compiler: r.compiler, diagnostics: r.diagnostics, targets: r.targets.map(t => ({ name: t.name, status: t.status, declarations: t.declarationCount, bytes: t.contextBytes, omissions: t.omitted.length, external: t.external.length })) })), null, 2)); +} diff --git a/research/type-driven-design/package-lock.json b/research/type-driven-design/package-lock.json new file mode 100644 index 0000000..b61bb2a --- /dev/null +++ b/research/type-driven-design/package-lock.json @@ -0,0 +1,491 @@ +{ + "name": "specdog-type-design-research", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "specdog-type-design-research", + "devDependencies": { + "fast-check": "4.10.2", + "typescript59": "npm:typescript@5.9.3", + "typescript60": "npm:typescript@6.0.3", + "typescript71": "npm:typescript@7.1.0-beta", + "zod": "4.6.7" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.1.0-beta.tgz", + "integrity": "sha512-DXDG+jIWxBHGdoO70tcHON33rCnSzsEZj9P13oyf/r/KEGIo6awsdSNVNjAw0uMwEpqRu75IrgCcW58YcVkZHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-android-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-android-arm64/-/typescript-android-arm64-7.1.0-beta.tgz", + "integrity": "sha512-UZx6PxoQavuvaVTrSf3aw0Oy/J/uJpRylipA0kzc038jMvZXh0UtE8vnQp20FqtdgD5bhXZ0aJrRJyoTSUO58Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-beta.tgz", + "integrity": "sha512-q7mxblcyX0E7hXsY4k+Q6KWKK5mGmfVq+BDesdTgRVgjodk27aIxER77Zyj9+TgSTlCje8F7+JM6f6hl2BuNdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-beta.tgz", + "integrity": "sha512-D5EYqG5Wm/guw9+0j8+Mn1Jbr/gfS/VtFTuoH8PJtFbcqeDaQGMulc7SW8q51QG6Y3wGOGX3nbsTOw6fIKWWAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.1.0-beta.tgz", + "integrity": "sha512-9A0XX0tcnH9qPdolSKKzSjTb93dlB0UEcAJbHYEsrFX2fkFNSum9+dvkQQJ9vLc+7vxHetDPujq9Qe6wx+Xhow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.1.0-beta.tgz", + "integrity": "sha512-EDf/dBHfY0TxX3NmUTek0hbffeyZlh3+ls6WQW3ICB8vYxwYLFnzg7wkgEUvl9eRlpiJ/nmc2S8arPMzINJ7rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-beta.tgz", + "integrity": "sha512-AvN9HmzOQh46G4312ooSgMHhbB7DtnLwg8D9mo324X49B7weMwtJrBG1MxT9IyI8kzDd7YRtW+1hWGun7WG7Qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-beta.tgz", + "integrity": "sha512-hNqQBgQ5b5oGqN76QqbAU6CwYhJZ6iOcWc2kPTUs2OentgpxZktETziH6KIyRFJPaiBncognwn2ZLu3DNhHiWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.1.0-beta.tgz", + "integrity": "sha512-xbUustlBrFflt2pNEyG5ye+d4WGPc/rfr2xK29MXeobPSw297JqwuKRrvbKM5o3fE/eUM/c+urVm6jQ3IaMVTw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.1.0-beta.tgz", + "integrity": "sha512-fS4/G7/0bXZ3NSq3nBMAXyg65I3tO4S7j08VNQ2g1aNC/qq79gE7Qne1BNjVsjMFfHBVBFjQYoTQCjmVU2iFlg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.1.0-beta.tgz", + "integrity": "sha512-0upCCxYPX8X6gOwkvfv+Ee7/ufyy7k97mTrGE0e5BxLlnUEa+w+rtgnv2dsKcxFdwdeZYVlZKCqgN/xCBV08+A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.1.0-beta.tgz", + "integrity": "sha512-0jKU9YRgOPRgiy5V/UWv37kB53PaTpIbwGCbbOMUPPAkrjVRNOIAJaz7Wa7s3H7mdDebX1LopUEPEJR+7Xa7zg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.1.0-beta.tgz", + "integrity": "sha512-j+d04s2kxaPQh6DLPb01sknAQfzsrdSZy15jBRHyLEZoaaQ19dVjbFRlkNVvYwUG7ZdP8+y5b/7RmPxdtHUKdQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-beta.tgz", + "integrity": "sha512-kcD51ihf+pnI5KT4uQQYG04OpwznaZYBlIQhDhl8fflFd27NktxXsUeguxZyvUMyWfum7IF2bBS6Jv7ox0AgFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.1.0-beta.tgz", + "integrity": "sha512-8W85YyoGK+rgGoSU9jJQUpELbrKcZcbdJk2jPNQ6UKDKI7ryPlPFTrV2jCE5RQuv4Tt96hMDTc33s4YfRrLFBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.1.0-beta.tgz", + "integrity": "sha512-iGsTE0ve2PG0AcyZinwAR0LnGEZhu/y4dceS0hZsYCaKdYETvQtvtlNWlmYKempsBr6C7Il7dMXHVk+VFm9u0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.1.0-beta.tgz", + "integrity": "sha512-Z2FERdHB9fAKI+KGqdWn04O7uNlnB+VYiEm6INE1aZuzzLNd3KBqQ6IpKUJF2ny4GigGU6nVu75XD9tAUODPng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.1.0-beta.tgz", + "integrity": "sha512-NVG3qeyCq/rJEOhaRjR3LsWgjrjo5vJW25omVLeHAVnpKIG4pE4cenwwKGR06DiJ1s0QTOymcJeKXMEpsiNBVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.1.0-beta.tgz", + "integrity": "sha512-ovAdoWXEn2B5/fyWG69pLU3FWmQwPWOjQV5TFbcxtpnOv+R3g8ySGHl1NYRq7Lb/GYdFDvaGcZ+SiU7iKuiMsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-beta.tgz", + "integrity": "sha512-lJbo71IEOfL7ZZIZnArRaOLqjn9BAfoSzyUQn0aSjfSpDZL4ru0afUsdBfo6PQEjD6ssdmi0n/RwaPADX6bsvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-beta.tgz", + "integrity": "sha512-UKcHvbybY2tDTvFW2b1vwiAvDlIRooo69lh8CXWVhOQejcvpM5+IgbiFW9EIwR5Wi5X/v+BdN7gdWbigBuJ3rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/typescript59": { + "name": "typescript", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript60": { + "name": "typescript", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript71": { + "name": "typescript", + "version": "7.1.0-beta", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-beta.tgz", + "integrity": "sha512-cjWjZAJDsmZzsNC5GwHNlW0YPM65jZa5oR0KyQcYX3KSVIfTvtBq//vewISyatKk4OLi5grZjHhIVE9fdrw1UQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.1.0-beta", + "@typescript/typescript-android-arm64": "7.1.0-beta", + "@typescript/typescript-darwin-arm64": "7.1.0-beta", + "@typescript/typescript-darwin-x64": "7.1.0-beta", + "@typescript/typescript-freebsd-arm64": "7.1.0-beta", + "@typescript/typescript-freebsd-x64": "7.1.0-beta", + "@typescript/typescript-linux-arm": "7.1.0-beta", + "@typescript/typescript-linux-arm64": "7.1.0-beta", + "@typescript/typescript-linux-loong64": "7.1.0-beta", + "@typescript/typescript-linux-mips64el": "7.1.0-beta", + "@typescript/typescript-linux-ppc64": "7.1.0-beta", + "@typescript/typescript-linux-riscv64": "7.1.0-beta", + "@typescript/typescript-linux-s390x": "7.1.0-beta", + "@typescript/typescript-linux-x64": "7.1.0-beta", + "@typescript/typescript-netbsd-arm64": "7.1.0-beta", + "@typescript/typescript-netbsd-x64": "7.1.0-beta", + "@typescript/typescript-openbsd-arm64": "7.1.0-beta", + "@typescript/typescript-openbsd-x64": "7.1.0-beta", + "@typescript/typescript-sunos-x64": "7.1.0-beta", + "@typescript/typescript-win32-arm64": "7.1.0-beta", + "@typescript/typescript-win32-x64": "7.1.0-beta" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/research/type-driven-design/package.json b/research/type-driven-design/package.json new file mode 100644 index 0000000..ee03312 --- /dev/null +++ b/research/type-driven-design/package.json @@ -0,0 +1,15 @@ +{ + "name": "specdog-type-design-research", + "private": true, + "type": "module", + "scripts": { + "test": "node --test test/*.test.mjs" + }, + "devDependencies": { + "fast-check": "4.10.2", + "typescript59": "npm:typescript@5.9.3", + "typescript60": "npm:typescript@6.0.3", + "typescript71": "npm:typescript@7.1.0-beta", + "zod": "4.6.7" + } +} diff --git a/research/type-driven-design/render-thesis.py b/research/type-driven-design/render-thesis.py new file mode 100644 index 0000000..1e33a31 --- /dev/null +++ b/research/type-driven-design/render-thesis.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +import argparse +import hashlib +import html +import re +from pathlib import Path + +from markdown_it import MarkdownIt +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import ( + BaseDocTemplate, Flowable, Frame, KeepTogether, PageBreak, PageTemplate, + Paragraph, Preformatted, Spacer, Table, TableStyle, +) +from reportlab.platypus.tableofcontents import TableOfContents + + +ROOT = Path(__file__).resolve().parents[2] +INK = colors.HexColor('#15283A') +ACCENT = colors.HexColor('#166C75') +MUTED = colors.HexColor('#5D6975') +LINE = colors.HexColor('#D9E0E6') +WIDTH = A4[0] - 112 + + +def setup_fonts(directory: Path | None) -> None: + files = { + 'Body': 'Times New Roman.ttf', + 'BodyBold': 'Times New Roman Bold.ttf', + 'BodyItalic': 'Times New Roman Italic.ttf', + 'Sans': 'Arial.ttf', + 'SansBold': 'Arial Bold.ttf', + } + if directory is None: + directory = Path('/System/Library/Fonts/Supplemental') + if all((directory / file).exists() for file in files.values()): + for name, file in files.items(): + pdfmetrics.registerFont(TTFont(name, str(directory / file))) + pdfmetrics.registerFontFamily('Body', normal='Body', bold='BodyBold', italic='BodyItalic', boldItalic='BodyBold') + pdfmetrics.registerFontFamily('Sans', normal='Sans', bold='SansBold', italic='Sans', boldItalic='SansBold') + else: + for name, base in [('Body', 'Times-Roman'), ('BodyBold', 'Times-Bold'), ('BodyItalic', 'Times-Italic'), ('Sans', 'Helvetica'), ('SansBold', 'Helvetica-Bold')]: + pdfmetrics.registerFont(pdfmetrics.Font(name, base, 'WinAnsiEncoding')) + pdfmetrics.registerFontFamily('Body', normal='Body', bold='BodyBold', italic='BodyItalic', boldItalic='BodyBold') + pdfmetrics.registerFontFamily('Sans', normal='Sans', bold='SansBold', italic='Sans', boldItalic='SansBold') + + +def inline(children) -> str: + result = [] + for token in children or []: + if token.type == 'text': + text = html.escape(token.content) + text = re.sub(r'\[(\d+)\]', r'[\1]', text) + result.append(text) + elif token.type == 'code_inline': + result.append(f'{html.escape(token.content)}') + elif token.type in ('strong_open', 'strong_close', 'em_open', 'em_close'): + tag = 'b' if token.type.startswith('strong') else 'i' + result.append(f'' if token.type.endswith('close') else f'<{tag}>') + elif token.type == 'link_open': + result.append(f'') + elif token.type == 'link_close': + result.append('') + elif token.type in ('softbreak', 'hardbreak'): + result.append(' ' if token.type == 'softbreak' else '
') + return ''.join(result) + + +class EvidenceDiagram(Flowable): + def __init__(self, source): + super().__init__() + self.labels = [label for _, label in re.findall(r'(\w+)\[([^\]]+)\]', source)] + self.width = WIDTH + self.height = 153 + + def draw(self): + canvas = self.canv + boxes = [(0, 92), (169, 92), (338, 92), (338, 21), (169, 21), (0, 21)] + box_width = 145 + canvas.setStrokeColor(LINE) + canvas.setFillColor(colors.HexColor('#F1F6F7')) + for (x, y), label in zip(boxes, self.labels): + canvas.roundRect(x, y, box_width, 39, 5, stroke=1, fill=1) + canvas.setFillColor(INK) + canvas.setFont('Sans', 9) + canvas.drawCentredString(x + box_width / 2, y + 15, label) + canvas.setFillColor(colors.HexColor('#F1F6F7')) + canvas.setStrokeColor(ACCENT) + canvas.setFillColor(ACCENT) + for index in range(5): + x1, y1 = boxes[index] + x2, y2 = boxes[index + 1] + if index == 2: + start, end = (x1 + 72.5, y1), (x2 + 72.5, y2 + 39) + triangle = [(end[0] - 3, end[1] + 5), (end[0] + 3, end[1] + 5), end] + elif index < 2: + start, end = (x1 + box_width, y1 + 19.5), (x2, y2 + 19.5) + triangle = [(end[0] - 5, end[1] - 3), (end[0] - 5, end[1] + 3), end] + else: + start, end = (x1, y1 + 19.5), (x2 + box_width, y2 + 19.5) + triangle = [(end[0] + 5, end[1] - 3), (end[0] + 5, end[1] + 3), end] + canvas.line(*start, *end) + path = canvas.beginPath() + path.moveTo(*triangle[0]) + for point in triangle[1:]: + path.lineTo(*point) + path.close() + canvas.drawPath(path, stroke=0, fill=1) + + +class ThesisDocument(BaseDocTemplate): + def afterFlowable(self, flowable): + if isinstance(flowable, Paragraph) and flowable.style.name == 'Chapter': + title = flowable.getPlainText() + key = 'chapter-' + hashlib.sha256(title.encode()).hexdigest()[:12] + self.canv.bookmarkPage(key) + self.canv.addOutlineEntry(title, key, 0) + self.notify('TOCEntry', (0, title, self.page, key)) + + +def build(source: Path, output: Path, font_directory: Path | None) -> None: + setup_fonts(font_directory) + styles = getSampleStyleSheet() + styles.add(ParagraphStyle(name='BodyTextThesis', fontName='Body', fontSize=10.7, leading=15.3, alignment=TA_JUSTIFY, spaceAfter=7, textColor=INK)) + styles.add(ParagraphStyle(name='Chapter', fontName='SansBold', fontSize=18, leading=23, textColor=INK, spaceAfter=18, keepWithNext=True)) + styles.add(ParagraphStyle(name='Subsection', fontName='SansBold', fontSize=11.2, leading=15, textColor=ACCENT, spaceBefore=12, spaceAfter=7, keepWithNext=True)) + styles.add(ParagraphStyle(name='CoverTitle', fontName='SansBold', fontSize=29, leading=35, textColor=INK, spaceAfter=22)) + styles.add(ParagraphStyle(name='CoverSubtitle', fontName='Sans', fontSize=15, leading=21, textColor=ACCENT, spaceAfter=20)) + styles.add(ParagraphStyle(name='Small', fontName='Sans', fontSize=9.2, leading=13, textColor=MUTED, spaceAfter=10)) + styles.add(ParagraphStyle(name='Cell', fontName='Sans', fontSize=8.4, leading=11.4, textColor=INK)) + styles.add(ParagraphStyle(name='CellHead', fontName='SansBold', fontSize=8.6, leading=11.5, textColor=INK)) + styles.add(ParagraphStyle(name='Reference', fontName='Body', fontSize=9.3, leading=13, textColor=INK, spaceAfter=10, splitLongWords=True)) + styles.add(ParagraphStyle(name='CodeThesis', fontName='Courier', fontSize=8, leading=11, backColor=colors.HexColor('#F3F5F7'), borderPadding=10, spaceBefore=6, spaceAfter=10)) + styles.add(ParagraphStyle(name='TOCThesis', fontName='Sans', fontSize=10.2, leading=16, textColor=INK, leftIndent=0, firstLineIndent=0, spaceBefore=5)) + + def page(canvas, doc): + if doc.page == 1: + return + canvas.saveState() + canvas.setFont('Sans', 8) + canvas.setFillColor(MUTED) + canvas.drawString(56, A4[1] - 34, 'DOTDOG / EVIDENCE-LINKED TYPE-DRIVEN DESIGN') + canvas.setStrokeColor(LINE) + canvas.line(56, A4[1] - 43, A4[0] - 56, A4[1] - 43) + canvas.drawString(56, 29, 'Applied research thesis | 31 October 2026') + canvas.drawRightString(A4[0] - 56, 29, str(doc.page)) + canvas.restoreState() + + output.parent.mkdir(parents=True, exist_ok=True) + document = ThesisDocument(str(output), pagesize=A4, leftMargin=56, rightMargin=56, topMargin=65, bottomMargin=50, title='Type-Driven Design as Evidence-Linked Engineering', author='Prepared for Specdog', subject='Applied technical thesis on Dotdog use cases and tested evidence') + frame = Frame(56, 50, WIDTH, A4[1] - 115, leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0) + document.addPageTemplates(PageTemplate(id='thesis', frames=frame, onPage=page)) + story = [Spacer(1, 77), Paragraph('SPECDOG / APPLIED RESEARCH', styles['Small']), Spacer(1, 22), Paragraph('Type-Driven Design
as Evidence-Linked
Engineering', styles['CoverTitle']), Paragraph('A practical thesis for Dotdog', styles['CoverSubtitle']), Spacer(1, 28), Paragraph('How specifications, types, checks, and graphs can support real maintenance workflows without confusing relationships with proof.', styles['BodyTextThesis']), Spacer(1, 34), Paragraph('Prepared for Specdog
Research cutoff: 31 October 2026', styles['Small']), Spacer(1, 20), Paragraph('Technical research, measured prototypes, and a bounded adoption proposal. Not a released Dotdog feature or a peer-reviewed result.', styles['Small']), PageBreak(), Paragraph('Contents', styles['CoverSubtitle'])] + toc = TableOfContents() + toc.levelStyles = [styles['TOCThesis']] + story += [toc] + + tokens = MarkdownIt('commonmark').enable('table').parse(source.read_text()) + active = False + references = False + index = 0 + while index < len(tokens): + token = tokens[index] + if token.type == 'heading_open': + content = tokens[index + 1] + if token.tag == 'h2' and content.content == 'Abstract': + active = True + if active: + if token.tag == 'h2': + story += [PageBreak(), Paragraph(inline(content.children), styles['Chapter'])] + references = content.content == 'References' + else: + story.append(Paragraph(inline(content.children), styles['Subsection'])) + index += 3 + continue + if not active: + index += 1 + continue + if token.type == 'paragraph_open': + content = tokens[index + 1] + if references: + match = re.match(r'\[(\d+)\]\s+(.*?)\s+(https?://\S+)$', content.content) + if not match: + raise ValueError('Malformed reference: ' + content.content) + number, label, url = match.groups() + text = f'[{number}] {html.escape(label)}
{html.escape(url)}' + story.append(Paragraph(text, styles['Reference'])) + else: + story.append(Paragraph(inline(content.children), styles['BodyTextThesis'])) + index += 3 + continue + if token.type in ('bullet_list_open', 'ordered_list_open'): + ordinal = int(token.attrGet('start') or 1) + ordered = token.type == 'ordered_list_open' + index += 1 + while index < len(tokens) and tokens[index].type not in ('bullet_list_close', 'ordered_list_close'): + if tokens[index].type == 'inline': + prefix = f'{ordinal}. ' if ordered else '- ' + story.append(Paragraph(prefix + inline(tokens[index].children), styles['BodyTextThesis'])) + ordinal += 1 + index += 1 + index += 1 + continue + if token.type == 'table_open': + rows, row = [], [] + header = False + index += 1 + while tokens[index].type != 'table_close': + current = tokens[index] + if current.type == 'thead_open': + header = True + elif current.type == 'thead_close': + header = False + elif current.type == 'tr_open': + row = [] + elif current.type == 'inline': + row.append(Paragraph(inline(current.children), styles['CellHead' if header else 'Cell'])) + elif current.type == 'tr_close': + rows.append(row) + index += 1 + widths = [WIDTH / len(rows[0])] * len(rows[0]) + if len(widths) == 3: + widths = [WIDTH * .48, WIDTH * .2, WIDTH * .32] + table = Table(rows, colWidths=widths, repeatRows=1, hAlign='LEFT') + table.setStyle(TableStyle([('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#E9F1F3')), ('LINEBELOW', (0, 0), (-1, 0), .8, ACCENT), ('LINEBELOW', (0, 1), (-1, -1), .35, LINE), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('LEFTPADDING', (0, 0), (-1, -1), 8), ('RIGHTPADDING', (0, 0), (-1, -1), 8), ('TOPPADDING', (0, 0), (-1, -1), 8), ('BOTTOMPADDING', (0, 0), (-1, -1), 8)])) + story += [Spacer(1, 6), table, Spacer(1, 12)] + index += 1 + continue + if token.type == 'fence': + if token.info.strip() == 'mermaid': + story.append(KeepTogether([EvidenceDiagram(token.content), Paragraph('Figure 1. A navigable evidence path. Each arrow records a relationship, not a proof.', styles['Small'])])) + else: + story.append(Preformatted(token.content, styles['CodeThesis'])) + index += 1 + document.multiBuild(story) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--source', type=Path, default=ROOT / 'docs/dotdog-type-driven-design-thesis.md') + parser.add_argument('--output', type=Path, default=ROOT / 'output/pdf/dotdog-type-driven-design-thesis.pdf') + parser.add_argument('--font-dir', type=Path) + args = parser.parse_args() + build(args.source, args.output, args.font_dir) diff --git a/research/type-driven-design/requirements-pdf.txt b/research/type-driven-design/requirements-pdf.txt new file mode 100644 index 0000000..ab00e87 --- /dev/null +++ b/research/type-driven-design/requirements-pdf.txt @@ -0,0 +1,3 @@ +reportlab==5.1.0 +markdown-it-py==4.2.0 +pypdf==6.21.4 diff --git a/research/type-driven-design/results/corpora.json b/research/type-driven-design/results/corpora.json new file mode 100644 index 0000000..cb73c4d --- /dev/null +++ b/research/type-driven-design/results/corpora.json @@ -0,0 +1,642 @@ +{ + "node": "v26.8.1", + "measurement": "One local run; bytes are not model tokens; timings are not a benchmark", + "results": [ + { + "repository": "specdog/dotdog", + "revision": "773dc265ecc30f1ec3cb417d1eb051719ea78ba9", + "compiler": "5.9.3", + "scope": "Targeted research configuration, not the repository full build or test suite", + "options": { + "strict": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "exactOptionalPropertyTypes": false + }, + "programSourceFiles": 90, + "localSourceFiles": 4, + "localSourceBytes": 14705, + "diagnostics": [], + "setupMs": 513.89, + "targets": [ + { + "name": "selectionFromOptions", + "file": "packages/dotdog/src/workspace/selection.ts", + "status": "checked", + "declarationCount": 2, + "names": [ + "selectionFromOptions", + "WorkspaceSelection" + ], + "omitted": [], + "external": [], + "contextBytes": 360, + "targetFileBytes": 1074, + "extractionMs": 2.61 + }, + { + "name": "WorkspaceConfig", + "file": "packages/dotdog/src/workspace/types.ts", + "status": "checked", + "declarationCount": 6, + "names": [ + "WorkspaceConfig", + "WorkspaceIdentity", + "RepoConfig", + "WorkspaceGroup", + "WorkspaceEdge", + "RepoSpecConfig" + ], + "omitted": [], + "external": [], + "contextBytes": 907, + "targetFileBytes": 1793, + "extractionMs": 0.16 + }, + { + "name": "RepositoryWorldModel", + "file": "packages/dotdog/src/dag/schema.ts", + "status": "partial", + "declarationCount": 8, + "names": [ + "RepositoryWorldModel", + "WorldNode", + "WorldEdge", + "WorldPrediction", + "WorldNodeKind", + "GraphOrigin", + "Confidence", + "WorldEdgeVerb" + ], + "omitted": [], + "external": [ + "Record" + ], + "contextBytes": 1783, + "targetFileBytes": 5223, + "extractionMs": 0.22 + }, + { + "name": "auditDesign", + "file": "packages/dotdog/src/design/audit.ts", + "status": "checked", + "declarationCount": 4, + "names": [ + "auditDesign", + "DesignReport", + "DesignFinding", + "DesignSeverity" + ], + "omitted": [], + "external": [], + "contextBytes": 646, + "targetFileBytes": 6615, + "extractionMs": 0.31 + } + ] + }, + { + "repository": "sindresorhus/type-fest", + "revision": "c3e825ffc1e687dd13a0003b771802d95964aaf9", + "compiler": "5.9.3", + "scope": "Targeted research configuration, not the repository full build or test suite", + "options": { + "strict": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "exactOptionalPropertyTypes": true + }, + "programSourceFiles": 157, + "localSourceFiles": 70, + "localSourceBytes": 227070, + "diagnostics": [], + "setupMs": 631.47, + "targets": [ + { + "name": "Simplify", + "file": "source/simplify.d.ts", + "status": "partial", + "declarationCount": 3, + "names": [ + "Simplify", + "BrandedPrimitive", + "Primitive" + ], + "omitted": [], + "external": [ + "Extract" + ], + "contextBytes": 664, + "targetFileBytes": 5943, + "extractionMs": 0.36 + }, + { + "name": "Merge", + "file": "source/merge.d.ts", + "status": "partial", + "declarationCount": 24, + "names": [ + "Merge", + "If", + "IsEqual", + "IsAny", + "CombineTwoArrays", + "_Merge", + "IsNever", + "_IsEqual", + "IsArrayReadonly", + "DropLeading", + "CallSignatureOf", + "MergeObjects", + "UnknownArray", + "SimplifyObject", + "PickIndexSignature", + "KeyAliases", + "OmitIndexSignature", + "NormalizedKeys", + "ToString", + "StringToNumber", + "_StringToNumber", + "UnwrapBrand", + "PositiveInfinity", + "NegativeInfinity" + ], + "omitted": [ + { + "name": "IfNotAnyOrNever", + "reason": "budget" + }, + { + "name": "_UnwrapBrand", + "reason": "budget" + }, + { + "name": "Primitive", + "reason": "budget" + } + ], + "external": [ + "Array", + "NoInfer", + "PropertyKey", + "ReadonlyArray", + "Record", + "Required" + ], + "contextBytes": 5724, + "targetFileBytes": 7423, + "extractionMs": 0.97 + }, + { + "name": "Jsonify", + "file": "source/jsonify.d.ts", + "status": "partial", + "declarationCount": 24, + "names": [ + "Jsonify", + "IsAny", + "PositiveInfinity", + "NegativeInfinity", + "JsonPrimitive", + "JsonValue", + "NormalizeJsonValue", + "WithoutToJson", + "EmptyObject", + "TypedArray", + "NotJsonable", + "UnknownArray", + "JsonifyList", + "UntagTuple", + "JsonifyObject", + "UndefinedToOptional", + "OmittedValueToUndefined", + "ToJsonResult", + "IsUnknown", + "JsonObject", + "JsonArray", + "NormalizeJsonList", + "emptyObjectSymbol", + "FindGlobalType" + ], + "omitted": [ + { + "name": "NeverToNull", + "reason": "budget" + }, + { + "name": "JsonifyListItem", + "reason": "budget" + }, + { + "name": "ContainsItself", + "reason": "budget" + }, + { + "name": "IsArrayOfUnknownLength", + "reason": "budget" + }, + { + "name": "RemoveFlavors", + "reason": "budget" + }, + { + "name": "TagContainer", + "reason": "budget" + }, + { + "name": "UnwrapTagged", + "reason": "budget" + }, + { + "name": "SimplifyObject", + "reason": "budget" + }, + { + "name": "DefinedKeyFilter", + "reason": "budget" + }, + { + "name": "OptionalKeyFilter", + "reason": "budget" + } + ], + "external": [ + "Array", + "ArrayBuffer", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "Boolean", + "DataView", + "Exclude", + "Float32Array", + "Float64Array", + "Function", + "Int16Array", + "Int32Array", + "Int8Array", + "NoInfer", + "Number", + "Omit", + "ReadonlyArray", + "ReadonlyMap", + "ReadonlySet", + "Record", + "RegExp", + "SharedArrayBuffer", + "String", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "tag" + ], + "contextBytes": 7523, + "targetFileBytes": 13123, + "extractionMs": 0.96 + }, + { + "name": "Tagged", + "file": "source/tagged.d.ts", + "status": "partial", + "declarationCount": 3, + "names": [ + "Tagged", + "Tag", + "TagContainer" + ], + "omitted": [], + "external": [ + "PropertyKey", + "tag" + ], + "contextBytes": 348, + "targetFileBytes": 13391, + "extractionMs": 0.08 + } + ] + }, + { + "repository": "specdog/dotdog", + "revision": "773dc265ecc30f1ec3cb417d1eb051719ea78ba9", + "compiler": "6.0.3", + "scope": "Targeted research configuration, not the repository full build or test suite", + "options": { + "strict": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "exactOptionalPropertyTypes": false + }, + "programSourceFiles": 97, + "localSourceFiles": 4, + "localSourceBytes": 14705, + "diagnostics": [], + "setupMs": 505.36, + "targets": [ + { + "name": "selectionFromOptions", + "file": "packages/dotdog/src/workspace/selection.ts", + "status": "checked", + "declarationCount": 2, + "names": [ + "selectionFromOptions", + "WorkspaceSelection" + ], + "omitted": [], + "external": [], + "contextBytes": 360, + "targetFileBytes": 1074, + "extractionMs": 2.48 + }, + { + "name": "WorkspaceConfig", + "file": "packages/dotdog/src/workspace/types.ts", + "status": "checked", + "declarationCount": 6, + "names": [ + "WorkspaceConfig", + "WorkspaceIdentity", + "RepoConfig", + "WorkspaceGroup", + "WorkspaceEdge", + "RepoSpecConfig" + ], + "omitted": [], + "external": [], + "contextBytes": 907, + "targetFileBytes": 1793, + "extractionMs": 0.12 + }, + { + "name": "RepositoryWorldModel", + "file": "packages/dotdog/src/dag/schema.ts", + "status": "partial", + "declarationCount": 8, + "names": [ + "RepositoryWorldModel", + "WorldNode", + "WorldEdge", + "WorldPrediction", + "WorldNodeKind", + "GraphOrigin", + "Confidence", + "WorldEdgeVerb" + ], + "omitted": [], + "external": [ + "Record" + ], + "contextBytes": 1783, + "targetFileBytes": 5223, + "extractionMs": 0.14 + }, + { + "name": "auditDesign", + "file": "packages/dotdog/src/design/audit.ts", + "status": "checked", + "declarationCount": 4, + "names": [ + "auditDesign", + "DesignReport", + "DesignFinding", + "DesignSeverity" + ], + "omitted": [], + "external": [], + "contextBytes": 646, + "targetFileBytes": 6615, + "extractionMs": 0.28 + } + ] + }, + { + "repository": "sindresorhus/type-fest", + "revision": "c3e825ffc1e687dd13a0003b771802d95964aaf9", + "compiler": "6.0.3", + "scope": "Targeted research configuration, not the repository full build or test suite", + "options": { + "strict": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "exactOptionalPropertyTypes": true + }, + "programSourceFiles": 164, + "localSourceFiles": 70, + "localSourceBytes": 227070, + "diagnostics": [], + "setupMs": 592.35, + "targets": [ + { + "name": "Simplify", + "file": "source/simplify.d.ts", + "status": "partial", + "declarationCount": 3, + "names": [ + "Simplify", + "BrandedPrimitive", + "Primitive" + ], + "omitted": [], + "external": [ + "Extract" + ], + "contextBytes": 664, + "targetFileBytes": 5943, + "extractionMs": 0.31 + }, + { + "name": "Merge", + "file": "source/merge.d.ts", + "status": "partial", + "declarationCount": 24, + "names": [ + "Merge", + "If", + "IsEqual", + "IsAny", + "CombineTwoArrays", + "_Merge", + "IsNever", + "_IsEqual", + "IsArrayReadonly", + "DropLeading", + "CallSignatureOf", + "MergeObjects", + "UnknownArray", + "SimplifyObject", + "PickIndexSignature", + "KeyAliases", + "OmitIndexSignature", + "NormalizedKeys", + "ToString", + "StringToNumber", + "_StringToNumber", + "UnwrapBrand", + "PositiveInfinity", + "NegativeInfinity" + ], + "omitted": [ + { + "name": "IfNotAnyOrNever", + "reason": "budget" + }, + { + "name": "_UnwrapBrand", + "reason": "budget" + }, + { + "name": "Primitive", + "reason": "budget" + } + ], + "external": [ + "Array", + "NoInfer", + "PropertyKey", + "ReadonlyArray", + "Record", + "Required" + ], + "contextBytes": 5724, + "targetFileBytes": 7423, + "extractionMs": 0.71 + }, + { + "name": "Jsonify", + "file": "source/jsonify.d.ts", + "status": "partial", + "declarationCount": 24, + "names": [ + "Jsonify", + "IsAny", + "PositiveInfinity", + "NegativeInfinity", + "JsonPrimitive", + "JsonValue", + "NormalizeJsonValue", + "WithoutToJson", + "EmptyObject", + "TypedArray", + "NotJsonable", + "UnknownArray", + "JsonifyList", + "UntagTuple", + "JsonifyObject", + "UndefinedToOptional", + "OmittedValueToUndefined", + "ToJsonResult", + "IsUnknown", + "JsonObject", + "JsonArray", + "NormalizeJsonList", + "emptyObjectSymbol", + "FindGlobalType" + ], + "omitted": [ + { + "name": "NeverToNull", + "reason": "budget" + }, + { + "name": "JsonifyListItem", + "reason": "budget" + }, + { + "name": "ContainsItself", + "reason": "budget" + }, + { + "name": "IsArrayOfUnknownLength", + "reason": "budget" + }, + { + "name": "RemoveFlavors", + "reason": "budget" + }, + { + "name": "TagContainer", + "reason": "budget" + }, + { + "name": "UnwrapTagged", + "reason": "budget" + }, + { + "name": "SimplifyObject", + "reason": "budget" + }, + { + "name": "DefinedKeyFilter", + "reason": "budget" + }, + { + "name": "OptionalKeyFilter", + "reason": "budget" + } + ], + "external": [ + "Array", + "ArrayBuffer", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "Boolean", + "DataView", + "Exclude", + "Float32Array", + "Float64Array", + "Function", + "Int16Array", + "Int32Array", + "Int8Array", + "NoInfer", + "Number", + "Omit", + "ReadonlyArray", + "ReadonlyMap", + "ReadonlySet", + "Record", + "RegExp", + "SharedArrayBuffer", + "String", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "tag" + ], + "contextBytes": 7523, + "targetFileBytes": 13123, + "extractionMs": 0.85 + }, + { + "name": "Tagged", + "file": "source/tagged.d.ts", + "status": "partial", + "declarationCount": 3, + "names": [ + "Tagged", + "Tag", + "TagContainer" + ], + "omitted": [], + "external": [ + "PropertyKey", + "tag" + ], + "contextBytes": 348, + "targetFileBytes": 13391, + "extractionMs": 0.07 + } + ] + } + ] +} diff --git a/research/type-driven-design/results/experiment-notes.json b/research/type-driven-design/results/experiment-notes.json new file mode 100644 index 0000000..2316c4f --- /dev/null +++ b/research/type-driven-design/results/experiment-notes.json @@ -0,0 +1,68 @@ +{ + "initialContextSuite": { + "tests": 16, + "passed": 14, + "failed": 2, + "cause": "Generic type parameter binders were emitted as independent declarations", + "resolution": "Skip type-parameter declaration nodes; retain the enclosing generic signature and its bounds" + }, + "initialModuleFixture": "Negative parser tests initially included unrelated ESM/CJS diagnostics TS1479 and TS1541; fixture configuration was corrected before accepting negative evidence.", + "initialDotdogCorpus": [ + { + "compiler": "5.9.3", + "options": { + "strict": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "NodeNext" + }, + "diagnostics": [ + { + "code": 2375, + "file": "packages/dotdog/src/dag/schema.ts", + "line": 131 + }, + { + "code": 2375, + "file": "packages/dotdog/src/dag/schema.ts", + "line": 146 + }, + { + "code": 2835, + "file": "packages/dotdog/src/workspace/selection.ts", + "line": 1 + } + ] + }, + { + "compiler": "6.0.3", + "options": { + "strict": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": false, + "target": "ESNext", + "module": "NodeNext" + }, + "diagnostics": [ + { + "code": 2375, + "file": "packages/dotdog/src/dag/schema.ts", + "line": 131 + }, + { + "code": 2375, + "file": "packages/dotdog/src/dag/schema.ts", + "line": 146 + }, + { + "code": 2835, + "file": "packages/dotdog/src/workspace/selection.ts", + "line": 1 + } + ] + } + ], + "brandMarkerRegression": "Computed unique-symbol references required their own traversal; a dedicated regression now verifies retention of a local marker declaration.", + "optionalPropertyPolicy": "The remaining optional-field mismatch is TS2379, reproduced on both compiler versions rather than suppressed." +} diff --git a/research/type-driven-design/results/tests.json b/research/type-driven-design/results/tests.json new file mode 100644 index 0000000..05701c3 --- /dev/null +++ b/research/type-driven-design/results/tests.json @@ -0,0 +1,50 @@ +{ + "scope": "Research prototype tests, including explicit reproductions of known production/prototype limitations", + "node": "v26.8.1", + "local": { + "tests": 47, + "pass": 47, + "fail": 0, + "cancelled": 0, + "skipped": 0, + "todo": 0, + "duration_ms": 9136.708042 + }, + "cleanLockfileReplay": { + "tests": 47, + "pass": 47, + "fail": 0, + "cancelled": 0, + "skipped": 0, + "todo": 0, + "duration_ms": 10778.634042 + }, + "generatedInputs": { + "arbitraryJSON": { + "runs": 1000, + "seed": 20261031 + }, + "nestedManifestMutations": { + "runs": 1000, + "seed": 20261101 + }, + "validManifests": { + "runs": 500, + "seed": 20261102 + } + }, + "versions": { + "typescript59": "5.9.3", + "typescript60": "6.0.3", + "typescript71": "7.1.0-beta", + "zod": "4.6.7", + "fast-check": "4.10.2" + }, + "knownLimitationsExercised": [ + "Existing Dotdog validator crashes on malformed nested data", + "Existing Dotdog validator accepts a group missing repos", + "Parser output is not assignable to existing interfaces with exactOptionalPropertyTypes enabled", + "The TypeScript 5/6 context adapter does not implement the 7.1 API", + "Project-reference builds and inferred-signature closure are unsupported" + ] +} diff --git a/research/type-driven-design/results/thesis-tests.json b/research/type-driven-design/results/thesis-tests.json new file mode 100644 index 0000000..6590707 --- /dev/null +++ b/research/type-driven-design/results/thesis-tests.json @@ -0,0 +1,28 @@ +{ + "scope": "47 baseline checks plus 14 additional synthetic graph-evidence cases; not a production graph integration", + "local": { + "tests": 61, + "pass": 61, + "fail": 0, + "skipped": 0, + "cancelled": 0, + "duration_ms": 14425.276667 + }, + "cleanLockfileReplay": { + "tests": 61, + "pass": 61, + "fail": 0, + "skipped": 0, + "cancelled": 0, + "duration_ms": 14186.777584 + }, + "sourceHashes": { + "trace-evidence.mjs": "a92607e61d6c129dc59f9547d482bc9034a2a5a61b660aa54da057362738ba4b", + "test/trace-evidence.test.mjs": "f71162bd8550f1b0c6d374a16ba1d3ba29eb3d25250ff218a052996f04d44015" + }, + "limitations": [ + "The graph links and run records in this experiment are explicit synthetic fixtures.", + "The evaluator checks declared evidence bookkeeping, not semantic rule satisfaction.", + "Previously documented parser/configuration and context-completeness limitations remain." + ] +} diff --git a/research/type-driven-design/test/boundary.test.mjs b/research/type-driven-design/test/boundary.test.mjs new file mode 100644 index 0000000..5ecdd8c --- /dev/null +++ b/research/type-driven-design/test/boundary.test.mjs @@ -0,0 +1,122 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { resolve, join } from 'node:path'; +import { pathToFileURL, fileURLToPath } from 'node:url'; +import { dependency } from '../dependencies.mjs'; +import { parseWorkspace } from '../workspace-parser.ts'; +import { fixture } from './helpers.mjs'; + +const dotdog = process.env.DOTDOG_RESEARCH_ROOT; +if (!dotdog) throw new Error('DOTDOG_RESEARCH_ROOT must identify the read-only Dotdog checkout.'); +const { validateWorkspaceConfig } = await import(pathToFileURL(join(dotdog, 'packages/dotdog/src/workspace/validator.ts'))); +const valid = { version: 1, workspace: { id: 'example-workspace' }, repos: [{ alias: 'example-service', path: '.' }] }; +const parse = input => parseWorkspace(input, validateWorkspaceConfig); + +test('reproduces existing malformed-input crashes in the real validator', () => { + assert.equal(validateWorkspaceConfig(valid).valid, true); + assert.throws(() => validateWorkspaceConfig({ ...valid, repos: [null] }), TypeError); + assert.throws(() => validateWorkspaceConfig({ ...valid, groups: {} }), TypeError); + assert.equal(parse({ ...valid, repos: [null] }).ok, false); + assert.equal(parse({ ...valid, groups: {} }).ok, false); +}); + +test('returns checked data on success and no data on malformed input', () => { + const result = parse(valid); + assert.equal(result.ok, true); + assert.deepEqual(result.data, valid); + for (const input of [null, [], 1, '', {}, { ...valid, repos: [null] }, { ...valid, repos: [1] }, { ...valid, groups: [null] }, { ...valid, groups: {} }, { ...valid, edges: {} }, { ...valid, edges: [null] }, { ...valid, repos: [{ alias: 'example-service', path: 2 }] }]) { + const rejected = parse(input); + assert.equal(rejected.ok, false); + assert.equal('data' in rejected, false); + assert.ok(rejected.errors.length); + } +}); + +test('preserves real duplicate-alias and cross-reference checks', () => { + const duplicate = parse({ ...valid, repos: [...valid.repos, ...valid.repos] }); + assert.equal(duplicate.stage, 'semantic'); + assert.ok(duplicate.errors.some(error => error.code === 'duplicate_repo_alias')); + const unknown = parse({ ...valid, edges: [{ from: 'example-service', to: 'missing', type: 'http' }] }); + assert.equal(unknown.stage, 'semantic'); + assert.ok(unknown.errors.some(error => error.code === 'unknown_edge_to')); +}); + +test('rejects an incomplete group that the existing validator accepts', () => { + const input = { ...valid, groups: [{ name: 'example-group' }] }; + assert.equal(validateWorkspaceConfig(input).valid, true); + assert.equal(parse(input).ok, false); +}); + +test('returns a result for 1000 seeded arbitrary JSON values', () => { + const fc = dependency('fast-check'); + fc.assert(fc.property(fc.jsonValue(), input => { + const result = parse(input); + assert.equal(typeof result.ok, 'boolean'); + if (result.ok) assert.equal(validateWorkspaceConfig(result.data).valid, true); + else assert.ok(result.errors.length); + }), { seed: 20261031, numRuns: 1000 }); +}); + +test('returns a result for 1000 seeded nested-manifest mutations', () => { + const fc = dependency('fast-check'); + fc.assert(fc.property(fc.jsonValue(), fc.constantFrom('repos', 'workspace', 'groups', 'edges'), (value, key) => { + const input = { ...valid, [key]: value }; + const result = parse(input); + assert.equal(typeof result.ok, 'boolean'); + if (result.ok) assert.equal(validateWorkspaceConfig(result.data).valid, true); + }), { seed: 20261101, numRuns: 1000 }); +}); + +test('accepts and returns 500 seeded valid manifests rather than rejecting everything', () => { + const fc = dependency('fast-check'); + const aliases = fc.uniqueArray(fc.stringMatching(/^[a-z][a-z0-9]{0,8}$/), { minLength: 1, maxLength: 6 }); + fc.assert(fc.property(aliases, names => { + const input = { + ...valid, + repos: names.map(alias => ({ alias, path: `./${alias}` })), + groups: [{ name: 'example-group', repos: names }], + edges: [{ from: names[0], to: names.at(-1), type: 'http' }], + }; + const result = parse(input); + assert.equal(result.ok, true); + assert.deepEqual(result.data, input); + }), { seed: 20261102, numRuns: 500 }); +}); + +for (const compiler of ['typescript59', 'typescript60']) { + test(`${compiler}: parser result narrows to the real WorkspaceConfig without a cast`, t => { + const parser = fileURLToPath(new URL('../workspace-parser.ts', import.meta.url)); + const types = resolve(dotdog, 'packages/dotdog/src/workspace/types.ts'); + const project = fixture(t, { + 'package.json': '{"type":"module"}', + 'use.ts': `import { parseWorkspace } from ${JSON.stringify(parser)}; +import type { WorkspaceConfig } from ${JSON.stringify(types)}; +declare const input: unknown; +declare const validate: (value: unknown) => { valid: boolean; errors: [] }; +declare function consume(config: WorkspaceConfig): void; +const result = parseWorkspace(input, validate); +if (result.ok) consume(result.data); +else { const errors = result.errors; } +`, + }, { allowImportingTsExtensions: true, esModuleInterop: true, skipLibCheck: true, module: 'Node16', moduleResolution: 'Node16' }, compiler); + assert.deepEqual(project.open().diagnostics, []); + project.write('bad.ts', `import { parseWorkspace } from ${JSON.stringify(parser)}; declare const validate: (value: unknown) => { valid: boolean; errors: [] }; const result = parseWorkspace(null, validate); result.data;`); + const diagnostics = project.open().diagnostics; + assert.deepEqual(diagnostics.map(d => [d.file, d.code]), [['bad.ts', 2339]]); + }); + + test(`${compiler}: exact optional-property incompatibility remains visible`, t => { + const parser = fileURLToPath(new URL('../workspace-parser.ts', import.meta.url)); + const types = resolve(dotdog, 'packages/dotdog/src/workspace/types.ts'); + const project = fixture(t, { + 'package.json': '{"type":"module"}', + 'use.ts': `import { parseWorkspace } from ${JSON.stringify(parser)}; +import type { WorkspaceConfig } from ${JSON.stringify(types)}; +declare const validate: (value: unknown) => { valid: boolean; errors: [] }; +declare function consume(config: WorkspaceConfig): void; +const result = parseWorkspace(null, validate); +if (result.ok) consume(result.data);`, + }, { allowImportingTsExtensions: true, esModuleInterop: true, skipLibCheck: true, module: 'Node16', moduleResolution: 'Node16', exactOptionalPropertyTypes: true }, compiler); + assert.deepEqual(project.open().diagnostics.map(d => [d.file, d.code]), [['use.ts', 2379]]); + }); +} diff --git a/research/type-driven-design/test/context.test.mjs b/research/type-driven-design/test/context.test.mjs new file mode 100644 index 0000000..fa859a9 --- /dev/null +++ b/research/type-driven-design/test/context.test.mjs @@ -0,0 +1,124 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { join } from 'node:path'; +import { fixture } from './helpers.mjs'; +import { dependency } from '../dependencies.mjs'; +import { openProject } from '../context.mjs'; + +for (const compiler of ['typescript59', 'typescript60']) { + test(`${compiler}: resolves re-exports, dependent unions, and recursive types`, t => { + const project = fixture(t, { + 'model.ts': 'export type State = { kind: "ready"; next?: State } | { kind: "done"; receipt: string }; export type Box = { value: T };', + 'operation.ts': 'import type { State, Box } from "./model"; export function wrap(state: T): Box { return { value: state }; }', + 'index.ts': 'export { wrap as packageState } from "./operation";', + }, {}, compiler); + const context = project.open().extract(join(project.root, 'index.ts'), 'packageState'); + assert.equal(context.status, 'checked'); + assert.deepEqual(context.declarations.map(d => d.name), ['wrap', 'State', 'Box']); + assert.match(context.text, /\(state: T\): Box/); + assert.equal(context.declarations[0].file, 'operation.ts'); + assert.doesNotMatch(context.text, /return \{ value: state \}/); + }); + + test(`${compiler}: retains all overload contracts`, t => { + const project = fixture(t, { 'index.ts': 'export function convert(value: string): number; export function convert(value: number): string; export function convert(value: string | number): string | number { return typeof value === "string" ? value.length : String(value); }' }, {}, compiler); + const context = project.open().extract(join(project.root, 'index.ts'), 'convert'); + assert.equal(context.status, 'checked'); + assert.deepEqual(context.declarations[0].signatures, ['(value: string): number', '(value: number): string']); + }); + + test(`${compiler}: reports missing imports rather than approving context`, t => { + const project = fixture(t, { 'index.ts': 'import type { Missing } from "./absent"; export function run(value: Missing): Missing { return value; }' }, {}, compiler); + const context = project.open().extract(join(project.root, 'index.ts'), 'run'); + assert.equal(context.status, 'unverified'); + assert.ok(context.diagnostics.some(d => d.code === 2307)); + }); + + test(`${compiler}: honors config inheritance and path aliases`, t => { + const project = fixture(t, { + 'base.json': JSON.stringify({ compilerOptions: { strict: true, paths: { '@model': ['./model.ts'] } } }), + 'tsconfig.json': JSON.stringify({ extends: './base.json', compilerOptions: { types: [], noEmit: true }, files: ['index.ts', 'model.ts'] }), + 'model.ts': 'export type Identifier = { readonly id: string };', + 'index.ts': 'import type { Identifier } from "@model"; export function identify(value: Identifier): Identifier { return value; }', + }, {}, compiler); + const context = project.open().extract(join(project.root, 'index.ts'), 'identify'); + assert.equal(context.status, 'checked'); + assert.ok(context.declarations.some(d => d.name === 'Identifier')); + }); + + test(`${compiler}: truncation is explicit and never slices declarations`, t => { + const project = fixture(t, { 'index.ts': 'export interface Value { id: string }; export function identity(value: Value): Value { return value; }' }, {}, compiler); + const context = project.open().extract(join(project.root, 'index.ts'), 'identity', { maxDeclarations: 1 }); + assert.equal(context.status, 'partial'); + assert.equal(context.declarations.length, 1); + assert.deepEqual(context.omitted, [{ name: 'Value', reason: 'budget' }]); + }); + + test(`${compiler}: changing imported types invalidates the evidence`, t => { + const project = fixture(t, { + 'model.ts': 'export type Value = { id: string };', + 'index.ts': 'import type { Value } from "./model"; export function read(value: Value): string { return value.id; }', + }, {}, compiler); + const before = project.open().extract(join(project.root, 'index.ts'), 'read'); + project.write('model.ts', 'export type Value = { id: number };'); + const after = project.open().extract(join(project.root, 'index.ts'), 'read'); + assert.notEqual(before.sourceHash, after.sourceHash); + assert.equal(after.status, 'unverified'); + assert.ok(after.diagnostics.some(d => d.code === 2322)); + }); +} + +test('a solution with project references is explicitly unsupported', t => { + const project = fixture(t, { 'tsconfig.json': '{"files":[],"references":[{"path":"./lib"}]}' }); + assert.equal(project.open().status, 'unsupported'); +}); + +test('malformed configs do not produce checked context', t => { + const project = fixture(t, { 'tsconfig.json': '{ broken' }); + assert.equal(project.open().status, 'invalid-config'); +}); + +test('missing files and exported symbols remain unresolved', t => { + const project = fixture(t, { 'index.ts': 'export type Found = string;' }); + const session = project.open(); + assert.equal(session.extract(join(project.root, 'missing.ts'), 'Found').status, 'missing-file'); + assert.equal(session.extract(join(project.root, 'index.ts'), 'Missing').status, 'missing-export'); +}); + +test('the installed TypeScript 7.1 beta is not claimed compatible', () => { + const info = dependency('typescript71/package.json'); + assert.match(info.version, /^7\.1\./); + assert.equal(openProject({ version: info.version }, 'unused').status, 'unsupported'); +}); + +test('inferred signature dependencies are not claimed fully extracted', t => { + const project = fixture(t, { 'index.ts': 'type Value = { id: string }; declare const value: Value; export function read() { return value; }' }); + const context = project.open().extract(join(project.root, 'index.ts'), 'read'); + assert.equal(context.status, 'partial'); + assert.ok(context.omitted.some(d => d.reason === 'inferred-signature-dependencies')); +}); + +test('classes are not exported with implementation bodies', t => { + const project = fixture(t, { 'index.ts': 'export class Value { id = "body-only"; }' }); + const context = project.open().extract(join(project.root, 'index.ts'), 'Value'); + assert.equal(context.status, 'partial'); + assert.equal(context.text, ''); +}); + +test('context byte budgets include source locations and separators', t => { + const project = fixture(t, { 'index.ts': 'export type Value = { id: string };' }); + const context = project.open().extract(join(project.root, 'index.ts'), 'Value', { maxBytes: 20 }); + assert.equal(context.status, 'partial'); + assert.ok(Buffer.byteLength(context.text) <= 20); +}); + +test('computed unique-symbol keys retain the declaration that establishes brand identity', t => { + const project = fixture(t, { + 'brand.d.ts': 'export declare const marker: unique symbol;', + 'index.ts': 'import { marker } from "./brand"; export type Tagged = string & { readonly [marker]: "tag" };', + }); + const context = project.open().extract(join(project.root, 'index.ts'), 'Tagged'); + assert.equal(context.status, 'checked'); + assert.deepEqual(context.declarations.map(d => d.name), ['Tagged', 'marker']); + assert.match(context.text, /declare const marker: unique symbol/); +}); diff --git a/research/type-driven-design/test/contracts.test.mjs b/research/type-driven-design/test/contracts.test.mjs new file mode 100644 index 0000000..676a8db --- /dev/null +++ b/research/type-driven-design/test/contracts.test.mjs @@ -0,0 +1,80 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { fixture } from './helpers.mjs'; + +const model = ` +declare const brand: unique symbol; +export type OrderId = string & { readonly [brand]: "order" }; +export type UserId = string & { readonly [brand]: "user" }; +export type Draft = { state: "draft"; id: OrderId }; +export type Paid = { state: "paid"; id: OrderId; receipt: string }; +export type Shipped = { state: "shipped"; id: OrderId; receipt: string; tracking: string }; +export type Order = Draft | Paid | Shipped; +export function paidOrderId(order: Paid): OrderId { return order.id; } +export function lookup(id: OrderId): string { return id; } +export function stateLabel(order: Order): string { + switch (order.state) { + case "draft": return "draft"; + case "paid": return "paid"; + case "shipped": return "shipped"; + default: { const exhaustive: never = order; return exhaustive; } + } +} +`; +const valid = `import { type OrderId, type Paid, type Shipped, lookup, paidOrderId, stateLabel } from "./model"; +declare const id: OrderId; +const paid: Paid = { state: "paid", id, receipt: "receipt" }; +const shipped: Shipped = { state: "shipped", id, receipt: "receipt", tracking: "tracking" }; +lookup(paidOrderId(paid)); stateLabel(shipped);`; + +for (const compiler of ['typescript59', 'typescript60']) { + for (const [name, bad, expected, mutate] of [ + ['wrong identifier', 'import { lookup, type UserId } from "./model"; declare const user: UserId; lookup(user);', 2345, source => source.replace('string & { readonly [brand]: "order" }', 'string').replace('string & { readonly [brand]: "user" }', 'string')], + ['wrong state', 'import { paidOrderId, type Draft } from "./model"; declare const draft: Draft; paidOrderId(draft);', 2345, source => source.replace('paidOrderId(order: Paid)', 'paidOrderId(order: Paid | Draft)')], + ['missing state data', 'import { type Shipped, type OrderId } from "./model"; declare const id: OrderId; const shipped: Shipped = { state: "shipped", id, receipt: "receipt" };', 2741, source => source.replace('tracking: string', 'tracking?: string')], + ]) { + test(`${compiler}: ${name} contract detects deliberate weakening`, t => { + const project = fixture(t, { 'model.ts': model, 'valid.ts': valid }, {}, compiler); + assert.deepEqual(project.open().diagnostics, []); + project.write('bad.ts', bad); + const rejected = project.open().diagnostics; + assert.deepEqual(rejected.map(d => [d.file, d.code]), [['bad.ts', expected]]); + project.write('model.ts', mutate(model)); + assert.deepEqual(project.open().diagnostics, []); + }); + } + + test(`${compiler}: adding a state fails until the exhaustive handler is updated`, t => { + const project = fixture(t, { 'model.ts': model, 'valid.ts': valid }, {}, compiler); + assert.deepEqual(project.open().diagnostics, []); + const extraState = model.replace('Draft | Paid | Shipped;', 'Draft | Paid | Shipped | { state: "refunded"; id: OrderId };'); + project.write('model.ts', extraState); + assert.deepEqual(project.open().diagnostics.map(d => [d.file, d.code]), [['model.ts', 2322]]); + project.write('model.ts', extraState.replace('const exhaustive: never = order; return exhaustive;', 'return "unknown";')); + assert.deepEqual(project.open().diagnostics, []); + }); + + test(`${compiler}: unrelated compiler errors cannot satisfy a negative contract`, t => { + const project = fixture(t, { 'model.ts': model, 'valid.ts': valid, 'bad.ts': 'undeclaredValue;' }, {}, compiler); + const diagnostics = project.open().diagnostics; + assert.deepEqual(diagnostics.map(d => d.code), [2304]); + assert.equal(diagnostics.some(d => d.file === 'bad.ts' && d.code === 2345), false); + }); + + test(`${compiler}: disabling null checks weakens the optional-value contract`, t => { + const project = fixture(t, { 'index.ts': 'export function read(value?: string): string { return value; }' }, {}, compiler); + assert.ok(project.open().diagnostics.some(d => d.code === 2322)); + project.write('tsconfig.json', JSON.stringify({ compilerOptions: { strict: false, noEmit: true, types: [] }, files: ['index.ts'] })); + assert.deepEqual(project.open().diagnostics, []); + }); + + test(`${compiler}: unknown is checked but any and assertions can bypass a contract`, t => { + const project = fixture(t, { + 'model.ts': model, + 'unsafe.ts': 'import { paidOrderId, type Paid } from "./model"; declare const input: any; paidOrderId(input); paidOrderId({} as Paid);', + }, {}, compiler); + assert.deepEqual(project.open().diagnostics, []); + project.write('unsafe.ts', 'import { paidOrderId } from "./model"; declare const input: unknown; paidOrderId(input);'); + assert.deepEqual(project.open().diagnostics.map(d => [d.file, d.code]), [['unsafe.ts', 2345]]); + }); +} diff --git a/research/type-driven-design/test/corpora.test.mjs b/research/type-driven-design/test/corpora.test.mjs new file mode 100644 index 0000000..65c55c7 --- /dev/null +++ b/research/type-driven-design/test/corpora.test.mjs @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { evaluate } from '../evaluate-repositories.mjs'; + +test('resolves selected symbols in two real repositories without hiding partial context', () => { + assert.ok(process.env.DOTDOG_RESEARCH_ROOT, 'Set DOTDOG_RESEARCH_ROOT.'); + assert.ok(process.env.TYPE_FEST_RESEARCH_ROOT, 'Set TYPE_FEST_RESEARCH_ROOT.'); + const evaluation = evaluate(process.env.DOTDOG_RESEARCH_ROOT, process.env.TYPE_FEST_RESEARCH_ROOT); + assert.equal(evaluation.results.length, 4); + for (const result of evaluation.results) { + assert.deepEqual(result.diagnostics, []); + assert.equal(result.targets.length, 4); + for (const target of result.targets) { + assert.ok(target.declarationCount > 0); + assert.ok(target.contextBytes <= 16000); + } + } + for (const result of evaluation.results.filter(r => r.repository === 'sindresorhus/type-fest')) { + assert.ok(result.targets.every(target => target.status === 'partial')); + } +}); diff --git a/research/type-driven-design/test/helpers.mjs b/research/type-driven-design/test/helpers.mjs new file mode 100644 index 0000000..c8597db --- /dev/null +++ b/research/type-driven-design/test/helpers.mjs @@ -0,0 +1,21 @@ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { dependency } from '../dependencies.mjs'; +import { openProject } from '../context.mjs'; + +export function fixture(t, files, options = {}, compiler = 'typescript60') { + const root = mkdtempSync(join(tmpdir(), 'specdog-type-test-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const write = (name, text) => { + const file = join(root, name); + mkdirSync(dirname(file), { recursive: true }); + writeFileSync(file, text); + return file; + }; + write('tsconfig.json', JSON.stringify({ compilerOptions: { + strict: true, noEmit: true, target: 'ES2022', module: 'CommonJS', types: [], ...options, + }, include: ['**/*.ts'] })); + for (const [name, text] of Object.entries(files)) write(name, text); + return { root, write, open: () => openProject(dependency(compiler), join(root, 'tsconfig.json')) }; +} diff --git a/research/type-driven-design/test/native-api.test.mjs b/research/type-driven-design/test/native-api.test.mjs new file mode 100644 index 0000000..1c7a44d --- /dev/null +++ b/research/type-driven-design/test/native-api.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { basename, resolve } from 'node:path'; +import { dependency } from '../dependencies.mjs'; + +test('TypeScript 7.1 beta API detects a bad assignment and a repaired snapshot', () => { + const { API } = dependency('typescript71/sync'); + const { ScriptTarget } = dependency('typescript71/ast'); + const { createFileSystemWithLib, createFileSystemLayer } = dependency('typescript71/fs'); + const api = new API(); + const file = resolve('type-research-snapshot.ts'); + let before; + let after; + let program; + let updated; + try { + before = api.createSnapshot({ + fileSystem: createFileSystemWithLib([[file, 'export const count: number = "invalid";']]), + createPrograms: [{ rootFiles: [file], compilerOptions: { strict: true, noEmit: true, target: ScriptTarget.ES2022 } }], + }); + program = before.operation.createdPrograms[0]; + assert.ok(program.getSemanticDiagnostics().some(d => d.code === 2322)); + after = before.update({ + fileSystem: createFileSystemLayer([[file, 'export const count: number = 42;']]), + ensurePrograms: [program.id], + }); + updated = after.getProgram(program.id); + assert.ok(updated); + assert.deepEqual(updated.getSemanticDiagnostics(), []); + assert.ok(program.getSemanticDiagnostics().some(d => d.code === 2322)); + } finally { + updated?.[Symbol.dispose]?.(); + program?.[Symbol.dispose]?.(); + after?.[Symbol.dispose]?.(); + before?.[Symbol.dispose]?.(); + api[Symbol.dispose](); + } +}); + +test('TypeScript 7.1 beta resolves a re-exported operation and detects an imported contract change', () => { + const { API, ModuleKind, ModuleResolutionKind, TypeFormatFlags } = dependency('typescript71/sync'); + const { ScriptTarget } = dependency('typescript71/ast'); + const { createFileSystemWithLib, createFileSystemLayer } = dependency('typescript71/fs'); + const api = new API(); + const model = resolve('native-type-model.ts'), entry = resolve('native-type-entry.ts'), consumer = resolve('native-type-consumer.ts'); + const modelText = 'export type Paid = { state: "paid"; receipt: string }; export function ship(order: Paid): Paid { return order; }'; + let before, after, program, updated; + try { + before = api.createSnapshot({ + fileSystem: createFileSystemWithLib([ + [model, modelText], + [entry, 'export { ship as dispatch } from "./native-type-model";'], + [consumer, 'import { dispatch } from "./native-type-entry"; dispatch({ state: "paid", receipt: "receipt" });'], + ]), + createPrograms: [{ rootFiles: [model, entry, consumer], compilerOptions: { strict: true, noEmit: true, target: ScriptTarget.ES2022, module: ModuleKind.ESNext, moduleResolution: ModuleResolutionKind.Bundler } }], + }); + program = before.operation.createdPrograms[0]; + assert.deepEqual(program.getSemanticDiagnostics(), []); + const checker = program.getProject().checker; + const module = checker.getSymbolOfSourceFile(entry); + assert.ok(module); + const exports = checker.getExportsOfModule(module); + assert.equal(exports.find(symbol => symbol.name === 'missing'), undefined); + const alias = exports.find(symbol => symbol.name === 'dispatch'); + assert.ok(alias); + const operation = checker.getAliasedSymbol(alias); + assert.equal(operation.name, 'ship'); + assert.equal(basename(operation.declarations[0].path), 'native-type-model.ts'); + assert.equal(checker.typeToString(checker.getTypeOfSymbol(operation), undefined, TypeFormatFlags.NoTruncation), '(order: Paid) => Paid'); + after = before.update({ fileSystem: createFileSystemLayer([[model, modelText.replace('receipt: string', 'receipt: number')]]), ensurePrograms: [program.id] }); + updated = after.getProgram(program.id); + assert.deepEqual(updated.getSemanticDiagnostics().map(diagnostic => diagnostic.code), [2322]); + assert.deepEqual(program.getSemanticDiagnostics(), []); + } finally { + updated?.[Symbol.dispose]?.(); + program?.[Symbol.dispose]?.(); + after?.[Symbol.dispose]?.(); + before?.[Symbol.dispose]?.(); + api[Symbol.dispose](); + } +}); diff --git a/research/type-driven-design/test/trace-evidence.test.mjs b/research/type-driven-design/test/trace-evidence.test.mjs new file mode 100644 index 0000000..b134dcc --- /dev/null +++ b/research/type-driven-design/test/trace-evidence.test.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { assessRule } from '../trace-evidence.mjs'; + +function fixture() { + const rule = { id: 'manifest-input', revision: 'rule-1', obligations: ['compile', 'runtime'] }; + const inputs = { 'repo:Workspace': 'type-1', 'repo:parse': 'parser-1', 'repo:compile-test': 'test-1', 'repo:runtime-test': 'test-2' }; + const current = { inputs, compiler: '6.0.3', configuration: 'config-1', completeness: 'complete' }; + const links = [ + { rule: rule.id, obligation: 'compile', check: 'compile', reviewed: true, artifacts: ['repo:Workspace', 'repo:parse', 'repo:compile-test'] }, + { rule: rule.id, obligation: 'runtime', check: 'runtime', reviewed: true, artifacts: ['repo:Workspace', 'repo:parse', 'repo:runtime-test'] }, + ]; + const runs = ['compile', 'runtime'].map(check => ({ check, kind: check, status: 'passed', inputs: { ...inputs }, compiler: current.compiler, configuration: current.configuration, ruleRevision: rule.revision })); + return { rule, links, runs, current }; +} + +test('reviewed current records satisfy the declared evidence checklist, not a semantic proof', () => { + const f = fixture(); + assert.equal(assessRule(f.rule, f.links, f.runs, f.current).status, 'current-evidence'); +}); + +for (const [name, change, expected] of [ + ['compile evidence cannot cover a runtime obligation', f => { f.links = f.links.slice(0, 1); }, 'missing-check'], + ['inferred or unreviewed links do not count as reviewed evidence', f => { f.links[1].reviewed = false; }, 'unreviewed'], + ['a changed parser invalidates old runtime evidence', f => { f.current.inputs['repo:parse'] = 'parser-2'; }, 'stale'], + ['a changed configuration invalidates old runtime evidence', f => { f.current.configuration = 'config-2'; }, 'stale'], + ['a changed compiler invalidates old runtime evidence', f => { f.current.compiler = '7.1.0-beta'; }, 'stale'], + ['a renamed or missing symbol remains unresolved', f => { delete f.current.inputs['repo:parse']; }, 'unresolved-input'], + ['a skipped check is not a pass', f => { f.runs[1].status = 'skipped'; }, 'not-passed'], + ['a failed check is not hidden by a successful check', f => { f.runs[1].status = 'failed'; }, 'check-failed'], + ['changed requirement text invalidates old evidence', f => { f.rule.revision = 'rule-2'; }, 'stale'], + ['a compiler result cannot be relabeled as runtime evidence', f => { f.runs[1].kind = 'compile'; }, 'wrong-check-kind'], + ['ambiguous run history does not silently select an old pass', f => { f.runs.push({ ...f.runs[1], status: 'failed' }); }, 'ambiguous-history'], + ['partial type context cannot support a complete-evidence claim', f => { f.current.completeness = 'partial'; }, 'incomplete-inputs'], + ['unknown dependency completeness cannot support a complete-evidence claim', f => { delete f.current.completeness; }, 'incomplete-inputs'], +]) { + test(name, () => { + const f = fixture(); + change(f); + const result = assessRule(f.rule, f.links, f.runs, f.current); + assert.equal(result.status, 'gap'); + const runtime = result.obligations.find(item => item.obligation === 'runtime'); + assert.equal(runtime.checks?.[0]?.status || runtime.status, expected); + }); +} diff --git a/research/type-driven-design/trace-evidence.mjs b/research/type-driven-design/trace-evidence.mjs new file mode 100644 index 0000000..1d32725 --- /dev/null +++ b/research/type-driven-design/trace-evidence.mjs @@ -0,0 +1,20 @@ +export function assessRule(rule, links, runs, current) { + const obligations = rule.obligations.map(obligation => { + const candidates = links.filter(link => link.rule === rule.id && link.obligation === obligation); + const reviewed = candidates.filter(link => link.reviewed === true); + if (!reviewed.length) return { obligation, status: candidates.length ? 'unreviewed' : 'missing-check' }; + const checks = reviewed.map(link => { + const matches = runs.filter(run => run.check === link.check); + if (matches.length > 1) return { check: link.check, status: 'ambiguous-history' }; + const run = matches[0]; + if (!run) return { check: link.check, status: 'not-run' }; + if (run.kind !== obligation) return { check: link.check, status: 'wrong-check-kind' }; + if (current.completeness !== 'complete') return { check: link.check, status: 'incomplete-inputs' }; + if (!link.artifacts.length || link.artifacts.some(id => !current.inputs[id] || !run.inputs[id])) return { check: link.check, status: 'unresolved-input' }; + if (run.ruleRevision !== rule.revision || run.compiler !== current.compiler || run.configuration !== current.configuration || link.artifacts.some(id => run.inputs[id] !== current.inputs[id])) return { check: link.check, status: 'stale' }; + return { check: link.check, status: run.status === 'passed' ? 'current-evidence' : run.status === 'failed' ? 'check-failed' : 'not-passed' }; + }); + return { obligation, status: checks.every(check => check.status === 'current-evidence') ? 'current-evidence' : 'gap', checks }; + }); + return { rule: rule.id, status: obligations.length && obligations.every(item => item.status === 'current-evidence') ? 'current-evidence' : 'gap', obligations }; +} diff --git a/research/type-driven-design/workspace-parser.ts b/research/type-driven-design/workspace-parser.ts new file mode 100644 index 0000000..9f0db78 --- /dev/null +++ b/research/type-driven-design/workspace-parser.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +const repo = z.object({ + alias: z.string().min(1), + path: z.string().min(1), + role: z.string().optional(), + remote: z.string().optional(), + defaultBranch: z.string().optional(), + specs: z.object({ enabled: z.boolean().optional(), path: z.string().optional() }).optional(), +}); + +export const workspaceSchema = z.object({ + version: z.literal(1), + workspace: z.object({ id: z.string().min(1), name: z.string().optional(), description: z.string().optional() }), + repos: z.array(repo).min(1), + groups: z.array(z.object({ name: z.string().min(1), repos: z.array(z.string()) })).optional(), + edges: z.array(z.object({ from: z.string(), to: z.string(), type: z.string().min(1), label: z.string().optional() })).optional(), +}); + +export type Workspace = z.output; +export type Issue = { code: string; message: string; path?: string }; +export type Result = { ok: true; data: Workspace } | { ok: false; stage: 'shape' | 'semantic'; errors: Issue[] }; +export type SemanticValidator = (input: unknown) => { valid: boolean; errors: Issue[] }; + +export function parseWorkspace(input: unknown, validate: SemanticValidator): Result { + const parsed = workspaceSchema.safeParse(input); + if (!parsed.success) return { + ok: false, + stage: 'shape', + errors: parsed.error.issues.map(issue => ({ code: issue.code, message: issue.message, path: issue.path.map(String).join('.') })), + }; + const checked = validate(parsed.data); + if (!checked.valid) return { ok: false, stage: 'semantic', errors: checked.errors }; + return { ok: true, data: parsed.data }; +}