@jjchill/probity-rules 0.1.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.
- package/CHANGELOG.md +18 -0
- package/GLOSSARY.template.md +33 -0
- package/README.md +96 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/js.d.ts +63 -0
- package/dist/presets/js.d.ts.map +1 -0
- package/dist/presets/js.js +112 -0
- package/dist/presets/js.js.map +1 -0
- package/dist/presets/kmp.d.ts +27 -0
- package/dist/presets/kmp.d.ts.map +1 -0
- package/dist/presets/kmp.js +253 -0
- package/dist/presets/kmp.js.map +1 -0
- package/dist/presets/kotlin.d.ts +40 -0
- package/dist/presets/kotlin.d.ts.map +1 -0
- package/dist/presets/kotlin.js +172 -0
- package/dist/presets/kotlin.js.map +1 -0
- package/dist/presets/swift.d.ts +10 -0
- package/dist/presets/swift.d.ts.map +1 -0
- package/dist/presets/swift.js +285 -0
- package/dist/presets/swift.js.map +1 -0
- package/dist/rules/acceptance-language.d.ts +95 -0
- package/dist/rules/acceptance-language.d.ts.map +1 -0
- package/dist/rules/acceptance-language.js +443 -0
- package/dist/rules/acceptance-language.js.map +1 -0
- package/dist/rules/gates.d.ts +125 -0
- package/dist/rules/gates.d.ts.map +1 -0
- package/dist/rules/gates.js +285 -0
- package/dist/rules/gates.js.map +1 -0
- package/dist/rules/kotlin.d.ts +323 -0
- package/dist/rules/kotlin.d.ts.map +1 -0
- package/dist/rules/kotlin.js +722 -0
- package/dist/rules/kotlin.js.map +1 -0
- package/dist/rules/ports-and-adapters.d.ts +86 -0
- package/dist/rules/ports-and-adapters.d.ts.map +1 -0
- package/dist/rules/ports-and-adapters.js +366 -0
- package/dist/rules/ports-and-adapters.js.map +1 -0
- package/dist/rules/scoping.d.ts +68 -0
- package/dist/rules/scoping.d.ts.map +1 -0
- package/dist/rules/scoping.js +93 -0
- package/dist/rules/scoping.js.map +1 -0
- package/dist/rules/spec-test-parity.d.ts +164 -0
- package/dist/rules/spec-test-parity.d.ts.map +1 -0
- package/dist/rules/spec-test-parity.js +456 -0
- package/dist/rules/spec-test-parity.js.map +1 -0
- package/dist/rules/swift.d.ts +50 -0
- package/dist/rules/swift.d.ts.map +1 -0
- package/dist/rules/swift.js +50 -0
- package/dist/rules/swift.js.map +1 -0
- package/dist/rules/ubiquitous-language.d.ts +36 -0
- package/dist/rules/ubiquitous-language.d.ts.map +1 -0
- package/dist/rules/ubiquitous-language.js +140 -0
- package/dist/rules/ubiquitous-language.js.map +1 -0
- package/dist/scripts/scope-report.d.ts +3 -0
- package/dist/scripts/scope-report.d.ts.map +1 -0
- package/dist/scripts/scope-report.js +184 -0
- package/dist/scripts/scope-report.js.map +1 -0
- package/kiro/README.md +20 -0
- package/kiro/kiro-agent.template.json +45 -0
- package/kiro/kiro-transcript-to-claude.py +183 -0
- package/kiro/probity-kiro-translate.py +132 -0
- package/kiro/probity-kiro.sh +87 -0
- package/kiro/skill-activation-forced-eval.sh +45 -0
- package/package.json +68 -0
- package/probity.config.kmp.ts +40 -0
- package/probity.config.kotlin.ts +37 -0
- package/probity.config.swift.ts +38 -0
- package/probity.config.ts +43 -0
- package/scripts/spec-parity.mjs +345 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Glossary drift protection for the `ubiquitous-language` skill.
|
|
5
|
+
*
|
|
6
|
+
* Glossary format convention (see GLOSSARY.template.md): a Markdown
|
|
7
|
+
* file where each term is a level-2 heading:
|
|
8
|
+
*
|
|
9
|
+
* ## Parcel
|
|
10
|
+
* A shipment registered for delivery to a recipient.
|
|
11
|
+
*
|
|
12
|
+
* One term per concept; the definition text is free-form. Terms are
|
|
13
|
+
* matched case-insensitively.
|
|
14
|
+
*/
|
|
15
|
+
const TERM_HEADING = /^##\s+(.+?)\s*$/gm;
|
|
16
|
+
export function extractGlossaryTerms(content) {
|
|
17
|
+
return [...content.matchAll(TERM_HEADING)].map((m) => (m[1] ?? '').trim());
|
|
18
|
+
}
|
|
19
|
+
function escapeRegex(text) {
|
|
20
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A term is "used" when it appears as a phrase in prose (case-
|
|
24
|
+
* insensitive, whitespace-tolerant) or as an identifier in code —
|
|
25
|
+
* PascalCase, camelCase, or snake_case concatenations of its words
|
|
26
|
+
* ("Delivery Window" → DeliveryWindow, deliveryWindow,
|
|
27
|
+
* delivery_window).
|
|
28
|
+
*/
|
|
29
|
+
export function termUsagePattern(term) {
|
|
30
|
+
const words = term.trim().split(/\s+/).map(escapeRegex);
|
|
31
|
+
const phrase = words.join('\\s+');
|
|
32
|
+
if (words.length === 1)
|
|
33
|
+
return new RegExp(`\\b${phrase}\\b`, 'i');
|
|
34
|
+
const capitalized = term
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\s+/)
|
|
37
|
+
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase());
|
|
38
|
+
const pascal = escapeRegex(capitalized.join(''));
|
|
39
|
+
const camel = escapeRegex(capitalized[0].toLowerCase() + capitalized.slice(1).join(''));
|
|
40
|
+
const snake = words.map((w) => w.toLowerCase()).join('_');
|
|
41
|
+
return new RegExp(`\\b(?:${phrase}|${pascal}|${camel}|${snake})\\b`, 'i');
|
|
42
|
+
}
|
|
43
|
+
const SKIP_DIRS = new Set([
|
|
44
|
+
'node_modules',
|
|
45
|
+
'.git',
|
|
46
|
+
'build',
|
|
47
|
+
'.gradle',
|
|
48
|
+
'out',
|
|
49
|
+
'dist',
|
|
50
|
+
'.idea',
|
|
51
|
+
]);
|
|
52
|
+
function walk(dir, out = []) {
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
63
|
+
walk(join(dir, entry.name), out);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
out.push(join(dir, entry.name));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
const DEFAULT_SEARCH_PATTERN = /\.(kt|kts|swift|ts|js|py|java|feature|md)$/;
|
|
72
|
+
/**
|
|
73
|
+
* Surfaces glossary drift at the moment it is created: when a write
|
|
74
|
+
* to the glossary removes or renames a term that specs, tests, or
|
|
75
|
+
* source code still use, the write is blocked with the list of files
|
|
76
|
+
* using the term — rename the usages in the same change (or record
|
|
77
|
+
* the old term's fate in the glossary entry) and re-apply. Removing
|
|
78
|
+
* a term nothing uses passes silently, as do additions and
|
|
79
|
+
* definition edits.
|
|
80
|
+
*
|
|
81
|
+
* The companion invariant — new spec/port vocabulary conforming to
|
|
82
|
+
* the glossary — is judged by `enforceAcceptanceLanguage` and
|
|
83
|
+
* `enforcePortsBoundary` when given `glossaryPath`.
|
|
84
|
+
*
|
|
85
|
+
* Applies to: write actions to the glossary file (scope via a
|
|
86
|
+
* `{ files, rules }` block on its path). Deterministic — no AI call.
|
|
87
|
+
*
|
|
88
|
+
* @param options.searchRoots — absolute paths to scan for usages
|
|
89
|
+
* (repo root is fine; node_modules/build dirs are skipped).
|
|
90
|
+
* @param options.searchPattern — which files count as usage sites
|
|
91
|
+
* (default: common source, spec, and doc extensions).
|
|
92
|
+
*/
|
|
93
|
+
export function surfaceGlossaryTermBreakage(options) {
|
|
94
|
+
const pattern = options.searchPattern ?? DEFAULT_SEARCH_PATTERN;
|
|
95
|
+
return async function surfaceGlossaryTermBreakage(action, ctx) {
|
|
96
|
+
if (action.kind !== 'write')
|
|
97
|
+
return { kind: 'pass' };
|
|
98
|
+
const before = await ctx?.readFile?.(action.path);
|
|
99
|
+
if (!before || before.kind !== 'present')
|
|
100
|
+
return { kind: 'pass' };
|
|
101
|
+
const beforeTerms = extractGlossaryTerms(before.content);
|
|
102
|
+
const afterTerms = new Set(extractGlossaryTerms(action.content).map((t) => t.toLowerCase()));
|
|
103
|
+
const removed = beforeTerms.filter((t) => !afterTerms.has(t.toLowerCase()));
|
|
104
|
+
if (removed.length === 0)
|
|
105
|
+
return { kind: 'pass' };
|
|
106
|
+
const glossaryPath = resolve(action.path);
|
|
107
|
+
const files = options.searchRoots
|
|
108
|
+
.flatMap((root) => walk(root))
|
|
109
|
+
.filter((file) => pattern.test(file) && resolve(file) !== glossaryPath);
|
|
110
|
+
const broken = [];
|
|
111
|
+
for (const term of removed) {
|
|
112
|
+
const usage = termUsagePattern(term);
|
|
113
|
+
const users = files.filter((file) => {
|
|
114
|
+
try {
|
|
115
|
+
return usage.test(readFileSync(file, 'utf8'));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (users.length > 0) {
|
|
122
|
+
const shown = users.slice(0, 10).map((f) => ` ${f}`);
|
|
123
|
+
if (users.length > 10)
|
|
124
|
+
shown.push(` …and ${users.length - 10} more`);
|
|
125
|
+
broken.push(`"${term}" is still used by:\n${shown.join('\n')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (broken.length === 0)
|
|
129
|
+
return { kind: 'pass' };
|
|
130
|
+
return {
|
|
131
|
+
kind: 'violation',
|
|
132
|
+
reason: 'This edit removes or renames glossary term(s) that specs, ' +
|
|
133
|
+
'tests, or code still use. The glossary is the source of ' +
|
|
134
|
+
'truth: update the usages in the same change, then re-apply ' +
|
|
135
|
+
'this edit.\n' +
|
|
136
|
+
broken.join('\n'),
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=ubiquitous-language.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ubiquitous-language.js","sourceRoot":"","sources":["../../rules/ubiquitous-language.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAIzC;;;;;;;;;;;GAWG;AAEH,MAAM,YAAY,GAAG,mBAAmB,CAAA;AAExC,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AAC5E,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACvD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,MAAM,CAAC,MAAM,MAAM,KAAK,EAAE,GAAG,CAAC,CAAA;IACjE,MAAM,WAAW,GAAG,IAAI;SACrB,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;IAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IAChD,MAAM,KAAK,GAAG,WAAW,CACvB,WAAW,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAC9D,CAAA;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzD,OAAO,IAAI,MAAM,CAAC,SAAS,MAAM,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,EAAE,GAAG,CAAC,CAAA;AAC3E,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc;IACd,MAAM;IACN,OAAO;IACP,SAAS;IACT,KAAK;IACL,MAAM;IACN,OAAO;CACR,CAAC,CAAA;AAEF,SAAS,IAAI,CAAC,GAAW,EAAE,MAAgB,EAAE;IAC3C,IAAI,OAAO,CAAA;IACX,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAA;QAClE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,sBAAsB,GAAG,4CAA4C,CAAA;AAE3E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,2BAA2B,CAAC,OAG3C;IACC,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAA;IAC/D,OAAO,KAAK,UAAU,2BAA2B,CAC/C,MAAc,EACd,GAAiB;QAEjB,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACpD,MAAM,MAAM,GAAG,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACjE,MAAM,WAAW,GAAG,oBAAoB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACxD,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,oBAAoB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CACjE,CAAA;QACD,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;QAC3E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACjD,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW;aAC9B,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAC7B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,YAAY,CAAC,CAAA;QACzE,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;YACpC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;gBAClC,IAAI,CAAC;oBACH,OAAO,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;gBAC/C,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC,CAAC,CAAA;YACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;gBACvD,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE;oBAAE,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,MAAM,GAAG,EAAE,OAAO,CAAC,CAAA;gBACvE,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,wBAAwB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACjE,CAAC;QACH,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QAChD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,4DAA4D;gBAC5D,0DAA0D;gBAC1D,6DAA6D;gBAC7D,cAAc;gBACd,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;SACpB,CAAA;IACH,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-report.d.ts","sourceRoot":"","sources":["../../scripts/scope-report.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scoping dry-run for a Probity config: shows which files each
|
|
4
|
+
* `{ files, rules }` block actually claims in YOUR repo, before the
|
|
5
|
+
* first agent session finds out the hard way.
|
|
6
|
+
*
|
|
7
|
+
* Wrong glob scoping is the template set's main failure mode, and it
|
|
8
|
+
* fails in two directions: over-scoping is loud (enforcePortsBoundary
|
|
9
|
+
* pointed at adapter files blocks work those files are supposed to
|
|
10
|
+
* do), but under-scoping is silent — a glob slightly too narrow for
|
|
11
|
+
* your layout means enforcement simply never fires, and nothing
|
|
12
|
+
* notices. This report surfaces both at setup time:
|
|
13
|
+
*
|
|
14
|
+
* - DEAD SCOPE: a block whose globs match zero files
|
|
15
|
+
* - core-purity rules claiming adapter/DI/UI-looking paths
|
|
16
|
+
* - the acceptance-language rule claiming Robot/driver/DSL files
|
|
17
|
+
*
|
|
18
|
+
* Globs are resolved exactly as Probity resolves them — anchoring and
|
|
19
|
+
* picomatch semantics come from rules/scoping.ts, the same replica
|
|
20
|
+
* the workflow eval uses.
|
|
21
|
+
*
|
|
22
|
+
* Usage (from your project root, next to your probity.config.ts):
|
|
23
|
+
*
|
|
24
|
+
* npx probity-scope-report [--config probity.config.ts]
|
|
25
|
+
* [--root .] [--strict]
|
|
26
|
+
*
|
|
27
|
+
* --config defaults to probity.config.{ts,mts,js,mjs} found in
|
|
28
|
+
* --root; --root defaults to the current working directory. --strict
|
|
29
|
+
* exits 1 when any warning fires (for CI); otherwise warnings are
|
|
30
|
+
* advisory and the exit code is 0.
|
|
31
|
+
*
|
|
32
|
+
* The config is loaded via jiti — the same TypeScript loader Probity
|
|
33
|
+
* itself uses — rather than a bare dynamic `import()` of a .ts file,
|
|
34
|
+
* so this works whether the consumer's config is .ts, .mts, .js, or
|
|
35
|
+
* .mjs, with no separate build step for the config itself.
|
|
36
|
+
*/
|
|
37
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
38
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
39
|
+
import { createJiti } from 'jiti';
|
|
40
|
+
import { anchorGlob, buildMatcher, isRuleBlock } from '../rules/scoping.js';
|
|
41
|
+
const jiti = createJiti(import.meta.url);
|
|
42
|
+
const SKIP_DIRS = new Set([
|
|
43
|
+
'node_modules',
|
|
44
|
+
'.git',
|
|
45
|
+
'build',
|
|
46
|
+
'.gradle',
|
|
47
|
+
'out',
|
|
48
|
+
'dist',
|
|
49
|
+
'.idea',
|
|
50
|
+
]);
|
|
51
|
+
const SAMPLE_LIMIT = 6;
|
|
52
|
+
// Paths a core-purity rule should normally NOT claim (adapters, DI,
|
|
53
|
+
// UI, platform source sets) and a spec-language rule should normally
|
|
54
|
+
// NOT claim (layer-2 drivers). Heuristics: a hit is a warning to
|
|
55
|
+
// review, not proof of misconfiguration.
|
|
56
|
+
const CORE_RULES = /PortsBoundary|forbidNewAmbientEffects|forbidContentPattern/;
|
|
57
|
+
const ADAPTERISH_SEGMENT = /^(adapter|adapters|infra|infrastructure|ui|di)$/;
|
|
58
|
+
const LANGUAGE_RULES = /AcceptanceLanguage/;
|
|
59
|
+
const DRIVERISH_FILE = /(Robot|Driver|Dsl)\.\w+$|[/\\](drivers?|dsl)[/\\]/;
|
|
60
|
+
function parseArgs(argv) {
|
|
61
|
+
const out = { strict: false };
|
|
62
|
+
for (let i = 0; i < argv.length; i++) {
|
|
63
|
+
if (argv[i] === '--config')
|
|
64
|
+
out.config = argv[++i];
|
|
65
|
+
else if (argv[i] === '--root')
|
|
66
|
+
out.root = argv[++i];
|
|
67
|
+
else if (argv[i] === '--strict')
|
|
68
|
+
out.strict = true;
|
|
69
|
+
else {
|
|
70
|
+
console.error(`Unknown argument: ${argv[i]}`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function findConfig(dir) {
|
|
77
|
+
for (const ext of ['ts', 'mts', 'js', 'mjs']) {
|
|
78
|
+
const candidate = join(dir, `probity.config.${ext}`);
|
|
79
|
+
if (existsSync(candidate))
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
console.error(`No probity.config.{ts,mts,js,mjs} in ${dir} — pass --config.`);
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
85
|
+
function walk(dir, out = []) {
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
96
|
+
walk(join(dir, entry.name), out);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out.push(join(dir, entry.name).replace(/\\/g, '/'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function ruleNames(block) {
|
|
105
|
+
return block.rules.map((rule) => rule.name || '(unnamed)').join(', ');
|
|
106
|
+
}
|
|
107
|
+
const args = parseArgs(process.argv.slice(2));
|
|
108
|
+
const root = resolve(args.root ?? process.cwd());
|
|
109
|
+
const configPath = resolve(args.config ?? findConfig(root));
|
|
110
|
+
const configDir = dirname(configPath);
|
|
111
|
+
const config = await jiti.import(configPath, { default: true });
|
|
112
|
+
const tree = walk(root);
|
|
113
|
+
console.log(`Scope report for ${configPath}`);
|
|
114
|
+
console.log(`Repo root: ${root} (${tree.length} files scanned)\n`);
|
|
115
|
+
const warnings = [];
|
|
116
|
+
const claimed = new Set();
|
|
117
|
+
let flatRules = 0;
|
|
118
|
+
for (const [index, entry] of config.rules.entries()) {
|
|
119
|
+
if (!isRuleBlock(entry)) {
|
|
120
|
+
flatRules++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const label = `block ${index + 1} [${ruleNames(entry)}]`;
|
|
124
|
+
if (!entry.files) {
|
|
125
|
+
console.log(`${label}\n files: (none) — applies to every write\n`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Anchor against the config's directory, exactly as loadConfig does.
|
|
129
|
+
const globs = entry.files.map((glob) => anchorGlob(glob, configDir));
|
|
130
|
+
const matched = tree.filter(buildMatcher(globs));
|
|
131
|
+
matched.forEach((file) => claimed.add(file));
|
|
132
|
+
console.log(label);
|
|
133
|
+
// Per-glob counts are informational: a template can legitimately
|
|
134
|
+
// carry globs for layouts a given repo doesn't use (e.g. classic
|
|
135
|
+
// src/main alongside KMP source sets) — only a whole-block zero is
|
|
136
|
+
// a warning.
|
|
137
|
+
for (const glob of entry.files) {
|
|
138
|
+
const count = glob.startsWith('!')
|
|
139
|
+
? null
|
|
140
|
+
: tree.filter(buildMatcher([anchorGlob(glob, configDir)])).length;
|
|
141
|
+
console.log(` ${glob}${count === null ? '' : ` (${count})`}`);
|
|
142
|
+
}
|
|
143
|
+
console.log(` → ${matched.length} file(s)`);
|
|
144
|
+
for (const file of matched.slice(0, SAMPLE_LIMIT))
|
|
145
|
+
console.log(` ${relative(root, file)}`);
|
|
146
|
+
if (matched.length > SAMPLE_LIMIT)
|
|
147
|
+
console.log(` …and ${matched.length - SAMPLE_LIMIT} more`);
|
|
148
|
+
console.log('');
|
|
149
|
+
if (matched.length === 0) {
|
|
150
|
+
warnings.push(`DEAD SCOPE: ${label} matches no files — these rules will never fire. ` +
|
|
151
|
+
'Adjust the globs to your layout (this is the silent failure mode).');
|
|
152
|
+
}
|
|
153
|
+
if (CORE_RULES.test(ruleNames(entry))) {
|
|
154
|
+
const suspicious = matched.filter((file) => relative(root, file).split(/[/\\]/).some((segment) => ADAPTERISH_SEGMENT.test(segment)));
|
|
155
|
+
if (suspicious.length > 0) {
|
|
156
|
+
warnings.push(`${label} claims ${suspicious.length} adapter/DI/UI-looking file(s), e.g. ` +
|
|
157
|
+
`${relative(root, suspicious[0])} — core-purity rules pointed at adapters ` +
|
|
158
|
+
'block work those files are supposed to do. Exclude them (negation globs) ' +
|
|
159
|
+
'or confirm these paths really are core.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (LANGUAGE_RULES.test(ruleNames(entry))) {
|
|
163
|
+
const suspicious = matched.filter((file) => DRIVERISH_FILE.test(file));
|
|
164
|
+
if (suspicious.length > 0) {
|
|
165
|
+
warnings.push(`${label} claims ${suspicious.length} driver/DSL-looking file(s), e.g. ` +
|
|
166
|
+
`${relative(root, suspicious[0])} — layer-2 drivers are supposed to contain ` +
|
|
167
|
+
'the mechanics the Language Test blocks. Exclude them (e.g. !**/*Robot.kt).');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (flatRules > 0) {
|
|
172
|
+
console.log(`${flatRules} flat rule(s) apply to every action (no files scope).\n`);
|
|
173
|
+
}
|
|
174
|
+
console.log(`${claimed.size}/${tree.length} files are claimed by at least one files-scoped block.\n`);
|
|
175
|
+
if (warnings.length === 0) {
|
|
176
|
+
console.log('No scoping warnings.');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(`${warnings.length} warning(s):\n`);
|
|
180
|
+
for (const warning of warnings)
|
|
181
|
+
console.log(` ⚠ ${warning}\n`);
|
|
182
|
+
}
|
|
183
|
+
process.exit(args.strict && warnings.length > 0 ? 1 : 0);
|
|
184
|
+
//# sourceMappingURL=scope-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-report.js","sourceRoot":"","sources":["../../scripts/scope-report.ts"],"names":[],"mappings":"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"}
|
package/kiro/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Kiro CLI enforcement assets
|
|
2
|
+
|
|
3
|
+
Run this framework under **Kiro CLI** with the same Probity guardrail the
|
|
4
|
+
Claude Code integration gets. Probity has no native Kiro vendor, so these
|
|
5
|
+
files translate between Kiro's hook contract and Probity's `claude-code`
|
|
6
|
+
contract.
|
|
7
|
+
|
|
8
|
+
| File | Role |
|
|
9
|
+
|------|------|
|
|
10
|
+
| `probity-kiro.sh` | preToolUse shim (event translation + deny→exit 2) |
|
|
11
|
+
| `probity-kiro-translate.py` | `event` / `reason` translation helpers |
|
|
12
|
+
| `kiro-transcript-to-claude.py` | Kiro session JSONL → Anthropic JSONL (for history rules) |
|
|
13
|
+
| `skill-activation-forced-eval.sh` | userPromptSubmit skill-activation hook |
|
|
14
|
+
| `kiro-agent.template.json` | agent config template (skills + both hooks) |
|
|
15
|
+
|
|
16
|
+
**Setup:** see [`docs/kiro-setup.md`](../../../docs/kiro-setup.md).
|
|
17
|
+
|
|
18
|
+
These are copy-in artifacts (like the `probity.config.*.ts` presets): install
|
|
19
|
+
them into a project's `.kiro/`. This directory is the single source of truth —
|
|
20
|
+
fix Kiro-integration issues here, then re-copy.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/kiro-agent.json",
|
|
3
|
+
"name": "REPLACE_WITH_AGENT_NAME",
|
|
4
|
+
"description": "REPLACE: Workspace agent that surfaces the agent-skills library (via the .kiro/skills symlink) and enforces this project's Probity rules on every write/shell through the Kiro preToolUse shim.",
|
|
5
|
+
"prompt": "You are the default agent for this workspace. Treat AGENTS.md as the highest-priority context after the user's direct instructions. Honor the mandatory skill-activation sequence injected on every user prompt before implementing. Probity enforces this project's spec-first / test-first workflow mechanically as a preToolUse hook.",
|
|
6
|
+
"tools": [
|
|
7
|
+
"*"
|
|
8
|
+
],
|
|
9
|
+
"toolsSettings": {
|
|
10
|
+
"read": {
|
|
11
|
+
"allowedPaths": [
|
|
12
|
+
"./**"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"write": {
|
|
16
|
+
"allowedPaths": [
|
|
17
|
+
"./**"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"resources": [
|
|
22
|
+
"file://AGENTS.md",
|
|
23
|
+
"file://docs/GLOSSARY.md",
|
|
24
|
+
"skill://.kiro/skills/**/SKILL.md"
|
|
25
|
+
],
|
|
26
|
+
"hooks": {
|
|
27
|
+
"userPromptSubmit": [
|
|
28
|
+
{
|
|
29
|
+
"command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/skill-activation-forced-eval.sh\""
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"preToolUse": [
|
|
33
|
+
{
|
|
34
|
+
"matcher": "write",
|
|
35
|
+
"command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/probity-kiro.sh\"",
|
|
36
|
+
"timeout_ms": 120000
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "shell",
|
|
40
|
+
"command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/probity-kiro.sh\"",
|
|
41
|
+
"timeout_ms": 120000
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Kiro session transcript -> Anthropic (Claude Code) transcript transducer.
|
|
4
|
+
|
|
5
|
+
Probity's claude-code vendor reconstructs conversation history from a
|
|
6
|
+
JSONL transcript in Anthropic's persisted message shape (see
|
|
7
|
+
node_modules/@nizos/probity/dist/vendors/claude-code/transcript.js):
|
|
8
|
+
|
|
9
|
+
{"type":"assistant","message":{"content":[
|
|
10
|
+
{"type":"tool_use","id":<id>,"name":<ClaudeToolName>,"input":{...}}]}}
|
|
11
|
+
{"type":"user","message":{"content":[
|
|
12
|
+
{"type":"tool_result","tool_use_id":<id>,"content":<string|blocks>}]}}
|
|
13
|
+
{"type":"user","message":{"content":[{"type":"text","text":<prompt>}]}}
|
|
14
|
+
|
|
15
|
+
Kiro persists a different, versioned event stream at
|
|
16
|
+
~/.kiro/sessions/cli/<KIRO_SESSION_ID>.jsonl:
|
|
17
|
+
|
|
18
|
+
{"kind":"Prompt","data":{"content":[{"kind":"text","data":<str>}]}}
|
|
19
|
+
{"kind":"AssistantMessage","data":{"content":[
|
|
20
|
+
{"kind":"toolUse","data":{"toolUseId","name","input"}},
|
|
21
|
+
{"kind":"text","data":<str>}, {"kind":"thinking",...}]}}
|
|
22
|
+
{"kind":"ToolResults","data":{"content":[
|
|
23
|
+
{"kind":"toolResult","data":{"toolUseId","content":[{"kind":"text","data"}],"status"}}]}}
|
|
24
|
+
|
|
25
|
+
This script maps the former onto the latter so Probity's history-based
|
|
26
|
+
rules (the commit green-gate `requireGreenTestRun`, the TDD RED-before-GREEN
|
|
27
|
+
judge, characterization/probe resolution) can see prior tool calls and their
|
|
28
|
+
output when running under Kiro.
|
|
29
|
+
|
|
30
|
+
Tool-name mapping matters: Probity's history `toCanonical` only treats a
|
|
31
|
+
Claude `Bash` action as a shell command and `Write`/`Edit` as a file write.
|
|
32
|
+
So Kiro's `shell` must become `Bash` (or the green-gate never sees the
|
|
33
|
+
`xcodebuild ... test` output), and Kiro's `write` becomes `Write`/`Edit`.
|
|
34
|
+
|
|
35
|
+
Reads Kiro JSONL from argv[1] (or stdin) and writes Claude JSONL to stdout.
|
|
36
|
+
Malformed lines are skipped, mirroring Probity's own tolerant reader.
|
|
37
|
+
"""
|
|
38
|
+
import json
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _text_blocks(content):
|
|
43
|
+
"""Join the payloads of a Kiro content array into one string.
|
|
44
|
+
|
|
45
|
+
Text blocks (`kind == "text"`) carry prompts and assistant prose. Tool
|
|
46
|
+
RESULTS, however, are persisted as `kind == "json"` blocks — the shell
|
|
47
|
+
tool's {exit_status, stdout, stderr} object, write confirmations, etc.
|
|
48
|
+
The history-based rules match on command OUTPUT (the green-gate looks for
|
|
49
|
+
a passing test run; the characterization gate looks for the marked test
|
|
50
|
+
failing), so those json payloads must be serialized in rather than
|
|
51
|
+
dropped — otherwise every command's output is invisible under Kiro and
|
|
52
|
+
the gates can never see a test run at all.
|
|
53
|
+
"""
|
|
54
|
+
out = []
|
|
55
|
+
for item in content or []:
|
|
56
|
+
if not isinstance(item, dict):
|
|
57
|
+
continue
|
|
58
|
+
kind = item.get("kind")
|
|
59
|
+
data = item.get("data")
|
|
60
|
+
if kind == "text":
|
|
61
|
+
if isinstance(data, str):
|
|
62
|
+
out.append(data)
|
|
63
|
+
elif kind == "json":
|
|
64
|
+
if isinstance(data, str):
|
|
65
|
+
out.append(data)
|
|
66
|
+
else:
|
|
67
|
+
try:
|
|
68
|
+
out.append(json.dumps(data))
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
pass
|
|
71
|
+
return "\n".join(out)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _map_tool(name, tool_input):
|
|
75
|
+
"""Map a Kiro tool name+input to a Claude (tool_name, input) pair.
|
|
76
|
+
|
|
77
|
+
Only the shapes Probity's history reader interprets need to be exact:
|
|
78
|
+
- shell -> Bash {command}
|
|
79
|
+
- write -> Write {file_path, content} / Edit {file_path, new_string}
|
|
80
|
+
Everything else is passed through under its own name and read by
|
|
81
|
+
Probity as an inert `other` action (no rule matches it).
|
|
82
|
+
"""
|
|
83
|
+
tool_input = tool_input if isinstance(tool_input, dict) else {}
|
|
84
|
+
if name in ("shell", "execute_bash", "execute_cmd"):
|
|
85
|
+
return "Bash", {"command": tool_input.get("command", "")}
|
|
86
|
+
if name in ("write", "fs_write", "fsWrite"):
|
|
87
|
+
op = tool_input.get("command")
|
|
88
|
+
path = tool_input.get("path", "")
|
|
89
|
+
if op == "strReplace":
|
|
90
|
+
return "Edit", {
|
|
91
|
+
"file_path": path,
|
|
92
|
+
"old_string": tool_input.get("oldStr", ""),
|
|
93
|
+
"new_string": tool_input.get("newStr", ""),
|
|
94
|
+
"replace_all": bool(tool_input.get("replaceAll", False)),
|
|
95
|
+
}
|
|
96
|
+
# create / insert / append: the introduced text is `content`.
|
|
97
|
+
return "Write", {
|
|
98
|
+
"file_path": path,
|
|
99
|
+
"content": tool_input.get("content", ""),
|
|
100
|
+
}
|
|
101
|
+
return name or "Unknown", tool_input
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transduce_line(raw, emit):
|
|
105
|
+
line = raw.strip()
|
|
106
|
+
if not line:
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
entry = json.loads(line)
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return
|
|
112
|
+
if not isinstance(entry, dict):
|
|
113
|
+
return
|
|
114
|
+
kind = entry.get("kind")
|
|
115
|
+
data = entry.get("data") or {}
|
|
116
|
+
content = data.get("content") if isinstance(data, dict) else None
|
|
117
|
+
|
|
118
|
+
if kind == "Prompt":
|
|
119
|
+
text = _text_blocks(content)
|
|
120
|
+
if text:
|
|
121
|
+
emit({"type": "user",
|
|
122
|
+
"message": {"content": [{"type": "text", "text": text}]}})
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if kind == "AssistantMessage":
|
|
126
|
+
blocks = []
|
|
127
|
+
for item in content or []:
|
|
128
|
+
if not isinstance(item, dict):
|
|
129
|
+
continue
|
|
130
|
+
ikind = item.get("kind")
|
|
131
|
+
idata = item.get("data") or {}
|
|
132
|
+
if ikind == "toolUse" and isinstance(idata, dict):
|
|
133
|
+
cname, cinput = _map_tool(idata.get("name"), idata.get("input"))
|
|
134
|
+
blocks.append({
|
|
135
|
+
"type": "tool_use",
|
|
136
|
+
"id": idata.get("toolUseId", ""),
|
|
137
|
+
"name": cname,
|
|
138
|
+
"input": cinput,
|
|
139
|
+
})
|
|
140
|
+
elif ikind == "text" and isinstance(idata, str):
|
|
141
|
+
blocks.append({"type": "text", "text": idata})
|
|
142
|
+
if blocks:
|
|
143
|
+
emit({"type": "assistant", "message": {"content": blocks}})
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if kind == "ToolResults":
|
|
147
|
+
for item in content or []:
|
|
148
|
+
if not isinstance(item, dict) or item.get("kind") != "toolResult":
|
|
149
|
+
continue
|
|
150
|
+
idata = item.get("data") or {}
|
|
151
|
+
if not isinstance(idata, dict):
|
|
152
|
+
continue
|
|
153
|
+
emit({
|
|
154
|
+
"type": "user",
|
|
155
|
+
"message": {"content": [{
|
|
156
|
+
"type": "tool_result",
|
|
157
|
+
"tool_use_id": idata.get("toolUseId", ""),
|
|
158
|
+
"content": _text_blocks(idata.get("content")),
|
|
159
|
+
}]},
|
|
160
|
+
})
|
|
161
|
+
return
|
|
162
|
+
# Unknown top-level kinds are ignored.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main(argv):
|
|
166
|
+
src = open(argv[1], "r", encoding="utf-8") if len(argv) > 1 else sys.stdin
|
|
167
|
+
out = sys.stdout
|
|
168
|
+
|
|
169
|
+
def emit(obj):
|
|
170
|
+
out.write(json.dumps(obj))
|
|
171
|
+
out.write("\n")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
for raw in src:
|
|
175
|
+
transduce_line(raw, emit)
|
|
176
|
+
finally:
|
|
177
|
+
if src is not sys.stdin:
|
|
178
|
+
src.close()
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
sys.exit(main(sys.argv))
|