dsh-context-guardian 0.1.0-alpha.10

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,206 @@
1
+ import { pathToFileURL } from 'node:url'
2
+
3
+ export const ATTRIBUTES = Object.freeze(['codename', 'code', 'owner', 'version', 'port', 'path', 'name'])
4
+
5
+ // "codename" is listed before "code" so the alternation never reads "codename" as "code".
6
+ const ATTR = '(' + ATTRIBUTES.join('|') + ')'
7
+ const SUBJ = '([A-Za-z][A-Za-z0-9_-]*)'
8
+ const VAL = '([A-Za-z0-9][A-Za-z0-9_./:-]*)'
9
+
10
+ // P1: "the release codename for project Heron is MAPLE-123" -> attribute, subject, value
11
+ const P1 = new RegExp('\\b(?:the\\s+)?(?:release\\s+)?' + ATTR + '\\s+for\\s+(?:project\\s+)?' + SUBJ + '\\s+is\\s+(?:now\\s+)?' + VAL, 'gi')
12
+ // P2: "Update: project Heron's release codename is now CEDAR-789" -> subject, attribute, value
13
+ const P2 = new RegExp('\\b(?:update:\\s*)?(?:project\\s+)?' + SUBJ + "'s\\s+(?:release\\s+)?" + ATTR + '\\s+is\\s+(?:now\\s+)?' + VAL, 'gi')
14
+
15
+ function makeFact(m, subjectIdx, attributeIdx) {
16
+ const subject = m[subjectIdx]
17
+ const attribute = m[attributeIdx]
18
+ if (!subject || !attribute) return null
19
+ return {
20
+ key: subject.toLowerCase() + ' ' + attribute.toLowerCase(),
21
+ // A trailing '.' or ':' is sentence punctuation, not part of the value.
22
+ value: m[3].replace(/[.:]+$/, ''),
23
+ index: m.index,
24
+ length: m[0].length,
25
+ }
26
+ }
27
+
28
+ export function extractFacts(text) {
29
+ const str = String(text ?? '')
30
+ const facts = []
31
+
32
+ // P1 captures (attribute, subject, value); P2 captures (subject, attribute, value).
33
+ for (const m of str.matchAll(P1)) {
34
+ const f = makeFact(m, 2, 1)
35
+ if (f) facts.push(f)
36
+ }
37
+ for (const m of str.matchAll(P2)) {
38
+ const f = makeFact(m, 1, 2)
39
+ if (f) facts.push(f)
40
+ }
41
+
42
+ // Sort by index; keep a match only if it does not start before the end of the
43
+ // previously kept one, so overlapping reads collapse to the first one.
44
+ facts.sort((a, b) => a.index - b.index)
45
+ const kept = []
46
+ let lastEnd = -1
47
+ for (const fact of facts) {
48
+ if (fact.index >= lastEnd) {
49
+ kept.push(fact)
50
+ lastEnd = fact.index + fact.length
51
+ }
52
+ }
53
+ return kept
54
+ }
55
+
56
+ export function pinKey(text) {
57
+ const facts = extractFacts(text)
58
+ if (facts.length === 0) return { key: null, value: null }
59
+ return { key: facts[0].key, value: facts[0].value }
60
+ }
61
+
62
+ export function applyProvenance(pins) {
63
+ if (!Array.isArray(pins)) return []
64
+
65
+ // Work on shallow copies: the caller's array and its objects are never touched.
66
+ const entries = pins.map((pin) => {
67
+ const source = pin && typeof pin === 'object' ? pin : {}
68
+ return {
69
+ pin: source,
70
+ seq: Number(source.seq) || 0,
71
+ facts: extractFacts(source.text),
72
+ }
73
+ })
74
+
75
+ for (let i = 0; i < entries.length; i++) {
76
+ const mine = entries[i]
77
+ for (const fact of mine.facts) {
78
+ // Any OTHER pin, wherever it sits in the array, supersedes only if its seq is
79
+ // strictly higher and it states the same key with a different value. The
80
+ // reported seq is the lowest such one, not the first one found.
81
+ let by = null
82
+ for (let j = 0; j < entries.length; j++) {
83
+ if (j === i) continue
84
+ const other = entries[j]
85
+ if (other.seq <= mine.seq) continue
86
+ for (const of_ of other.facts) {
87
+ if (of_.key !== fact.key) continue
88
+ if (of_.value.toLowerCase() === fact.value.toLowerCase()) continue
89
+ if (by === null || other.seq < by) by = other.seq
90
+ }
91
+ }
92
+ fact.superseded = by !== null
93
+ fact.supersededBy = by
94
+ }
95
+ }
96
+
97
+ return entries.map(({ pin, facts }) => {
98
+ const first = facts.length > 0 ? facts[0] : null
99
+ // A pin with no facts has nothing to keep and is therefore never retired.
100
+ const allSuperseded = facts.length > 0 && facts.every((f) => f.superseded)
101
+ const mapFact = (f) => ({
102
+ key: f.key,
103
+ value: f.value,
104
+ index: f.index,
105
+ length: f.length,
106
+ superseded: f.superseded,
107
+ supersededBy: f.supersededBy,
108
+ })
109
+ return Object.assign({}, pin, {
110
+ key: first ? first.key : null,
111
+ value: first ? first.value : null,
112
+ facts: facts.map(mapFact),
113
+ superseded: allSuperseded,
114
+ supersededBy: allSuperseded
115
+ ? Math.max(...facts.map((f) => f.supersededBy))
116
+ : null,
117
+ })
118
+ })
119
+ }
120
+
121
+ export function renderPinText(pin) {
122
+ const text = String((pin && pin.text) ?? '')
123
+ const facts = pin && Array.isArray(pin.facts) ? pin.facts : []
124
+ if (facts.length === 0) return text
125
+
126
+ // Replace the last span first so the remaining indexes stay valid.
127
+ const spans = facts
128
+ .filter((f) => f && f.superseded)
129
+ .slice()
130
+ .sort((a, b) => b.index - a.index)
131
+
132
+ let out = text
133
+ for (const f of spans) {
134
+ const start = f.index
135
+ const end = f.index + f.length
136
+ if (!(Number.isInteger(start) && Number.isInteger(end)) || start < 0 || end > out.length || end < start) continue
137
+ out = out.slice(0, start) + '[' + f.key + ': superseded by seq ' + f.supersededBy + ']' + out.slice(end)
138
+ }
139
+ return out
140
+ }
141
+
142
+ export function selftest() {
143
+ const plant = 'Important, keep this for later in the conversation: the release codename for project Heron is MAPLE-123; and the release codename for project Kite is RIVER-456.'
144
+ const update = 'Important, keep this for later in the conversation: the release codename for project Heron is now CEDAR-789.'
145
+ const checks = [
146
+ ['p1_two_facts', () => {
147
+ const f = extractFacts(plant)
148
+ return f.length === 2 && f[0].key === 'heron codename' && f[0].value === 'MAPLE-123' && f[1].key === 'kite codename' && f[1].value === 'RIVER-456'
149
+ }],
150
+ ['p2_possessive_update', () => {
151
+ const f = extractFacts("Update: project Heron's release codename is now CEDAR-789.")
152
+ return f.length === 1 && f[0].key === 'heron codename' && f[0].value === 'CEDAR-789'
153
+ }],
154
+ ['code_is_not_codename', () => extractFacts('the code for Kite is ABC123')[0].key === 'kite code'],
155
+ ['no_match_null_key', () => {
156
+ const k = pinKey('please remember that I like tea')
157
+ return k.key === null && k.value === null
158
+ }],
159
+ ['partial_supersede_keeps_pin', () => {
160
+ const r = applyProvenance([{ text: plant, seq: 3 }, { text: update, seq: 30 }])
161
+ return r[0].superseded === false && r[0].facts[0].superseded === true && r[0].facts[0].supersededBy === 30 && r[0].facts[1].superseded === false && r[1].superseded === false
162
+ }],
163
+ ['full_supersede', () => {
164
+ const r = applyProvenance([{ text: 'For reference: the release codename for project Owl is ONE-1.', seq: 2 }, { text: 'For reference: the release codename for project Owl is TWO-2.', seq: 9 }])
165
+ return r[0].superseded === true && r[0].supersededBy === 9 && r[0].key === 'owl codename'
166
+ }],
167
+ ['same_value_restated_not_superseded', () => {
168
+ const r = applyProvenance([{ text: 'the release codename for project Owl is ONE-1', seq: 2 }, { text: 'the release codename for project Owl is one-1', seq: 9 }])
169
+ return r[0].superseded === false && r[0].facts[0].superseded === false
170
+ }],
171
+ ['older_pin_never_supersedes_newer', () => {
172
+ const r = applyProvenance([{ text: 'the release codename for project Owl is TWO-2', seq: 9 }, { text: 'the release codename for project Owl is ONE-1', seq: 2 }])
173
+ return r[0].superseded === false && r[1].superseded === true
174
+ }],
175
+ ['render_strikes_only_stale_fact', () => {
176
+ const r = applyProvenance([{ text: plant, seq: 3 }, { text: update, seq: 30 }])
177
+ const t = renderPinText(r[0])
178
+ return !t.includes('MAPLE-123') && t.includes('RIVER-456') && t.includes('[heron codename: superseded by seq 30]')
179
+ }],
180
+ ['input_not_mutated', () => {
181
+ const pins = [{ text: plant, seq: 3 }, { text: update, seq: 30 }]
182
+ applyProvenance(pins)
183
+ return !('facts' in pins[0]) && !('superseded' in pins[0])
184
+ }],
185
+ ]
186
+ let passed = 0
187
+ for (const [name, fn] of checks) {
188
+ let ok = false
189
+ let label = name
190
+ try {
191
+ ok = Boolean(fn())
192
+ } catch (err) {
193
+ label = name + ' (raised ' + (err && err.name) + ': ' + (err && err.message) + ')'
194
+ }
195
+ if (ok) passed += 1
196
+ console.log((ok ? 'ok ' : 'FAIL ') + label)
197
+ }
198
+ const failed = checks.length - passed
199
+ console.log('cg_provenance selftest: ' + checks.length + ' checks, ' + passed + ' passed, ' + failed + ' failed')
200
+ return { checks: checks.length, passed, failed }
201
+ }
202
+
203
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href && process.argv.includes('--selftest')) {
204
+ const r = selftest()
205
+ process.exit(r.failed === 0 && r.checks > 0 ? 0 : 1)
206
+ }