trackfw 6.10.0 → 7.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.
@@ -0,0 +1,194 @@
1
+ 'use strict'
2
+
3
+ // Node port of the D9 extension point from internal/integrations/render.go
4
+ // (ThirdPartyReference, UpsertThirdPartyReference, ApplyThirdPartyReferences,
5
+ // NormalizeThirdPartyContent, ResolveThirdPartySkillDestination) — see the
6
+ // D9 amendment in
7
+ // docs/adr/ADR-2026-08-15-gate-de-duas-fases-para-artefatos-de-terceiro-quarentena-parecer-vinculado-por-checksum-e-deteccao-por-proveniencia-versionada.md.
8
+ //
9
+ // D9 note (see docs/agents-working-context.md, ML-1A entry): this file
10
+ // persists .trackfw/thirdparty-references.json, an artifact NOT mentioned
11
+ // by the original ADR text (D1-D8) — it exists because a plain rendered
12
+ // catalog agent file cannot simply have a reference block appended once:
13
+ // the next `trackfw agents update` calls buildPlans -> render again, which
14
+ // re-derives content straight from the catalog asset and knows nothing
15
+ // about the appended block. Persisting the reference here and having
16
+ // buildPlans call applyThirdPartyReferences after every render (see
17
+ // ../integrations/index.js) makes the canonical render reproduce the
18
+ // block, so repeated `agents update` runs settle at state "current"
19
+ // instead of fighting the third-party attachment.
20
+
21
+ const fs = require('node:fs')
22
+ const path = require('node:path')
23
+
24
+ const { atomicWrite } = require('./quarantine')
25
+ const { target: catalogTarget, surfaceFor } = require('../integrations/catalog')
26
+
27
+ const THIRDPARTY_REFERENCES_SCHEMA_VERSION = 1
28
+
29
+ // truncateBeforeIdSegment drops the "{{id}}"-bearing path segment and
30
+ // everything after it, returning the shared parent directory. Duplicated
31
+ // (not imported) from npm/src/integrations/catalog.js — that module does
32
+ // not export it, and this ML's file list does not include editing
33
+ // catalog.js; this is the same small, pure, low-risk duplication already
34
+ // used by the Go reference itself (see markers.go's doc comment on
35
+ // Checksum) rather than widening catalog.js's public surface for one
36
+ // caller.
37
+ function truncateBeforeIdSegment(p) {
38
+ const segments = p.split('/')
39
+ const index = segments.findIndex(segment => segment.includes('{{id}}'))
40
+ if (index === -1) return p
41
+ return segments.slice(0, index).join('/')
42
+ }
43
+
44
+ function thirdPartyReferencesPath(root) {
45
+ return path.join(root, '.trackfw', 'thirdparty-references.json')
46
+ }
47
+
48
+ function emptyRegistry() {
49
+ return { schema_version: THIRDPARTY_REFERENCES_SCHEMA_VERSION, entries: {} }
50
+ }
51
+
52
+ function loadThirdPartyReferenceRegistry(root) {
53
+ const filename = thirdPartyReferencesPath(root)
54
+ let data
55
+ try {
56
+ data = fs.readFileSync(filename, 'utf8')
57
+ } catch (err) {
58
+ if (err.code === 'ENOENT') return emptyRegistry()
59
+ throw new Error(`read thirdparty reference registry: ${err.message}`)
60
+ }
61
+ let reg
62
+ try {
63
+ reg = JSON.parse(data)
64
+ } catch (err) {
65
+ throw new Error(`decode thirdparty reference registry: ${err.message}`)
66
+ }
67
+ if (reg.schema_version !== THIRDPARTY_REFERENCES_SCHEMA_VERSION) {
68
+ throw new Error(`unsupported thirdparty reference registry schema ${reg.schema_version}`)
69
+ }
70
+ if (!reg.entries || Array.isArray(reg.entries)) reg.entries = {}
71
+ return reg
72
+ }
73
+
74
+ function writeThirdPartyReferenceRegistry(root, reg) {
75
+ const toWrite = { schema_version: THIRDPARTY_REFERENCES_SCHEMA_VERSION, entries: reg.entries || {} }
76
+ const data = `${JSON.stringify(toWrite, null, 2)}\n`
77
+ atomicWrite(thirdPartyReferencesPath(root), data, 0o600)
78
+ }
79
+
80
+ // upsertThirdPartyReference records (or replaces, keyed by ref.slug) a
81
+ // reference from the rendered catalog agent artifact (targetID,
82
+ // agentItemID) to a third-party skill, persisted under root (project root
83
+ // — D4). Idempotent: calling it twice with the same slug replaces the
84
+ // prior entry instead of duplicating it. Mirrors
85
+ // internal/integrations/render.go:UpsertThirdPartyReference.
86
+ function upsertThirdPartyReference(root, targetID, agentItemID, ref) {
87
+ const reg = loadThirdPartyReferenceRegistry(root)
88
+ const key = `${targetID}/${agentItemID}`
89
+ const refs = reg.entries[key] || []
90
+ let replaced = false
91
+ for (let i = 0; i < refs.length; i++) {
92
+ if (refs[i].slug === ref.slug) {
93
+ refs[i] = ref
94
+ replaced = true
95
+ break
96
+ }
97
+ }
98
+ if (!replaced) refs.push(ref)
99
+ refs.sort((a, b) => a.slug.localeCompare(b.slug))
100
+ reg.entries[key] = refs
101
+ writeThirdPartyReferenceRegistry(root, reg)
102
+ }
103
+
104
+ // THIRD_PARTY_REF_START/END are the composition markers (D5), dedicated and
105
+ // distinct from the auxiliary-rules-file markers (a different subsystem).
106
+ // Mirrors internal/integrations/render.go:thirdPartyRefStart/thirdPartyRefEnd.
107
+ const THIRD_PARTY_REF_START = '<!-- trackfw:thirdparty-skills:start -->'
108
+ const THIRD_PARTY_REF_END = '<!-- trackfw:thirdparty-skills:end -->'
109
+
110
+ // applyThirdPartyReferences injects or updates the third-party reference
111
+ // block in content for (targetID, agentItemID), based on entries persisted
112
+ // by upsertThirdPartyReference. When root is empty or there are no entries
113
+ // for this key, content is returned byte-for-byte unchanged — this is the
114
+ // guarantee that every agent artifact which never received a third-party
115
+ // attachment renders exactly as it did before D5/D9 were introduced.
116
+ // Mirrors internal/integrations/render.go:ApplyThirdPartyReferences.
117
+ function applyThirdPartyReferences(root, content, targetID, agentItemID) {
118
+ if (!root) return content
119
+ const reg = loadThirdPartyReferenceRegistry(root)
120
+ const refs = reg.entries[`${targetID}/${agentItemID}`] || []
121
+ if (!refs.length) return content
122
+
123
+ let block = `${THIRD_PARTY_REF_START}\n`
124
+ for (const ref of refs) {
125
+ block += `- Third-party skill "${ref.slug}": ${ref.destination} (source: ${ref.url})\n`
126
+ }
127
+ block += THIRD_PARTY_REF_END
128
+
129
+ let text = content
130
+ const start = text.indexOf(THIRD_PARTY_REF_START)
131
+ if (start === -1) {
132
+ if (!text.endsWith('\n')) text += '\n'
133
+ text += `\n${block}\n`
134
+ return text
135
+ }
136
+ // Search for the end marker starting at start, not from the beginning of
137
+ // text (hefesto-tf finding, ML-4C): searching the whole text could find
138
+ // an END marker that appears BEFORE start, producing end < start and
139
+ // silently corrupting the composed output. Anchoring the search at start
140
+ // makes that impossible: end is either -1 or >= start.
141
+ const relEnd = text.slice(start).indexOf(THIRD_PARTY_REF_END)
142
+ if (relEnd === -1) {
143
+ // Malformed (start without end): append a fresh block rather than
144
+ // guess at repair.
145
+ text += `\n${block}\n`
146
+ return text
147
+ }
148
+ const end = start + relEnd
149
+ const newText = text.slice(0, start) + block + text.slice(end + THIRD_PARTY_REF_END.length)
150
+ return `${newText.replace(/\n+$/, '')}\n`
151
+ }
152
+
153
+ // normalizeThirdPartyContent applies the same normalization used for
154
+ // managed catalog skill content (trim + single trailing newline) to raw
155
+ // third-party bytes before they are written through IntegrationManager, so
156
+ // third-party artifacts are stored with the same on-disk convention as
157
+ // catalog artifacts. Mirrors
158
+ // internal/integrations/render.go:NormalizeThirdPartyContent.
159
+ function normalizeThirdPartyContent(content) {
160
+ const text = Buffer.isBuffer(content) ? content.toString('utf8') : String(content)
161
+ return `${text.trim()}\n`
162
+ }
163
+
164
+ // resolveThirdPartySkillDestination computes where a third-party artifact's
165
+ // content should live for (targetID, scope), per D5: the shared parent
166
+ // directory of the target's own canonical project/global Skills install
167
+ // path template, followed by "/thirdparty/<slug>.md". The directory always
168
+ // comes from the catalog's own path template — never a hardcoded
169
+ // ".claude" — so every target the catalog declares a Skills capability for
170
+ // is supported without per-target special-casing. Mirrors
171
+ // internal/integrations/render.go:ResolveThirdPartySkillDestination.
172
+ function resolveThirdPartySkillDestination(targetID, scope, slug) {
173
+ let targetEntry
174
+ try {
175
+ targetEntry = catalogTarget(targetID)
176
+ } catch {
177
+ throw new Error(`unknown target "${targetID}"`)
178
+ }
179
+ const surface = surfaceFor(targetEntry, undefined, 'skills')
180
+ const installPath = (surface.paths.skills || []).find(entry => entry.scope === scope)
181
+ if (!installPath) throw new Error(`target ${targetID} surface ${surface.id} has no ${scope} skills path`)
182
+ const baseDir = truncateBeforeIdSegment(installPath.path)
183
+ return { destination: `${baseDir}/thirdparty/${slug}.md`, surfaceID: surface.id }
184
+ }
185
+
186
+ module.exports = {
187
+ thirdPartyReferencesPath,
188
+ upsertThirdPartyReference,
189
+ applyThirdPartyReferences,
190
+ normalizeThirdPartyContent,
191
+ resolveThirdPartySkillDestination,
192
+ THIRD_PARTY_REF_START,
193
+ THIRD_PARTY_REF_END,
194
+ }