feat(cli): migrate ASK workspace to .ask/ + introduce ask.lock + Zod-validated I/O#3
Merged
Merged
Conversation
ASK CLI를 .ask/ 워크스페이스로 이전하고 ask.lock + Zod 스키마 기반의 타입세이프 결정적 직렬화를 도입하기 위한 트랙. 새로 머지된 add-docs/ setup-docs/sync-docs 스킬이 전제하는 레이아웃과 CLI 동작을 정합 맞춤. - spec.md: FR-1~6, SC-1~5, risks, out-of-scope 정리 - plan.md: 15개 task, Zod schemas → paths → lockfile → sync → legacy migration 순의 점진 전략. validator 통과 - workspace skeleton: state/, tracks/active|completed/, investigations/, research/, templates/, scripts/ 디렉터리와 .gitkeep 추가
Discriminated union on source field for SourceConfig (github/npm/web/ llms-txt) and LockEntry. Wraps each in ConfigSchema and LockSchema with schemaVersion/lockfileVersion literal pinning to 1. - contentHash regex enforces sha256-<hex> - npm integrity regex permits sha256/384/512 SRI hashes - github commit field optional, hex 40 when present - generatedAt/fetchedAt require ISO-8601 datetime 17 unit tests cover valid/invalid cases for each variant.
sortedJSON recursively sorts object keys (arrays preserve order), emits 2-space indent + trailing newline. Output is byte-stable across runs with shuffled input. contentHash computes SHA-256 over files sorted by relpath, framed as <relpath>\0<bytes>\0 so path/content boundaries cannot collide. Returns sha256-<64hex>. 11 unit tests cover key sorting, determinism, primitives, hash order-independence, content sensitivity, and path/content disambiguation.
readConfig/writeConfig/readLock/writeLock validate via Zod, write through sortedJSON for byte-stable output, and sort docs[] by name. Default empty values returned when files do not exist. Invalid input throws without writing. Paths: - .ask/config.json - .ask/ask.lock 10 unit tests cover round-trip, byte-identity, sorting, and validation errors using a temp directory per test.
getDocsDir now returns <project>/.ask/docs instead of .please/docs. saveDocs and listDocs follow automatically through the helper. Adds storage.test.ts asserting the path change and round-trip behavior for write + replace.
config.ts now delegates to readConfig/writeConfig from io.ts. Hand-rolled JSON.parse/stringify removed. addDocEntry matches by name only (versions change over time and we keep one entry per library). schemas.ts: WebSourceConfig.maxDepth changed from .default(1) to optional to match the existing sources/index.ts interface — runtime supplies the default.
agents.ts builds the marker block from getLibraryDocsDir, which already returns .ask/docs/... after T004 — no source change needed. Test pins the behavior so future regressions in the storage path or marker logic are caught. 5 tests cover empty state, marker block content, surrounding-content preservation, CLAUDE.md creation, and CLAUDE.md idempotency.
GithubSource.fetch now resolves the ref to a commit sha via
`git ls-remote` and returns it through FetchResult.meta. The dereferenced
tag commit is preferred when ls-remote returns both ref and ^{} forms.
If git is unavailable or the ref cannot be resolved, commit is left
undefined — the lockfile records the absence rather than guessing.
FetchResult gains an optional meta field carrying source-specific
metadata (commit, ref, integrity, tarball, urls) so all four sources
can populate ask.lock through a uniform shape.
NpmSource.fetch now calls `npm view <spec> dist.integrity` and returns the SRI hash through FetchResult.meta.integrity alongside the tarball URL. The lookup is wrapped in try/catch — older registries may not expose the field, in which case the lock leaves it undefined.
After saveDocs, the add command now builds a LockEntry from the source config + FetchResult.meta + computed contentHash and calls upsertLockEntry. The lock entry includes: - version, fetchedAt, fileCount, contentHash for every source - repo/ref/commit for github (commit omitted when ls-remote fails) - tarball/integrity for npm (from dist.integrity) - urls for web, url for llms-txt upsertLockEntry only updates generatedAt when the entry actually changed (ignoring fetchedAt), so back-to-back identical add runs leave the lock byte-stable. Adds removeLockEntries for the upcoming sync work. Also corrects the consola message from .please/config.json to .ask/config.json.
sync now reads .ask/ask.lock and re-fetches each entry. Per entry: 1. Run the source adapter to get current files + meta. 2. Compute the new contentHash and compare to the lockfile entry. 3. If unchanged → skip (logs 'unchanged'), preserving disk state. 4. If drifted → save new docs, delete the old version dir (only after the new fetch succeeded), update config, upsert lockfile, regenerate skill. AGENTS.md is regenerated once at the end. Final summary reports drifted/unchanged/failed counts so the user knows exactly what moved.
migrate-legacy.ts moves .please/docs/ → .ask/docs/ and parses .please/config.json through Zod, rewriting it as .ask/config.json through the deterministic writer. The presence of .ask/ is the idempotency sentinel — once migrated, the function returns early. Hooked into all four subcommands (add, sync, list, remove) at the top of run() so any invocation triggers migration once. ask.lock is not migrated — it does not exist in the legacy layout and will be populated by the next add or sync run. README.md and ARCHITECTURE.md updated to reference .ask/ paths.
Asserts that write→read→write→read→write produces byte-identical lock files across 30 github entries. This catches any future regression in sorted-key serialization or in the read/write helper round-trip.
- migrate-legacy.test.ts: 5 tests covering empty state, .ask sentinel, doc move, config rewrite via writeConfig (sorted by name), and exactly-once execution against a tampered file - lock-pipeline.test.ts: 6 tests exercising the add+sync wiring at the helper level (saveDocs + addDocEntry + upsertLockEntry round-trip, SC-4 byte-stable re-add, content drift updates generatedAt, SC-5 version bump, removeLockEntries prune) - migrate-legacy.ts now uses writeConfig instead of writing JSON directly so the migrated config is sorted and validated through the same pipeline as a fresh write - github.ts moves the whitespace regex to module scope (e18e/prefer- static-regex) - lint:fix reordered imports across all touched files 58 tests pass, lint clean, build clean.
All 15 tasks complete: 58 unit tests passing, lint clean, build clean. Status moves to 'implemented'; phase moves to 'review'.
Critical fixes (from review-code-reviewer + silent-failure-hunter + type-analyzer agents): 1. npm integrity: schema field made optional and buildLockEntry omits when undefined instead of coercing to '' (which crashes Zod validation downstream). Matches the existing github.commit pattern. 2. npm tarball: throw early in buildLockEntry if missing rather than coercing to '' and producing an opaque Zod URL error far from the real cause. 3. Command injection: github resolveCommit now uses execFileSync to bypass the shell — branch/tag are user-supplied. Schema also adds a GitRefField regex constraint on branch/tag in GithubSourceConfig. 4. Silent failure: github resolveCommit now logs a consola.warn with the underlying error before returning undefined, so users notice when commit pinning silently fails. 5. JSON.parse error wrapping: io.ts readConfig/readLock wrap parse failures with the file path and a recovery hint instead of bubbling the raw SyntaxError. 6. Sync ordering: drift branch in syncCmd now runs every write that can throw (saveDocs, addDocEntry, upsertLockEntry) BEFORE the destructive removeDocs. A mid-flow failure leaves both directories on disk with config still pointing at the old version — recoverable. 7. Migration safety: migrate-legacy.ts parses+validates the legacy config first (in memory), then writes the new config (which becomes the idempotency sentinel — .ask/config.json, not bare .ask/), then moves docs, then removes legacy. Failures throw rather than silently leaving a half-migrated workspace. Type-safety fixes: 8. SourceConfig duplication: sources/index.ts now re-exports SourceConfig from schemas.ts and derives the per-source aliases via Extract. The Zod schema is the single source of truth for runtime invariants — callers can no longer construct a value that type-checks but fails Zod (e.g. urls: [] for web). 9. getSource is typed: Record<SourceKind, DocSource> instead of Record<string, DocSource>, so adding a fifth source variant is a compile error until the registry map is updated. migrate-legacy.test.ts updated to use the new sentinel (.ask/config.json instead of bare .ask/). 58 tests pass, lint clean, build clean.
Findings from /simplify code-reuse + quality + efficiency review:
Reuse:
- storage.ts now calls getAskDir() from io.ts instead of hardcoding
'.ask' as a string literal
- migrate-legacy.ts uses getAskDir() and getConfigPath() helpers,
drops the duplicate NEW_DIR constant
- contentHash now accepts DocFile-like input ({ relpath, content })
directly so callers don't have to repeat the TextEncoder adapter
- index.ts buildLockEntry uses the FetchResult type instead of an
inline structural duplicate
Quality:
- config.ts drops dead 're-export getConfigPath' (unused outside io.ts)
- io.ts EMPTY_CONFIG/EMPTY_LOCK constants removed — readConfig/readLock
now return fresh literals directly (clearer than spread+override)
- migrate-legacy.ts strips redundant 'Step 1/2/3/4' inline narration
comments — the function-level jsdoc already enumerates the same flow
with rationale
Efficiency:
- upsertLockEntry early-returns on no-op writes, preserving the lock
file's mtime for build caches and file watchers
- contentHash hoists TextEncoder and NUL separator to module scope
(allocated once per module instead of per file)
- consola messages use path.relative(getConfigPath/getLockPath) so the
'.ask/config.json' / '.ask/ask.lock' string literals live in one place
Skipped (out of scope for this loop):
- Parallelizing github+npm fetches in syncCmd — significant code change,
separate track
- Per-entry batched lock writes in syncCmd — sync already short-circuits
unchanged entries via continue, so the win is small
- readJson<T>/writeJson<T> generic helpers — only 2 callers each,
premature DRY
58 tests pass, lint clean, build clean.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying ask-registry with
|
| Latest commit: |
962ceed
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://867e18eb.ask-registry.pages.dev |
| Branch Preview URL: | https://feat-ask-workspace-migration.ask-registry.pages.dev |
This was referenced Apr 8, 2026
Merged
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Three new agent skills (
add-docs,setup-docs,sync-docs) shipped infeat/add-docs-skillsdescribe a workspace layout the CLI did not yet implement: storage path.ask/docs/, config file.ask/config.json, and a new.ask/ask.lockrecording resolved versions, content hashes, and source-specific metadata. Without this PR, the skills and the CLI describe two different worlds.This PR closes the gap by migrating the CLI to
.ask/, introducing a Zod-validated lockfile, and routing all config/lock I/O through deterministic, type-safe helpers.Track:
.please/docs/tracks/completed/ask-workspace-migration-20260407/What changed
New modules
packages/cli/src/schemas.ts— Zod discriminated unions forSourceConfig,Config,LockEntry,Lock. Single source of truth for runtime invariants. Schema versions pinned to v1.packages/cli/src/io.ts—readConfig/writeConfig/readLock/writeLock/upsertLockEntry+sortedJSON(deterministic serializer) +contentHash(SHA-256 over framed file list).packages/cli/src/migrate-legacy.ts— one-shot.please/→.ask/migration with.ask/config.jsonas the idempotency sentinel.Pipeline changes
storage.ts—.please/docs/→.ask/docs/config.ts— pass-through wrapper aroundio.tshelpersagents.ts— auto-generated marker block now references.ask/docs/sources/index.ts—SourceConfigconsolidated toschemas.tsviaExtract<>;getSourcetyped toSourceKindsources/github.ts— exposes commit sha viagit ls-remote(usesexecFileSyncto bypass shell). Logs warning on resolution failure.sources/npm.ts— exposesdist.integrityvia the existingnpm viewcallindex.ts—addcommand now records to.ask/ask.lock;synccommand uses the lock as the drift baseline; legacy migration runs at the top of every subcommand.Tests
Documentation
README.md—.ask/pathsARCHITECTURE.md—.ask/pathsMigration safety
Existing projects with
.please/docs/are migrated automatically on the next CLI invocation:.please/config.jsonin memory and validate via Zod.ask/config.json(becomes the idempotency sentinel).please/docs/*into.ask/docs/Failures throw rather than silently leaving a half-migrated workspace. Re-running is safe — the function short-circuits when
.ask/config.jsonexists.Review history
/review:code-reviewiteration 1: 6 critical + 3 important issues found by review-code-reviewer + silent-failure-hunter + type-analyzer agents in parallel/review:code-reviewiteration 2: all 9 fixes verified, no new issues introduced/simplify: 11 of 13 findings applied (-21 net lines, -30%)Verification Checklist
Automated Tests
ConfigSchema.parse()accepts valid github/npm/web/llms-txt configs and rejects each with one required field missingwriteConfigfollowed byreadConfigfollowed bywriteConfigproduces byte-identical outputcontentHashis order-independent (shuffling the input file array yields the same hash)ask docs add <pkg>end-to-end creates.ask/docs/,.ask/config.json,.ask/ask.lock, and updatesAGENTS.mdmarker blockask docs syncafter a simulated version bump re-fetches only the changed entry, deletes the old version dir, and updates the lock.please/docs/foo@1.0.0/and.please/config.jsonget moved into.ask/, second run is silentManual Testing
/tmp/test-fresh/directory with onlypackage.json, runnode packages/cli/dist/index.js docs add zodand inspect.ask/.please/docs/populated, run anydocssubcommand, confirm migration runs once and never again.ask/config.jsonand confirm the next CLI run fails with a clear Zod error pointing at the offending fieldAcceptance Criteria Check
.ask/docs/,.ask/config.json,.ask/ask.lock,AGENTS.mdreferencing.ask/, no.please/docs/.please/project migrated on next run, deprecation warning printed once, second run silentaddruns leave config and lock byte-identical exceptgeneratedAt/fetchedAtonly updating when content changessyncafterbun update zodre-fetches only zod, deletes old dir after new fetch succeeds, updates lockQuality gates
bun run --cwd packages/cli test)bun run --cwd packages/cli build)bun run --cwd packages/cli lint)Follow-up