@skitterbyte/skitterspec-linear 7.0.2 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,115 +0,0 @@
1
- 'use strict'
2
-
3
- /**
4
- * `pull` — remote → repo, three-way aware.
5
- *
6
- * Applies remote-only fields to the local snapshot; a `both`-owned field where
7
- * both sides moved off base is a real **conflict** and pull refuses (unless
8
- * `--force`, which makes remote win after backing up the local side). On success
9
- * it rewrites the base for the fields it actually reconciled and stamps
10
- * `last_synced_at`. Body fields with no local frontmatter home yet are reported
11
- * as `deferred` and their base is deliberately left pending (not falsely synced).
12
- *
13
- * Pure orchestration over an injected `adapter` (readProject) + injected
14
- * `timestamp`; no clock, no MCP knowledge here (that's mcp.js). Tests drive it
15
- * with a fake in-memory adapter.
16
- */
17
-
18
- const { normalizeLocal, normalizeRemote } = require('./normalize.js')
19
- const { classify } = require('./compare.js')
20
- const { readBase, writeBase, backup } = require('./base.js')
21
- const { writeFrontmatter, applyMilestonesPull, applyTasksPull } = require('./write.js')
22
- const { frontmatterPatchFor } = require('./apply.js')
23
-
24
- // Collect the conflicting units across scalar (field-level) and keyed
25
- // (item-level) fields, as stable labels for the refusal message.
26
- function collectConflicts(fields) {
27
- const out = []
28
- for (const f of fields) {
29
- if (f.keyed) {
30
- for (const it of f.items) if (it.status === 'conflict') out.push(`${f.field}#${it.id}`)
31
- } else if (f.status === 'conflict') {
32
- out.push(f.field)
33
- }
34
- }
35
- return out
36
- }
37
-
38
- async function pull({ dir, snapshotDir, identifier, projectId, adapter, config, force = false, timestamp }) {
39
- const local = normalizeLocal(snapshotDir, config)
40
- const remoteRaw = await adapter.readProject(projectId)
41
- if (!remoteRaw) {
42
- return { ok: false, error: `remote project not found: ${projectId}` }
43
- }
44
- const remote = normalizeRemote(remoteRaw, config)
45
- const base = readBase(dir, identifier, config)
46
- const fields = classify(local, remote, base, config)
47
-
48
- const conflicts = collectConflicts(fields)
49
- if (conflicts.length && !force) {
50
- return {
51
- ok: false,
52
- blocked: true,
53
- reason: 'conflict',
54
- conflicts,
55
- message: `pull refused — ${conflicts.length} unit(s) changed on both sides: ` +
56
- `${conflicts.join(', ')}. Resolve locally or re-run with --force (remote wins).`,
57
- }
58
- }
59
-
60
- // --force overwrites local edits — back the local side up first.
61
- let backupPath = null
62
- if (force) {
63
- backupPath = backup('local', dir, identifier, config, { timestamp, data: local })
64
- }
65
-
66
- // Keyed body fields (e.g. milestones) — apply per-item via the denormalizer,
67
- // which writes/creates the matching phase files. Removals are report-only.
68
- const keyedApplied = []
69
- const keyedCreated = []
70
- const keyedReported = []
71
- for (const f of fields) {
72
- if (!f.keyed) continue
73
- const apply = f.field === 'tasks' ? applyTasksPull : applyMilestonesPull
74
- const res = apply(snapshotDir, f.items)
75
- if (res.applied.length || res.created.length) keyedApplied.push(f.field)
76
- keyedCreated.push(...res.created)
77
- keyedReported.push(...res.reported.map((id) => `${f.field}#${id}`))
78
- }
79
-
80
- // Scalar pull-owned fields → frontmatter (keyed fields handled above).
81
- const scalarPull = fields.filter((f) => f.pullable && !f.keyed)
82
- const fieldValues = {}
83
- for (const f of scalarPull) fieldValues[f.field] = remote[f.field]
84
- const { patch, applied, deferred } = frontmatterPatchFor(fieldValues, config)
85
-
86
- if (applied.length || timestamp) {
87
- writeFrontmatter(snapshotDir, config, { ...patch, last_synced_at: timestamp })
88
- }
89
-
90
- // Re-normalize local so the base reflects the phase-file writes we just made,
91
- // then advance base: scalar-applied fields take the remote value; keyed fields
92
- // take the (now-updated) local value so applied items read in-sync and any
93
- // report-only removal stays pending.
94
- const newLocal = normalizeLocal(snapshotDir, config)
95
- const newBase = { ...newLocal }
96
- for (const field of applied) newBase[field] = remote[field]
97
- newBase.__meta = { updatedAt: remoteRaw.updatedAt || null, syncedAt: timestamp }
98
- const basePath = writeBase(dir, identifier, config, newBase)
99
-
100
- return {
101
- ok: true,
102
- blocked: false,
103
- applied,
104
- deferred,
105
- keyedApplied,
106
- keyedCreated,
107
- keyedReported,
108
- conflictsForced: force ? conflicts : [],
109
- backupPath,
110
- basePath,
111
- pulled: [...scalarPull.map((f) => f.field), ...keyedApplied],
112
- }
113
- }
114
-
115
- module.exports = { pull }