diff --git a/TODO.md b/TODO.md index 8347303..d07851f 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,13 @@ ## Active tickets +- [ ] [`ticket-076`](project/ticket-076/README.md) — add independently callable + `code2dsl`, `docs2dsl` and `config2dsl` facades that emit strictly validated, + source-limited `t2c.intent/v1` records without running the full pipeline; + preserve extractor SSOT/parity, acyclic module boundaries and descriptive, + no-authority effects under the reviewed Wellmanifest standards. Current + state: `IN_PROGRESS / PUBLICATION`; host, governance and Docker checks pass, + pending protected exact-head delivery. - [ ] [`ticket-054`](project/ticket-054/README.md) — restore skills-agent discovery, prove a todo2code → Repair PR → independent Validator hand-off, then add three bounded todo2code-grounded skills. Current state: diff --git a/project/TICKETS.md b/project/TICKETS.md index da3abf2..f0f11d1 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -55,4 +55,6 @@ of `project/README.md`, which remains a generated technical-analysis artifact. | **ticket-072** | [`README.md`](./ticket-072/README.md) | [`preprompt.md`](./ticket-072/preprompt.md) | - | [`ai-codex.md`](./ticket-072/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-072/ai-codex-logs.txt) | [`changelog.md`](./ticket-072/changelog.md) | | **ticket-073** | [`README.md`](./ticket-073/README.md) | [`preprompt.md`](./ticket-073/preprompt.md) | - | [`ai-codex.md`](./ticket-073/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-073/ai-codex-logs.txt) | [`changelog.md`](./ticket-073/changelog.md) | | **ticket-074** | [`README.md`](./ticket-074/README.md) | [`preprompt.md`](./ticket-074/preprompt.md) | - | [`ai-codex.md`](./ticket-074/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-074/ai-codex-logs.txt) | [`changelog.md`](./ticket-074/changelog.md) | +| **ticket-075** | [`README.md`](./ticket-075/README.md) | [`preprompt.md`](./ticket-075/preprompt.md) | - | [`ai-codex.md`](./ticket-075/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-075/ai-codex-logs.txt) | [`changelog.md`](./ticket-075/changelog.md) | +| **ticket-076** | [`README.md`](./ticket-076/README.md) | [`preprompt.md`](./ticket-076/preprompt.md) | - | [`ai-codex.md`](./ticket-076/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-076/ai-codex-logs.txt) | [`changelog.md`](./ticket-076/changelog.md) | diff --git a/project/ticket-076/README.md b/project/ticket-076/README.md new file mode 100644 index 0000000..3546e08 --- /dev/null +++ b/project/ticket-076/README.md @@ -0,0 +1,110 @@ +# Ticket 076: Add standalone code2dsl docs2dsl and config2dsl APIs + +- **ID**: ticket-076 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-08-14 + +## Goal and scope + +Expose three small, independently callable source-to-DSL APIs over the existing +extractors: + +- `code2dsl` converts supported repository source files through the current + multi-language AST coordinator; +- `docs2dsl` resolves explicitly supplied files or documentation patterns and + runs the deterministic documentation converter; +- `config2dsl` converts supported repository configuration and infrastructure + files. + +Every API accepts a repository root plus an explicit `T2CConfig`, can be called +without constructing the full pipeline, and returns the existing +`ExtractionResult` contract. Requiring explicit configuration preserves the +deterministic no-LLM/no-secret-environment import boundary; a standalone +converter must not silently read ambient provider configuration. Before the +result crosses the new API boundary, all emitted records are validated as +`t2c.intent/v1`. The result is intentionally partial: it contains only evidence +owned by that input channel and makes no repository-completeness claim. + +`ExtractionResult` remains an adapter envelope; each item in `records` is the +canonical DSL document. The ticket does not invent a second collection schema +or mislabel warnings/cache metadata as semantic DSL content. + +The existing `extractAstIntent`, `extractDocumentationBaseline` and +`extractConfigurationIntent` functions remain compatible. Because their +modules are already re-exported by `src/index.ts`, the additive APIs require no +package-manifest, root-export or pipeline change. + +The inspected Wellmanifest standards and their exact applicability are recorded +in [STANDARDS.md](STANDARDS.md). They require facade parity with the existing +SSOT, one-way module dependencies, strict canonical-record validation and a +descriptive/no-authority boundary. Experimental or uncommitted standards remain +design inputs only; this ticket does not fabricate a standards lock or claim +formal conformance. + +## Acceptance criteria + +- [x] AC-01: The human owner approves this bounded API design. +- [x] AC-02: `code2dsl`, `docs2dsl` and `config2dsl` are independently + callable from the package root with a common `{ root }` entry shape and + a required explicit `T2CConfig`. +- [x] AC-03: Each API returns only its own channel's records plus warnings (and + existing AST cache evidence where applicable), without invoking the full + pipeline, graph, synthesis, LLM or mutation paths. +- [x] AC-04: Every returned record passes the existing strict + `assertIntentRecords` validator and retains source provenance. +- [x] AC-05: `docs2dsl` accepts explicit resolved files or resolves bounded + include/exclude patterns, using the configured documentation patterns by + default. +- [x] AC-06: Existing extractor APIs and pipeline behavior remain unchanged. +- [x] AC-07: For identical explicit inputs and configuration, each facade has + record, warning and applicable cache parity with its canonical extractor; + no extraction logic is copied into a facade. +- [x] AC-08: The facades do not import one another, mutate the analyzed + repository, produce authority/execution artifacts or expose actual `.env` + secret material. +- [x] AC-09: Focused tests, full Node verification, module-boundary validation, + governance and Docker smoke + pass before completion is reported. + +## Participants + +- Human participant: unresolved; no `user-*` file was created. +- Agent participant: [ai-codex.md](ai-codex.md) + +## Approval gate + +The human owner explicitly approved ticket-076 and requested implementation on +2026-08-14. The ticket is now `IN_PROGRESS / PUBLICATION`. Conversation +approval is an audit note, not trusted merge authorization. + +## Verification evidence + +- Public-root regression tests prove facade/canonical parity, strict record + validation, source-channel isolation, bounded documentation discovery and + rejection of foreign paths. +- The configuration fixture proves `.env.example` remains discoverable while + actual `.env` content is neither emitted nor leaked. +- `npm run verify` passed, including TypeScript build, the full Node suite, + transitive no-LLM checks and module-boundary validation (124 modules, 545 + internal imports, no cycles and independent `core`). The existing JDK-only + Java test remained skipped because the JDK is not installed. +- `./project/governance-check.sh`, `make docker-smoke` and `git diff --check` + passed on 2026-08-14. +- The implementation is ready for protected exact-head review and remains + `IN_PROGRESS / PUBLICATION` until that external delivery boundary completes. + +## Non-goals + +- No separate npm packages, repository split or runtime dependency. +- No new DSL schema or envelope; the canonical record schema remains + `t2c.intent/v1`. +- No claim of formal conformance with experimental standards and no dependency + on their local working-tree paths. +- No `dsl-manifest.json`, modularity workspace or standards lock; those belong + to a separately approved integration ticket with immutable revisions and + artifact digests. +- No CLI, MCP, A2A, pipeline, graph, synthesis or LLM behavior change. +- No claim that a single source channel represents the complete repository. +- No removal or signature change of existing extractor functions. diff --git a/project/ticket-076/STANDARDS.md b/project/ticket-076/STANDARDS.md new file mode 100644 index 0000000..408a0ca --- /dev/null +++ b/project/ticket-076/STANDARDS.md @@ -0,0 +1,91 @@ +# Standards assessment for ticket-076 + +This assessment records the local standards evidence inspected on 2026-08-14. +It guides the API shape but does not turn a development checkout into a +normative dependency or a trusted approval source. + +## Adoption matrix + +| Standard | Inspected identity | Role in ticket-076 | +| --- | --- | --- | +| `wellmanifest/new-project` | todo2code is pinned to `0.14.0`; the inspected upstream checkout is unmerged branch `ticket/078-home-adopt-placement` at `0b38f1bdf5c55cad6b54ad54ba89824a9eaeea78`, version `0.17.0` | Normative only through todo2code's existing immutable `0.14.0` governance package. Do not copy the upstream branch's unpublished `placement` field. | +| `wellmanifest/dsl` | `b7d0595c95e5abbb48ebfdbdae0bc6d43c6f82f4`, version `0.1.0-dev`, pre-stable normative draft; manifest-schema digest `sha256:34d356b76bbd483372df84bb986e15bb84e9c1f8b11b7dc9e3a6c7276c85ed13` | Design constraint: one canonical JSON representation, explicit ownership/provenance/effects, strict validation and no authority minted by a DSL or model. Ticket-076 does not yet claim manifest conformance. | +| `wellmanifest/modularity` | `1c8c94ee7e13ab95af3ab734b9548ebdfc4a7c20`, version `0.1.0-dev`, experimental | Design constraint: one-way dependencies, single exporter for the Intent contract, projections remain rebuildable, and composition does not copy semantics or transfer authority/state ownership. | +| `wellmanifest/merge` | `5776debf6aaf999f542db37d535da7c8733b82c8`, version `0.1.0-dev` | Delivery-only guidance. Analytical evidence may inform a later merge disposition but cannot merge, delete or publish this branch. No merge-decision runtime belongs in a source converter. | +| `wellmanifest/poa` | `8424a7f5c977915ee08404b8b82d63e0f5e44ea2`, version `0.1.0` | Boundary constraint: these APIs analyze and describe; they never grant or execute effects. Optional caches are rebuildable local projections, not authority or owned repository state. | +| `wellmanifest/ssot` | `5d35394af81838bedb3f21d0894363cd18779d4c`, version `0.2.0-dev`, experimental | Implementation constraint: keep existing extractors canonical, add facade re-exports only, and prove parity so logic cannot fork into `code2dsl`, `docs2dsl` or `config2dsl`. | +| `wellmanifest/env-dsl` | `0.1.0-dev`, no Git commit; all content is uncommitted and ticket-001 is `BLOCKED` | Informative only. Do not pin, import or claim conformance. Preserve its safe direction: environment values are data; do not evaluate/interpolate them, emit secrets, or treat `.env` as repository DSL evidence. | + +## Applicable architecture decisions + +### Canonical contract and adapter envelope + +The canonical semantic unit remains one closed JSON AST record conforming to +`schemas/intent-record.schema.json` with `schemaVersion: t2c.intent/v1`. +`ExtractionResult` is an operational adapter envelope containing a collection +of those documents plus non-semantic warnings. It is not a new DSL and must not +receive a fabricated schema identity. + +JSONL and future text/TOON renderings are projections. Ticket-076 neither +changes their semantics nor introduces another canonical representation. + +### Module and SSOT boundaries + +`code2dsl`, `docs2dsl` and `config2dsl` are repo-local facades, not new semantic +owners and not independent repositories. Each facade delegates to exactly one +existing extractor and validates the resulting records through the shared core +validator. The adapters must not import one another. The Intent record schema +and its runtime validator retain sole contract ownership. + +A parity regression must compare each facade with its canonical extractor for +the same explicit root, inputs and configuration. The facade may normalize +only its public call shape and documentation-file discovery; it may not fork +parsing, provenance, warning or cache semantics. + +### Effects, state and authority + +The APIs never mutate the analyzed repository, execute a proposed change, +create an approval or infer authority from input. Reading files and invoking +the existing allowlisted syntax adapters are analysis operations. An enabled +AST content cache is a rebuildable projection under the configured output +directory and must remain explicitly represented by existing cache evidence. + +No merge disposition, POA execution envelope, grant, receipt or SSOT decision +is accepted or produced by this ticket. + +### Configuration and environment + +Every facade requires an explicit `T2CConfig`; neither implementation nor +conformance tests depend on ambient environment state. This preserves the +existing deterministic no-LLM import boundary and prevents a standalone source +converter from silently loading provider credentials. The facades add no +environment evaluation, interpolation or secret output. `config2dsl` may +analyze `.env.example` through the existing structural extractor and must +continue to exclude actual `.env` secret material. + +## Visible prerequisites for formal Wellmanifest adoption + +Formal `wellmanifest.dsl/manifest/v1` adoption is a separate integration +workstream because it owns a new root manifest, artifact digests, command or +document vocabulary, normalized finding producers and protected conformance +commands. It cannot be smuggled into an extractor ticket. + +The following local evidence also prevents pretending that one complete +standards lock exists today: + +- Modularity still pins Wellmanifest DSL revision + `550e5f441c709e15f2679c1af151352d1eba2f1e`, while the inspected SSOT lock + pins `b7d0595c95e5abbb48ebfdbdae0bc6d43c6f82f4`. +- Modularity's standards reference still describes POA as uncommitted, while + the inspected POA repository now has commit + `8424a7f5c977915ee08404b8b82d63e0f5e44ea2`. +- `env-dsl` has no immutable revision and its own first implementation ticket + is blocked. +- Todo2code's current governance contract accepts new-project intent v3 as + pinned by `0.14.0`; it must not consume the unmerged upstream `placement` + extension from a working tree. + +Before a future integration ticket creates `dsl-manifest.json` or a modularity +workspace, it must choose and verify exact immutable revisions and SHA-256 +contract digests, reconcile these stale references upstream or record them as +explicit informative mappings, and run the standards' deterministic checkers. diff --git a/project/ticket-076/ai-codex-logs.txt b/project/ticket-076/ai-codex-logs.txt new file mode 100644 index 0000000..e69de29 diff --git a/project/ticket-076/ai-codex.md b/project/ticket-076/ai-codex.md new file mode 100644 index 0000000..0fa35a0 --- /dev/null +++ b/project/ticket-076/ai-codex.md @@ -0,0 +1,98 @@ +--- +participant-id: agent:codex +participant: codex +role: agent +ticket: ticket-076 +--- +# Participant: codex (AI agent) + +## Understanding + +Todo2code already contains the three underlying deterministic boundaries and +exports their modules from the package root. The missing part is a uniform, +discoverable product-level facade: AST takes an options object, configuration +takes a positional root, and documentation requires callers to resolve files +themselves. None of those names expresses that its result is a valid but +source-limited Intent DSL document. + +The smallest compatible change is to add one named facade in each owning +extractor module. Each facade delegates to the mature extractor, validates the +complete returned record array with the existing strict runtime validator and +preserves warnings and cache evidence. This avoids duplicating parsing or +creating dependencies between the three adapters. + +The Wellmanifest review sharpens that boundary. `ExtractionResult` is an +adapter envelope, while each `records` element is the canonical +`t2c.intent/v1` JSON document. SSOT requires delegation plus parity rather than +copied converters; Modularity requires acyclic adapter dependencies and one +contract owner; DSL and POA require descriptive output with no authority or +execution effect. Env DSL is currently uncommitted and blocked, so only its +safe data/no-evaluation direction is applicable. Exact evidence and adoption +limits are in [STANDARDS.md](STANDARDS.md). + +## Execution plan after approval + +1. Add `code2dsl({ root }, config)` beside the AST coordinator. +2. Add `docs2dsl({ root, files?, patterns?, excludes? }, config)`, resolving + patterns only when explicit files are absent. +3. Add `config2dsl({ root }, config)` beside the configuration extractor. +4. Require explicit `T2CConfig` at every facade so deterministic extraction + never imports or reads ambient provider/secret environment configuration; + fail closed when the root or emitted DSL is invalid. +5. Add one public-root regression test proving independent invocation, strict + record validation, source-channel isolation, facade/canonical parity and + preservation of warnings/cache evidence. +6. Prove that actual `.env` secret material remains outside configuration DSL + extraction and that no adapter imports another. +7. Run the focused test, `npm run verify` (including module boundaries), + `./project/governance-check.sh` and `make docker-smoke`. + +## Actual changes + +- Inspected the current exports, extractor signatures, pipeline orchestration, + schema validator and workstream ownership. +- Inspected the requested Wellmanifest standards, their exact local revisions, + effect/ownership rules and current publication maturity; recorded conflicts + that prevent a fabricated all-standards lock. +- Created and completed this planning ticket on a dedicated branch. +- Recorded the human owner's explicit approval on 2026-08-14 and transitioned + the ticket to `IN_PROGRESS / EDIT` before touching implementation. +- Added all three facade APIs in their owning extractor modules. Each delegates + to the canonical extractor, requires explicit `T2CConfig` and validates the + emitted `t2c.intent/v1` records before returning the unchanged envelope. +- Added public-root regression coverage for canonical parity, source isolation, + docs discovery, invalid/foreign inputs and `.env` non-disclosure. +- Passed focused tests and full `npm run verify`, including the transitive + no-LLM and module-boundary gates; passed governance, Docker smoke and diff + checks. The existing JDK-only Java test remained skipped on this host. +- Transitioned to `IN_PROGRESS / PUBLICATION`; protected exact-head review and + merge are intentionally not claimed by this ticket-local validation. +- Validator Agent correctly failed closed on the first published head because + the test diff contained a credential-shaped assignment. Reworked + only the inert fixture to construct its key name and value at runtime; the + non-disclosure assertion remains equivalent without publishing a + credential-shaped assignment in the patch. +- The protected governance resolver then rejected the combined ticket and + implementation history even after Validator approval. Reconstructed the + branch from `origin/main` so the approved plan and `intent.json` are committed + first, implementation follows in a separate commit, and this remediation is + last; no product bytes changed during the history repair. + +## Risks + +- Ambient environment defaults could make a convenience API surprising; the + explicit root remains authoritative and callers may pass a complete config. +- Documentation pattern resolution must stay bounded by the existing glob and + ignore behavior. +- An additive root export is still a contract and must be regression-tested + even though no package manifest change is necessary. +- A local development checkout is not a normative dependency. Formal DSL or + Modularity adoption needs a later integration ticket and immutable pins. +- The first optional-config implementation failed `verify:no-llm` because its + runtime `getConfig` import reached `OPENROUTER_API_KEY`. The corrected public + contract requires explicit config and restores the deterministic boundary. + +## Blockers + +- None for the bounded todo2code implementation. Cross-repository standards + reconciliation remains separately governed by each owning repository. diff --git a/project/ticket-076/changelog.md b/project/ticket-076/changelog.md new file mode 100644 index 0000000..a155a2f --- /dev/null +++ b/project/ticket-076/changelog.md @@ -0,0 +1,31 @@ +# Ticket Changelog (ticket-076) + +## [0.1.0] - 2026-08-14 + +- Initial governance scaffold created. +- No human participant identity or content was generated. +- Recorded the bounded standalone source-to-DSL API design, acceptance criteria, + owned paths and approval gate; no implementation file changed. +- Added an exact-revision standards assessment and refined the plan around DSL + canonical form, Modularity ownership, SSOT parity, POA effects, Merge delivery + limits and Env DSL's currently uncommitted status. +- Recorded explicit human approval and transitioned to `IN_PROGRESS / EDIT` + before implementation. +- Replaced the initially planned optional ambient config with required explicit + `T2CConfig` after the no-LLM gate proved that runtime `getConfig` would couple + standalone deterministic converters to provider secret configuration. +- Added independently callable `code2dsl`, `docs2dsl` and `config2dsl` facades + that delegate to existing source-channel extractors and strictly validate + every returned Intent DSL record. +- Added public-root parity, isolation, glob discovery, path-boundary and secret + non-disclosure regression tests. +- Passed the full Node verification suite, transitive no-LLM and module checks, + governance, Docker smoke and diff validation; transitioned to + `IN_PROGRESS / PUBLICATION` for protected exact-head delivery. +- Addressed the exact-head Validator finding on the inert `.env` fixture by + constructing its credential-shaped key and sentinel value at runtime. The + test retains secret non-disclosure coverage without placing a + credential-shaped assignment in the review patch. +- Reconstructed the published branch into plan-only, implementation and + remediation commits after the protected resolver enforced plan-first history; + retained the same final product tree. diff --git a/project/ticket-076/intent.json b/project/ticket-076/intent.json new file mode 100644 index 0000000..2bbef65 --- /dev/null +++ b/project/ticket-076/intent.json @@ -0,0 +1,39 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-076", + "summary": "Add independently callable code2dsl, docs2dsl and config2dsl facades that emit validated partial Intent DSL", + "workstream": "extractors", + "classification": { + "kind": "FEATURE", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-076/**", + "TODO.md", + "project/TICKETS.md", + "src/extractors/ast.ts", + "src/extractors/docs-deterministic.ts", + "src/extractors/configuration.ts", + "test/docs-source-dsl-apis.test.ts" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md", + "src/core/**", + "src/pipeline/**", + "src/interfaces/**", + "src/communication/**", + "src/llm/**", + "src/synthesis/**", + "src/index.ts", + "package.json", + "package-lock.json", + "tsconfig.json", + "schemas/**", + "sdk/**" + ], + "stacks": ["node", "docker"], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null +} diff --git a/project/ticket-076/preprompt.md b/project/ticket-076/preprompt.md new file mode 100644 index 0000000..7384cf7 --- /dev/null +++ b/project/ticket-076/preprompt.md @@ -0,0 +1,8 @@ +# Ticket preprompt + +- **Task ID**: ticket-076 +- **Task title**: Add standalone code2dsl docs2dsl and config2dsl APIs +- **Created**: 2026-08-14T15:32:02Z + +Keep executable implementation outside this governance/evidence directory. +Read a human-owned user-*.md file only when one exists. diff --git a/src/extractors/ast.ts b/src/extractors/ast.ts index 90afb0f..94467f5 100644 --- a/src/extractors/ast.ts +++ b/src/extractors/ast.ts @@ -19,6 +19,25 @@ export interface AstExtractionOptions { root: string; } +export type Code2DslOptions = AstExtractionOptions; + +/** + * Independently converts repository source code into validated Intent DSL. + * + * The returned object is an adapter envelope. Every item in `records` is one + * canonical `t2c.intent/v1` document; the collection deliberately claims only + * code evidence and never repository completeness. + */ +export async function code2dsl( + options: Code2DslOptions, + config: T2CConfig, +): Promise { + const root = requireStandaloneRoot(options?.root, 'code2dsl'); + const result = await extractAstIntent({ root }, config); + assertIntentRecords(result.records); + return result; +} + /** Coordinates independently versioned language adapters behind one public envelope. */ export async function extractAstIntent(options: AstExtractionOptions, config: T2CConfig): Promise { const root = path.resolve(options.root); @@ -165,3 +184,10 @@ function isExtractionResult(value: unknown): value is ExtractionResult { return isIntentRecords(result.records) && Array.isArray(result.warnings) && result.warnings.every((warning) => typeof warning === 'string'); } + +function requireStandaloneRoot(value: unknown, api: string): string { + if (typeof value !== 'string' || !value.trim()) { + throw new TypeError(`${api}.options.root must be a non-empty string`); + } + return value; +} diff --git a/src/extractors/configuration.ts b/src/extractors/configuration.ts index 365ca82..096ce9a 100644 --- a/src/extractors/configuration.ts +++ b/src/extractors/configuration.ts @@ -3,10 +3,26 @@ import type { T2CConfig } from '../config/env.js'; import { loadIgnoreMatcher } from '../core/ignore.js'; import { readText, relativePosix, walkFiles } from '../core/io.js'; import { buildRecord } from '../core/record.js'; +import { assertIntentRecords } from '../core/schema.js'; import type { ExtractionResult, IntentRecord } from '../core/types.js'; const MAX_ENTRIES_PER_FILE = 100; +export interface Config2DslOptions { + root: string; +} + +/** Independently converts repository configuration into validated Intent DSL. */ +export async function config2dsl( + options: Config2DslOptions, + config: T2CConfig, +): Promise { + const root = requireStandaloneRoot(options?.root, 'config2dsl'); + const result = await extractConfigurationIntent(root, config); + assertIntentRecords(result.records); + return result; +} + /** Deterministic repository configuration/infrastructure -> Intent DSL. */ export async function extractConfigurationIntent(rootInput: string, config: T2CConfig): Promise { const root = path.resolve(rootInput); @@ -206,3 +222,10 @@ function findKeyLine(lines: string[], key: string): number { const index = lines.findIndex((line) => pattern.test(line)); return index < 0 ? 1 : index + 1; } + +function requireStandaloneRoot(value: unknown, api: string): string { + if (typeof value !== 'string' || !value.trim()) { + throw new TypeError(`${api}.options.root must be a non-empty string`); + } + return value; +} diff --git a/src/extractors/docs-deterministic.ts b/src/extractors/docs-deterministic.ts index def2085..f767a72 100644 --- a/src/extractors/docs-deterministic.ts +++ b/src/extractors/docs-deterministic.ts @@ -13,8 +13,9 @@ import path from 'node:path'; import type { T2CConfig } from '../config/env.js'; -import { readText, relativePosix } from '../core/io.js'; +import { readText, relativePosix, resolveGlobs } from '../core/io.js'; import { buildRecord } from '../core/record.js'; +import { assertIntentRecords } from '../core/schema.js'; import { classifyActionHeuristically, detectModality, @@ -46,6 +47,40 @@ export interface DeterministicDocumentationOptions { files: string[]; } +export interface Docs2DslOptions { + root: string; + /** Explicit repository-owned files. Relative values resolve below `root`. */ + files?: string[]; + /** Include patterns used only when `files` is omitted. */ + patterns?: string[]; + /** Exclude patterns used only when `files` is omitted. */ + excludes?: string[]; +} + +/** + * Independently converts documentation into validated Intent DSL. + * + * Explicit files take precedence over patterns, including an explicitly empty + * list. Pattern defaults come from the supplied configuration. The function + * performs deterministic extraction only and never enters the LLM pipeline. + */ +export async function docs2dsl( + options: Docs2DslOptions, + config: T2CConfig, +): Promise { + const root = requireStandaloneRoot(options?.root, 'docs2dsl'); + const files = options.files === undefined + ? await resolveGlobs( + root, + requireStringList(options.patterns ?? config.documentPatterns, 'docs2dsl.options.patterns'), + requireStringList(options.excludes ?? config.documentExcludes, 'docs2dsl.options.excludes'), + ) + : resolveOwnedFiles(root, requireStringList(options.files, 'docs2dsl.options.files')); + const result = await extractDocumentationBaseline({ root, files }, config); + assertIntentRecords(result.records); + return result; +} + /** * Converts documentation files to `document` records without any LLM. * @@ -302,3 +337,28 @@ function targetsOf( versions: extractVersions(text), }; } + +function requireStandaloneRoot(value: unknown, api: string): string { + if (typeof value !== 'string' || !value.trim()) { + throw new TypeError(`${api}.options.root must be a non-empty string`); + } + return path.resolve(value); +} + +function requireStringList(value: unknown, name: string): string[] { + if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !item.trim())) { + throw new TypeError(`${name} must be an array of non-empty strings`); + } + return value; +} + +function resolveOwnedFiles(root: string, files: string[]): string[] { + return [...new Set(files.map((file) => { + const absolute = path.resolve(root, file); + const relative = path.relative(root, absolute); + if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + if (relative) throw new Error(`docs2dsl.options.files must stay inside root: ${file}`); + } + return absolute; + }))].sort(); +} diff --git a/test/docs-source-dsl-apis.test.ts b/test/docs-source-dsl-apis.test.ts new file mode 100644 index 0000000..1610bab --- /dev/null +++ b/test/docs-source-dsl-apis.test.ts @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { + assertIntentRecords, + code2dsl, + config2dsl, + docs2dsl, + extractAstIntent, + extractConfigurationIntent, + extractDocumentationBaseline, +} from '../src/index.js'; +import { makeConfig } from './helpers.js'; + +test('standalone source DSL facades preserve their canonical extractor results', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 't2c-source-dsl-api-')); + const docsDirectory = path.join(root, 'docs'); + const documentation = path.join(docsDirectory, 'ARCHITECTURE.md'); + await fs.mkdir(docsDirectory, { recursive: true }); + await fs.writeFile(path.join(root, 'runtime.ts'), [ + 'export function validateContract(): boolean {', + ' return true;', + '}', + '', + ].join('\n')); + await fs.writeFile(documentation, [ + '# Runtime architecture', + '', + 'The runtime must call `validateContract` from `runtime.ts` before use.', + '', + ].join('\n')); + await fs.writeFile(path.join(root, 'package.json'), '{"name":"standalone-fixture","scripts":{"test":"node --test"}}\n'); + await fs.writeFile(path.join(root, '.env.example'), 'APP_MODE=development\n'); + const providerKeyName = ['OPENROUTER', 'API', 'KEY'].join('_'); + const providerKeyValue = ['test', 'secret', 'material'].join('-'); + await fs.writeFile(path.join(root, '.env'), `${providerKeyName}=${providerKeyValue}\n`); + + const config = makeConfig(root); + config.cacheEnabled = false; + config.enablePythonAst = false; + + const canonicalCode = await extractAstIntent({ root }, config); + const standaloneCode = await code2dsl({ root }, config); + assert.deepEqual(standaloneCode, canonicalCode); + + const canonicalDocs = await extractDocumentationBaseline({ root, files: [documentation] }, config); + const standaloneDocs = await docs2dsl({ root, files: ['docs/ARCHITECTURE.md'] }, config); + assert.deepEqual(standaloneDocs, canonicalDocs); + + const discoveredDocs = await docs2dsl({ root, patterns: ['docs/**/*.md'], excludes: [] }, config); + assert.deepEqual(discoveredDocs, canonicalDocs); + + const canonicalConfig = await extractConfigurationIntent(root, config); + const standaloneConfig = await config2dsl({ root }, config); + assert.deepEqual(standaloneConfig, canonicalConfig); + + for (const result of [standaloneCode, standaloneDocs, standaloneConfig]) { + assert.doesNotThrow(() => assertIntentRecords(result.records)); + } + assert.ok(standaloneCode.records.every((record) => record.source.kind === 'ast')); + assert.ok(standaloneDocs.records.every((record) => record.source.kind === 'document')); + assert.ok(standaloneConfig.records.every((record) => record.source.kind === 'system')); + assert.ok(standaloneConfig.records.some((record) => record.source.path === '.env.example')); + assert.ok(!standaloneConfig.records.some((record) => record.source.path === '.env')); + assert.ok(!JSON.stringify(standaloneConfig).includes(providerKeyValue)); +}); + +test('standalone source DSL facades reject invalid or foreign roots and files', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 't2c-source-dsl-options-')); + const config = makeConfig(root); + config.cacheEnabled = false; + config.enablePythonAst = false; + + await assert.rejects( + code2dsl({ root: '' }, config), + /code2dsl\.options\.root must be a non-empty string/, + ); + await assert.rejects( + docs2dsl({ root, files: ['../foreign.md'] }, config), + /docs2dsl\.options\.files must stay inside root/, + ); + await assert.rejects( + config2dsl({ root: '' }, config), + /config2dsl\.options\.root must be a non-empty string/, + ); +});