@skitterbyte/skitterspec-linear 7.0.2 → 8.0.1
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.
- package/README.md +23 -25
- package/assets/core/SETUP.md +46 -51
- package/assets/core/linear.config.json.example +6 -7
- package/assets/core/linear.config.md +47 -77
- package/assets/rules/spec-planning.md +9 -7
- package/assets/skills/spec/SKILL.md +2 -2
- package/assets/skills/spec-go/SKILL.md +16 -18
- package/assets/skills/spec-push/SKILL.md +53 -47
- package/assets/skills/spec-status/SKILL.md +31 -26
- package/package.json +2 -2
- package/src/init.js +25 -0
- package/src/vendor/linear/cli-sanitise.js +0 -0
- package/src/vendor/linear/cli-sync.js +123 -204
- package/src/vendor/linear/config.js +18 -14
- package/src/vendor/sync-core/index.js +23 -19
- package/src/vendor/sync-core/src/base.js +8 -10
- package/src/vendor/sync-core/src/compare.js +83 -174
- package/src/vendor/sync-core/src/normalize.js +84 -80
- package/src/vendor/sync-core/src/push.js +39 -133
- package/src/vendor/sync-core/src/sanitise.js +4 -8
- package/src/vendor/sync-core/src/task-block.js +13 -1
- package/src/vendor/sync-core/src/write.js +15 -205
- package/assets/skills/spec-pull/SKILL.md +0 -49
- package/src/vendor/sync-core/src/apply.js +0 -66
- package/src/vendor/sync-core/src/pull.js +0 -115
|
@@ -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 }
|