@dezycro-ai/agent-plugin 2.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.
Files changed (55) hide show
  1. package/.claude-plugin/plugin.json +7 -0
  2. package/LICENSE +18 -0
  3. package/PERSONAS.md +309 -0
  4. package/README.md +125 -0
  5. package/bin/resolve-auth.js +112 -0
  6. package/dist/hooks/lib/authoring.d.ts +118 -0
  7. package/dist/hooks/lib/authoring.js +277 -0
  8. package/dist/hooks/lib/config.d.ts +28 -0
  9. package/dist/hooks/lib/config.js +175 -0
  10. package/dist/hooks/lib/controller-match.d.ts +16 -0
  11. package/dist/hooks/lib/controller-match.js +96 -0
  12. package/dist/hooks/lib/coverage.d.ts +17 -0
  13. package/dist/hooks/lib/coverage.js +66 -0
  14. package/dist/hooks/lib/hashing.d.ts +8 -0
  15. package/dist/hooks/lib/hashing.js +49 -0
  16. package/dist/hooks/lib/logging.d.ts +13 -0
  17. package/dist/hooks/lib/logging.js +72 -0
  18. package/dist/hooks/lib/publish-spec.d.ts +17 -0
  19. package/dist/hooks/lib/publish-spec.js +64 -0
  20. package/dist/hooks/lib/quality-gate.d.ts +67 -0
  21. package/dist/hooks/lib/quality-gate.js +187 -0
  22. package/dist/hooks/lib/router.d.ts +32 -0
  23. package/dist/hooks/lib/router.js +717 -0
  24. package/dist/hooks/lib/state.d.ts +34 -0
  25. package/dist/hooks/lib/state.js +238 -0
  26. package/dist/hooks/lib/undo.d.ts +11 -0
  27. package/dist/hooks/lib/undo.js +71 -0
  28. package/dist/hooks/lib/verify.d.ts +28 -0
  29. package/dist/hooks/lib/verify.js +94 -0
  30. package/dist/hooks/lib/workbook.d.ts +21 -0
  31. package/dist/hooks/lib/workbook.js +77 -0
  32. package/dist/hooks/types.d.ts +249 -0
  33. package/dist/hooks/types.js +14 -0
  34. package/hooks/companion-runner +108 -0
  35. package/hooks/hooks.json +74 -0
  36. package/install.js +84 -0
  37. package/package.json +50 -0
  38. package/prompts/existing-work-ranker.md +47 -0
  39. package/prompts/prd-drafter.md +53 -0
  40. package/prompts/prd-question.md +66 -0
  41. package/prompts/trd-context-gate.md +57 -0
  42. package/prompts/trd-discovery.md +71 -0
  43. package/prompts/trd-drafter.md +63 -0
  44. package/prompts/trd-question.md +61 -0
  45. package/prompts/trd-reviewer.md +74 -0
  46. package/prompts/workbook-sweep.md +89 -0
  47. package/setup.js +35 -0
  48. package/skills/author/SKILL.md +285 -0
  49. package/skills/import-features/SKILL.md +428 -0
  50. package/skills/init/SKILL.md +133 -0
  51. package/skills/publish-spec/SKILL.md +101 -0
  52. package/skills/status/SKILL.md +76 -0
  53. package/skills/sync/SKILL.md +76 -0
  54. package/skills/verify/SKILL.md +248 -0
  55. package/skills/work/SKILL.md +201 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * controller-match.ts — match file paths against `controllerPaths` globs from
3
+ * `.dezycro/config.json` (TRD §5.2).
4
+ *
5
+ * Minimal glob subset (stdlib only):
6
+ * - `**` zero or more path segments
7
+ * - `*` any chars within a single segment (no slashes)
8
+ * - `?` any single char within a single segment
9
+ * - `{a,b}` alternation (`*.{kt,java}`)
10
+ *
11
+ * Limitations: no character classes, no escaping, forward-slash only.
12
+ */
13
+ 'use strict';
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.expandAlternation = expandAlternation;
16
+ exports.globToRegex = globToRegex;
17
+ exports.matchesControllerPath = matchesControllerPath;
18
+ exports.selectControllers = selectControllers;
19
+ function normalizePath(p) {
20
+ return String(p || '').replace(/\\/g, '/');
21
+ }
22
+ function expandAlternation(glob) {
23
+ const m = glob.match(/\{([^{}]+)\}/);
24
+ if (!m || m.index === undefined)
25
+ return [glob];
26
+ const whole = m[0];
27
+ const inner = m[1];
28
+ const parts = inner.split(',');
29
+ const out = [];
30
+ for (const part of parts) {
31
+ const expanded = glob.slice(0, m.index) + part + glob.slice(m.index + whole.length);
32
+ out.push(...expandAlternation(expanded));
33
+ }
34
+ return out;
35
+ }
36
+ function globToRegex(glob) {
37
+ let re = '^';
38
+ for (let i = 0; i < glob.length; i += 1) {
39
+ const c = glob[i];
40
+ if (c === '*') {
41
+ if (glob[i + 1] === '*') {
42
+ i += 1;
43
+ if (glob[i + 1] === '/') {
44
+ i += 1;
45
+ re += '(?:.*/)?';
46
+ }
47
+ else {
48
+ re += '.*';
49
+ }
50
+ }
51
+ else {
52
+ re += '[^/]*';
53
+ }
54
+ }
55
+ else if (c === '?') {
56
+ re += '[^/]';
57
+ }
58
+ else if ('.+^$()|[]\\'.indexOf(c) !== -1) {
59
+ re += '\\' + c;
60
+ }
61
+ else {
62
+ re += c;
63
+ }
64
+ }
65
+ re += '$';
66
+ return new RegExp(re);
67
+ }
68
+ const _regexCache = new Map();
69
+ function compileGlob(glob) {
70
+ const hit = _regexCache.get(glob);
71
+ if (hit)
72
+ return hit;
73
+ const patterns = expandAlternation(glob).map(globToRegex);
74
+ _regexCache.set(glob, patterns);
75
+ return patterns;
76
+ }
77
+ function matchesControllerPath(filePath, globs) {
78
+ if (!filePath || !Array.isArray(globs) || globs.length === 0)
79
+ return false;
80
+ const norm = normalizePath(filePath);
81
+ for (const glob of globs) {
82
+ if (typeof glob !== 'string' || glob.length === 0)
83
+ continue;
84
+ const regexes = compileGlob(glob);
85
+ for (const re of regexes) {
86
+ if (re.test(norm))
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+ function selectControllers(filePaths, globs) {
93
+ if (!Array.isArray(filePaths))
94
+ return [];
95
+ return filePaths.filter((p) => matchesControllerPath(p, globs));
96
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * coverage.ts — deterministic coverage-snapshot formatter (TRD §5.4.2).
3
+ *
4
+ * One-liner format:
5
+ * Coverage: N path[s] uncovered, M endpoint[s] not in spec, K baseline[s] stale
6
+ *
7
+ * Zero-count segments are omitted. If all counts are zero, returns null.
8
+ */
9
+ import type { CoverageSnapshot } from '../types';
10
+ export declare function formatOneLiner(snapshot: CoverageSnapshot | null | undefined): string | null;
11
+ export declare function formatDelta(prev: CoverageSnapshot | null | undefined, next: CoverageSnapshot | null | undefined): string | null;
12
+ /**
13
+ * Detached coverage-fetch entry point. Deferred to V2.1 — for V2.0 the
14
+ * /dezycro:verify skill calls get_coverage_report directly and updates
15
+ * state via `companion-runner state --patch=…`.
16
+ */
17
+ export declare function fetch(_featureId: string): Promise<null>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * coverage.ts — deterministic coverage-snapshot formatter (TRD §5.4.2).
3
+ *
4
+ * One-liner format:
5
+ * Coverage: N path[s] uncovered, M endpoint[s] not in spec, K baseline[s] stale
6
+ *
7
+ * Zero-count segments are omitted. If all counts are zero, returns null.
8
+ */
9
+ 'use strict';
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.formatOneLiner = formatOneLiner;
12
+ exports.formatDelta = formatDelta;
13
+ exports.fetch = fetch;
14
+ function plural(n, word) {
15
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
16
+ }
17
+ function snapshotCounts(snapshot) {
18
+ if (!snapshot || typeof snapshot !== 'object')
19
+ return null;
20
+ return {
21
+ uncoveredPaths: Number(snapshot.uncoveredPaths ?? 0) || 0,
22
+ endpointsNotInSpec: Number(snapshot.endpointsNotInSpec ?? 0) || 0,
23
+ staleBaselines: Number(snapshot.staleBaselines ?? 0) || 0,
24
+ };
25
+ }
26
+ function formatOneLiner(snapshot) {
27
+ const c = snapshotCounts(snapshot);
28
+ if (!c)
29
+ return null;
30
+ const segments = [];
31
+ if (c.uncoveredPaths > 0)
32
+ segments.push(`${plural(c.uncoveredPaths, 'path')} uncovered`);
33
+ if (c.endpointsNotInSpec > 0)
34
+ segments.push(`${plural(c.endpointsNotInSpec, 'endpoint')} not in spec`);
35
+ if (c.staleBaselines > 0)
36
+ segments.push(`${plural(c.staleBaselines, 'baseline')} stale`);
37
+ if (segments.length === 0)
38
+ return null;
39
+ return `Coverage: ${segments.join(', ')}`;
40
+ }
41
+ function formatDelta(prev, next) {
42
+ const a = snapshotCounts(prev);
43
+ const b = snapshotCounts(next);
44
+ if (!b)
45
+ return null;
46
+ if (!a)
47
+ return formatOneLiner(next);
48
+ const deltas = [
49
+ ['paths uncovered', b.uncoveredPaths - a.uncoveredPaths],
50
+ ['endpoints not in spec', b.endpointsNotInSpec - a.endpointsNotInSpec],
51
+ ['baselines stale', b.staleBaselines - a.staleBaselines],
52
+ ];
53
+ const nonZero = deltas.filter(([, d]) => d !== 0);
54
+ if (nonZero.length === 0)
55
+ return null;
56
+ const parts = nonZero.map(([label, d]) => `${d > 0 ? '+' : ''}${d} ${label}`);
57
+ return `Coverage delta since last verify: ${parts.join(', ')}`;
58
+ }
59
+ /**
60
+ * Detached coverage-fetch entry point. Deferred to V2.1 — for V2.0 the
61
+ * /dezycro:verify skill calls get_coverage_report directly and updates
62
+ * state via `companion-runner state --patch=…`.
63
+ */
64
+ async function fetch(_featureId) {
65
+ return null;
66
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * hashing.ts — content-hash dedup for auto-logged workbook items (TRD §3.4).
3
+ *
4
+ * SHA-1 of `lowercase(<entity-kind>::<summary>::<linked-task-id-or-empty>)`
5
+ * truncated to 16 hex chars.
6
+ */
7
+ export type EntityKind = 'decision' | 'issue' | 'assumption' | 'task-status';
8
+ export declare function hashCandidate(entityKind: EntityKind | string, summary: string, linkedTaskId: string | null): string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * hashing.ts — content-hash dedup for auto-logged workbook items (TRD §3.4).
3
+ *
4
+ * SHA-1 of `lowercase(<entity-kind>::<summary>::<linked-task-id-or-empty>)`
5
+ * truncated to 16 hex chars.
6
+ */
7
+ 'use strict';
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.hashCandidate = hashCandidate;
43
+ const crypto = __importStar(require("crypto"));
44
+ function hashCandidate(entityKind, summary, linkedTaskId) {
45
+ const kind = String(entityKind || '').toLowerCase().trim();
46
+ const text = String(summary || '').toLowerCase().trim().replace(/\s+/g, ' ');
47
+ const link = String(linkedTaskId || '').toLowerCase().trim();
48
+ return crypto.createHash('sha1').update(`${kind}::${text}::${link}`).digest('hex').slice(0, 16);
49
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * logging.ts — JSON-lines logger for Companion.
3
+ *
4
+ * Writes to `.dezycro/companion.log` per TRD §12 (Observability). Best-effort:
5
+ * never throws upstream — a logging failure must never break a hook.
6
+ */
7
+ type Fields = Record<string, unknown> | undefined;
8
+ /** Append a single JSON-lines record to `<repoRoot>/.dezycro/companion.log`. */
9
+ export declare function append(repoRoot: string, record: Record<string, unknown>): void;
10
+ export declare function info(repoRoot: string, event: string, fields?: Fields): void;
11
+ export declare function warn(repoRoot: string, event: string, fields?: Fields): void;
12
+ export declare function error(repoRoot: string, event: string, fields?: Fields): void;
13
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * logging.ts — JSON-lines logger for Companion.
3
+ *
4
+ * Writes to `.dezycro/companion.log` per TRD §12 (Observability). Best-effort:
5
+ * never throws upstream — a logging failure must never break a hook.
6
+ */
7
+ 'use strict';
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.append = append;
43
+ exports.info = info;
44
+ exports.warn = warn;
45
+ exports.error = error;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const LOG_FILENAME = 'companion.log';
49
+ /** Append a single JSON-lines record to `<repoRoot>/.dezycro/companion.log`. */
50
+ function append(repoRoot, record) {
51
+ try {
52
+ const dezycroDir = path.join(repoRoot, '.dezycro');
53
+ if (!fs.existsSync(dezycroDir)) {
54
+ // We don't create the dir here — Companion init owns that.
55
+ return;
56
+ }
57
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n';
58
+ fs.appendFileSync(path.join(dezycroDir, LOG_FILENAME), line, { encoding: 'utf8' });
59
+ }
60
+ catch {
61
+ // Swallow — logging must never propagate.
62
+ }
63
+ }
64
+ function info(repoRoot, event, fields) {
65
+ append(repoRoot, { level: 'info', event, ...(fields || {}) });
66
+ }
67
+ function warn(repoRoot, event, fields) {
68
+ append(repoRoot, { level: 'warn', event, ...(fields || {}) });
69
+ }
70
+ function error(repoRoot, event, fields) {
71
+ append(repoRoot, { level: 'error', event, ...(fields || {}) });
72
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * publish-spec.ts — helper consumed by the `/dezycro:verify` skill to detect
3
+ * pending controller edits and announce + chain `/dezycro:publish-spec` before
4
+ * verifying (TRD §5.2).
5
+ *
6
+ * The hook layer does NOT run this. PostToolUse:edit sets `needsSpecPublish`
7
+ * + appends to `controllerEditsSinceLastPublish` (in router.ts); the verify
8
+ * skill reads via `needsPublishBeforeVerify`, announces, chains publish-spec,
9
+ * then runs verify. After publish-spec completes the skill clears the flags.
10
+ */
11
+ export interface PublishCheckResult {
12
+ needed: boolean;
13
+ controllerPaths: string[];
14
+ lastEditTs: string | null;
15
+ }
16
+ export declare function needsPublishBeforeVerify(repoRoot: string | null | undefined): PublishCheckResult;
17
+ export declare function buildAnnouncement(controllerPaths: string[]): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * publish-spec.ts — helper consumed by the `/dezycro:verify` skill to detect
3
+ * pending controller edits and announce + chain `/dezycro:publish-spec` before
4
+ * verifying (TRD §5.2).
5
+ *
6
+ * The hook layer does NOT run this. PostToolUse:edit sets `needsSpecPublish`
7
+ * + appends to `controllerEditsSinceLastPublish` (in router.ts); the verify
8
+ * skill reads via `needsPublishBeforeVerify`, announces, chains publish-spec,
9
+ * then runs verify. After publish-spec completes the skill clears the flags.
10
+ */
11
+ 'use strict';
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.needsPublishBeforeVerify = needsPublishBeforeVerify;
47
+ exports.buildAnnouncement = buildAnnouncement;
48
+ const state = __importStar(require("./state"));
49
+ function needsPublishBeforeVerify(repoRoot) {
50
+ if (!repoRoot)
51
+ return { needed: false, controllerPaths: [], lastEditTs: null };
52
+ const s = state.read(repoRoot);
53
+ const edits = Array.isArray(s.controllerEditsSinceLastPublish)
54
+ ? s.controllerEditsSinceLastPublish
55
+ : [];
56
+ const controllerPaths = edits.map((e) => e && e.path).filter(Boolean);
57
+ const lastEditTs = edits.length > 0 ? (edits[edits.length - 1].editedTs || null) : null;
58
+ const needed = Boolean(s.needsSpecPublish) && controllerPaths.length > 0;
59
+ return { needed, controllerPaths, lastEditTs };
60
+ }
61
+ function buildAnnouncement(controllerPaths) {
62
+ const list = (controllerPaths || []).join(', ');
63
+ return 'Controllers changed since last published spec: ' + list + '. Publishing spec before verify.';
64
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * quality-gate.ts — deterministic TRD-approval checks + code-side enforcement
3
+ * of the P-9 LLM result. TRD §5.5.3 QUALITY_GATE and §6.9 enforcement notes.
4
+ *
5
+ * No LLM calls live here. The skill markdown is responsible for invoking P-9;
6
+ * `enforceP9Result` is the tamper-proof code-side guard run against P-9's
7
+ * structured output.
8
+ */
9
+ export declare const INVENTION_KEYWORDS: readonly string[];
10
+ export interface RequiredHeading {
11
+ id: string;
12
+ pattern: RegExp;
13
+ }
14
+ export declare const REQUIRED_TRD_HEADINGS: readonly RequiredHeading[];
15
+ export declare const PLACEHOLDER_REGEX: RegExp;
16
+ export declare const MIN_TRD_LENGTH = 1000;
17
+ export declare const PILLAR_HEADING_REGEX: RegExp;
18
+ export declare const MIN_CONTEXT_CATEGORIES: readonly string[];
19
+ export declare const MIN_CONTEXT_EVIDENCE_CHARS = 80;
20
+ export declare const MIN_P9_SCORE = 0.85;
21
+ export declare function parsePrdPillarNames(prdContent: string | null | undefined): string[];
22
+ export interface BlockingIssue {
23
+ section: string;
24
+ issue: string;
25
+ required_fix: string;
26
+ }
27
+ export interface DeterministicGateResult {
28
+ passed: boolean;
29
+ blockingIssues: BlockingIssue[];
30
+ deterministic: true;
31
+ }
32
+ export declare function deterministicTrdGate(opts: {
33
+ trdContent: string | null | undefined;
34
+ prdContent: string | null | undefined;
35
+ }): DeterministicGateResult;
36
+ export interface MinContextGateInput {
37
+ discoverySummary: {
38
+ confirmed_context?: Record<string, string[]>;
39
+ likely_conventions?: Record<string, string[]>;
40
+ [extra: string]: unknown;
41
+ } | null;
42
+ answeredCategories: Record<string, string[]> | null;
43
+ }
44
+ export interface MinContextGateResult {
45
+ ready: boolean;
46
+ missing: Record<string, string[]>;
47
+ }
48
+ export declare function minContextGate(opts: MinContextGateInput): MinContextGateResult;
49
+ export interface P9Output {
50
+ approved?: boolean;
51
+ score?: number;
52
+ suspected_inventions?: Array<{
53
+ claim?: string;
54
+ }>;
55
+ blocking_issues?: Array<{
56
+ issue?: string;
57
+ }>;
58
+ }
59
+ export interface P9EnforcementResult {
60
+ passed: boolean;
61
+ reason: string | null;
62
+ score: number;
63
+ suspectedInventions: Array<{
64
+ claim?: string;
65
+ }>;
66
+ }
67
+ export declare function enforceP9Result(p9Output: P9Output | null | undefined): P9EnforcementResult;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * quality-gate.ts — deterministic TRD-approval checks + code-side enforcement
3
+ * of the P-9 LLM result. TRD §5.5.3 QUALITY_GATE and §6.9 enforcement notes.
4
+ *
5
+ * No LLM calls live here. The skill markdown is responsible for invoking P-9;
6
+ * `enforceP9Result` is the tamper-proof code-side guard run against P-9's
7
+ * structured output.
8
+ */
9
+ 'use strict';
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MIN_P9_SCORE = exports.MIN_CONTEXT_EVIDENCE_CHARS = exports.MIN_CONTEXT_CATEGORIES = exports.PILLAR_HEADING_REGEX = exports.MIN_TRD_LENGTH = exports.PLACEHOLDER_REGEX = exports.REQUIRED_TRD_HEADINGS = exports.INVENTION_KEYWORDS = void 0;
12
+ exports.parsePrdPillarNames = parsePrdPillarNames;
13
+ exports.deterministicTrdGate = deterministicTrdGate;
14
+ exports.minContextGate = minContextGate;
15
+ exports.enforceP9Result = enforceP9Result;
16
+ exports.INVENTION_KEYWORDS = Object.freeze([
17
+ 'architecture',
18
+ 'data model',
19
+ 'data-model',
20
+ 'datamodel',
21
+ 'api contract',
22
+ 'api-contract',
23
+ 'apicontract',
24
+ 'migration',
25
+ 'auth',
26
+ 'queue',
27
+ 'verifier-behavior',
28
+ 'verifier behavior',
29
+ ]);
30
+ exports.REQUIRED_TRD_HEADINGS = Object.freeze([
31
+ { id: 'architecture', pattern: /^##\s+architecture\b/im },
32
+ { id: 'data_model', pattern: /^##\s+data\s+model\b/im },
33
+ { id: 'api_contracts', pattern: /^##\s+api\s+contracts?\b/im },
34
+ { id: 'failure_modes', pattern: /^##\s+failure\s+modes?/im },
35
+ { id: 'migration', pattern: /^##\s+migration\b/im },
36
+ { id: 'open_questions', pattern: /^##\s+open\s+questions?\b/im },
37
+ ]);
38
+ exports.PLACEHOLDER_REGEX = /\b(TBD|TODO|FIXME|fill in later|to be determined)\b/i;
39
+ exports.MIN_TRD_LENGTH = 1000;
40
+ exports.PILLAR_HEADING_REGEX = /^##\s+Pillar\s+\d+\s*:\s*(.+?)\s*$/gim;
41
+ exports.MIN_CONTEXT_CATEGORIES = Object.freeze([
42
+ 'data_model',
43
+ 'api_contracts',
44
+ 'state_or_persistence',
45
+ 'failure_modes',
46
+ 'migration_or_rollout',
47
+ ]);
48
+ exports.MIN_CONTEXT_EVIDENCE_CHARS = 80;
49
+ exports.MIN_P9_SCORE = 0.85;
50
+ function normalizeWhitespace(s) {
51
+ return (s || '').toString().replace(/\s+/g, ' ').trim();
52
+ }
53
+ function parsePrdPillarNames(prdContent) {
54
+ if (!prdContent || typeof prdContent !== 'string')
55
+ return [];
56
+ const names = [];
57
+ exports.PILLAR_HEADING_REGEX.lastIndex = 0;
58
+ let match;
59
+ while ((match = exports.PILLAR_HEADING_REGEX.exec(prdContent)) !== null) {
60
+ const name = match[1].trim();
61
+ if (name)
62
+ names.push(name);
63
+ }
64
+ return names;
65
+ }
66
+ function deterministicTrdGate(opts) {
67
+ const { trdContent, prdContent } = opts;
68
+ const issues = [];
69
+ const trd = (trdContent || '').toString();
70
+ const trdLen = trd.trim().length;
71
+ if (trdLen < exports.MIN_TRD_LENGTH) {
72
+ issues.push({
73
+ section: 'document',
74
+ issue: `TRD is too short (${trdLen} chars; minimum ${exports.MIN_TRD_LENGTH}).`,
75
+ required_fix: 'Expand the draft with concrete technical detail.',
76
+ });
77
+ }
78
+ for (const required of exports.REQUIRED_TRD_HEADINGS) {
79
+ if (!required.pattern.test(trd)) {
80
+ issues.push({
81
+ section: required.id,
82
+ issue: `Required H2 section is missing.`,
83
+ required_fix: `Add a "## ${required.id.replace(/_/g, ' ')}" section grounded in the PRD + discovery.`,
84
+ });
85
+ }
86
+ }
87
+ const placeholderMatch = trd.match(exports.PLACEHOLDER_REGEX);
88
+ if (placeholderMatch) {
89
+ issues.push({
90
+ section: 'placeholders',
91
+ issue: `Placeholder token "${placeholderMatch[0]}" present — TRDs must not ship with unresolved placeholders.`,
92
+ required_fix: 'Resolve or move the open item into the ## Open Questions section.',
93
+ });
94
+ }
95
+ const pillarNames = parsePrdPillarNames(prdContent);
96
+ if (pillarNames.length > 0) {
97
+ const missing = [];
98
+ const haystack = trd.toLowerCase();
99
+ for (const name of pillarNames) {
100
+ if (!haystack.includes(name.toLowerCase()))
101
+ missing.push(name);
102
+ }
103
+ if (missing.length > 0) {
104
+ issues.push({
105
+ section: 'pillar_coverage',
106
+ issue: `TRD does not mention PRD pillar(s): ${missing.join(', ')}.`,
107
+ required_fix: 'Add a per-pillar technical-design section, or reference each pillar by name in the relevant TRD section.',
108
+ });
109
+ }
110
+ }
111
+ return {
112
+ passed: issues.length === 0,
113
+ blockingIssues: issues,
114
+ deterministic: true,
115
+ };
116
+ }
117
+ function minContextGate(opts) {
118
+ const summary = opts.discoverySummary || {};
119
+ const confirmed = summary.confirmed_context || {};
120
+ const likely = summary.likely_conventions || {};
121
+ const answers = opts.answeredCategories || {};
122
+ const missing = {};
123
+ for (const cat of exports.MIN_CONTEXT_CATEGORIES) {
124
+ const items = []
125
+ .concat(confirmed[cat] || [])
126
+ .concat(likely[cat] || [])
127
+ .concat(answers[cat] || []);
128
+ const combined = normalizeWhitespace(items.join(' '));
129
+ const reasons = [];
130
+ if (items.length === 0) {
131
+ reasons.push('no evidence in confirmed_context, likely_conventions, or QA answers');
132
+ }
133
+ else if (combined.length < exports.MIN_CONTEXT_EVIDENCE_CHARS) {
134
+ reasons.push(`evidence too thin (${combined.length} chars; need \u2265 ${exports.MIN_CONTEXT_EVIDENCE_CHARS})`);
135
+ }
136
+ if (reasons.length > 0)
137
+ missing[cat] = reasons;
138
+ }
139
+ return {
140
+ ready: Object.keys(missing).length === 0,
141
+ missing,
142
+ };
143
+ }
144
+ function enforceP9Result(p9Output) {
145
+ const out = p9Output || {};
146
+ const score = typeof out.score === 'number' ? out.score : 0;
147
+ const suspected = Array.isArray(out.suspected_inventions) ? out.suspected_inventions : [];
148
+ if (score < exports.MIN_P9_SCORE) {
149
+ return {
150
+ passed: false,
151
+ reason: `P-9 score ${score.toFixed(2)} is below the minimum ${exports.MIN_P9_SCORE.toFixed(2)} — TRD is not approved.`,
152
+ score,
153
+ suspectedInventions: suspected,
154
+ };
155
+ }
156
+ const flagged = [];
157
+ for (const inv of suspected) {
158
+ const claim = (inv && inv.claim ? inv.claim : '').toString().toLowerCase();
159
+ if (!claim)
160
+ continue;
161
+ const hit = exports.INVENTION_KEYWORDS.find((kw) => claim.includes(kw));
162
+ if (hit)
163
+ flagged.push({ claim: String(inv.claim), keyword: hit });
164
+ }
165
+ if (flagged.length > 0) {
166
+ const summary = flagged
167
+ .map((f) => `"${f.claim}" (keyword: ${f.keyword})`)
168
+ .join('; ');
169
+ return {
170
+ passed: false,
171
+ reason: `P-9 flagged invented technical claim(s): ${summary}. Move to Open Questions or ask the user.`,
172
+ score,
173
+ suspectedInventions: suspected,
174
+ };
175
+ }
176
+ if (out.approved !== true) {
177
+ return {
178
+ passed: false,
179
+ reason: out.blocking_issues && out.blocking_issues.length > 0
180
+ ? `P-9 reviewer rejected: ${String(out.blocking_issues[0].issue || 'blocking issue')}`
181
+ : 'P-9 reviewer did not approve the TRD.',
182
+ score,
183
+ suspectedInventions: suspected,
184
+ };
185
+ }
186
+ return { passed: true, reason: null, score, suspectedInventions: suspected };
187
+ }