Commit Graph

4 Commits

Author SHA1 Message Date
PerishCode cba8bf151d chore: align namespace lifecycle packaging 2026-05-14 16:35:46 +08:00
Cursor Agent b8394ae5b8 feat(plugins): patch-edit atomic file writes (Phase 7 safety patch)
Plan Z1 / spec §20.3.

apps/daemon/src/plugins/atoms/patch-edit.ts gains an
`atomicWriteFile(target, body)` helper:

  1. Write body to `<target>.<random>.tmp` (sibling of target).
  2. fs.rename(2) the tmp into place — atomic on POSIX when
     source + target share a filesystem (which they always do
     here because the tmp is sibling).
  3. On failure, best-effort unlink the orphan tmp.

Every fs.writeFile call inside patch-edit now routes through it:
  - file creation (--- /dev/null target)
  - plain in-place edits
  - plan/steps.json status updates
  - plan/receipts/step-<id>.json receipt writes

Effect: a daemon crash mid-write (SIGKILL, ENOSPC, etc.) can no
longer leave the source file truncated — the rename either
happens fully or not at all. The tmp file is the only collateral,
and the orphan-cleanup path keeps even that contained.

The test harness asserts both halves of the contract:
  - happy path leaves NO .tmp siblings on disk
  - context-mismatch rejection leaves the original byte-equal
    (atomicity covers the failure path too)
  - plan/ + plan/receipts/ never accumulate .tmp leftovers

Daemon tests: 1699 \u2192 1702 (+3 cases on
plugins-patch-edit-atomic: clean rename leaves no .tmp, rejected
patch preserves byte-equal source, plan/receipt writes never
leak .tmp siblings).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 16:38:46 +00:00
Cursor Agent bda5c0e3ed feat(plugins): token-map atom impl (Phase 6/7 entry slice)
Plan P1 / spec §10 / §21.3.1.

apps/daemon/src/plugins/atoms/token-map.ts ships the daemon-side
crosswalk behind the SKILL.md fragment landed in §3.M3. Given a
project cwd that already has a token bag (`code/tokens.json` from
design-extract OR `figma/tokens.json` from figma-extract) plus the
active design system's token vocabulary, the runner produces:

  <cwd>/token-map/colors.json
  <cwd>/token-map/typography.json
  <cwd>/token-map/spacing.json
  <cwd>/token-map/radius.json
  <cwd>/token-map/shadow.json
  <cwd>/token-map/unmatched.json     [{ source, sourceName?, kind, reason, hint? }]
  <cwd>/token-map/meta.json          { sourceKind, generatedAt,
                                       atomDigest, designSystemId?,
                                       targetTokenCount,
                                       sourceTokenCount,
                                       matchedTokenCount }

Match strategies (deterministic, first-win):

  1. Exact value match  (#5b8def === #5b8def).
  2. Normalised hex     (#abc → #aabbcc, ignores case).
  3. Fuzzy name         (--color-primary → --ds-color-primary; strips
                         leading -- / ds- / odds- / theme- prefixes
                         and lower-cases).

Anything else lands unmatched[] with one of:

  - 'no-target-equivalent'  no target with same value/name
  - 'target-collision'      multiple sources mapped to one target
                            (first wins; subsequent listed unmatched
                             with hint pointing at the first claimant)
  - 'invalid-source'        malformed source value

Strict mode aborts on any unmatched; soft mode (default) populates
unmatched.json + continues so the human can audit.

Helper:

  parseDesignSystemTokens(body) → DesignSystemToken[]

  Heuristic DESIGN.md token extraction. Lifts CSS custom properties
  (`--ds-color-fg: #111`) and markdown table rows that look like
  token declarations. Daemon callers may pass a hand-curated bag
  instead of relying on this parser.

Existing patch-edit typecheck noise fixed in passing:
  - PatchStepRecord now extends Omit<RewriteStep, 'rationale'> so
    optional override is sound under exactOptionalPropertyTypes.
  - rationale assignment guarded against undefined.

Daemon tests: 1588 → 1600 (+12 cases on plugins-token-map: exact
match, normalised hex, fuzzy name, no-target-equivalent, target-
collision, strict mode abort, multi-kind crosswalk, code+figma
disk inputs round-trip + bucket persistence + missing-input error,
DESIGN.md CSS custom property + markdown table parsers).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:24:22 +00:00
Cursor Agent 561da49019 feat(plugins): patch-edit atom impl (Phase 7 entry slice)
Plan O4 / spec §20.3 / §21.3.2.

apps/daemon/src/plugins/atoms/patch-edit.ts ships the daemon-side
unified-diff applier behind the SKILL.md fragment landed in §3.M4.

Public surface:

  applyPatchForStep({ cwd, stepId, diff, rationale?, allowOutOfStepCreation? })
    → { status: 'completed' | 'failed' | 'skipped',
        filesTouched, added, removed, reason? }

  skipStep({ cwd, stepId, rationale })   — explicit skip path
  readPlanProgress(cwd)                  — { total, terminal } for
                                            the pipeline runner's
                                            until expression
                                            'plan.steps.terminal === plan.steps.total'

Diff parsing supports:

  - plain edits with @@ headers and ' '/-/+ body lines
  - file creation via '--- /dev/null' + '+++ b/<path>'
  - file deletion via '--- a/<path>' + '+++ /dev/null'
  - multiple file hunks in one diff (split on '--- ')

Safety contract enforced before any file is written:

  1. Path traversal guard ('..', leading '/').
  2. Every touched file MUST be in step.files[]. Out-of-step file
     creation requires allowOutOfStepCreation=true (test harness
     escape hatch).
  3. Touching a file classified as 'shell' tier in
     plan/ownership.json requires step.risk='high'. Spec §11.5.1
     boundary: layout.tsx / theme.ts / globals.css / providers.tsx
     can't be edited under risk='low'/'medium'.
  4. Context lines must match exactly; stale patches surface
     'context mismatch at line N' WITHOUT mutating disk.

After a successful apply, the runner:

  - sets plan/steps.json[id].status='completed' + completedAt
  - writes plan/receipts/step-<id>.json with files[], added, removed,
    rationale, completedAt

skipStep() and 'already in terminal state' returns are idempotent
so the pipeline runner can safely re-call after a crash.

Daemon tests: 1571 → 1581 (+10 cases on plugins-patch-edit:
single-file edit + receipt persistence, /dev/null file creation,
/dev/null file deletion, off-step file rejection, shell-tier risk
gate, path-traversal rejection, stale-patch context mismatch
rollback, idempotent terminal-state return, skipStep + receipt,
readPlanProgress aggregate).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 15:14:09 +00:00