create-agdf 0.7.3 → 0.8.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/bin/create-agdf.js +149 -35
- package/generated/.github/skills/agdf-gate-check/SKILL.md +27 -8
- package/generated/.github/skills/agdf-interaction-locales.json +184 -0
- package/generated/.github/skills/agdf-runtime-contract.md +134 -3
- package/generated/.opencode/agdf-interaction-locales.json +184 -0
- package/generated/.opencode/agdf-runtime-contract.md +134 -3
- package/generated/.opencode/skills/agdf-gate-check/SKILL.md +27 -8
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/meta/agdf-interaction-locales.json +184 -0
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +44 -1
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +134 -3
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +27 -8
- package/lib/control-state/gate-approval-validator.js +49 -0
- package/lib/control-state/index.js +1 -0
- package/lib/interaction-presentation.js +169 -0
- package/package.json +3 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const REQUIRED_GATES = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
|
|
2
|
+
const ARTEFACT_ORDER = ["UR", "PRD", "SD", "TP"];
|
|
3
|
+
const OUTCOMES = new Set(["approve", "revise", "decline", "cancel", "no_response", "timeout", "empty", "invalid", "stale"]);
|
|
4
|
+
|
|
5
|
+
function plainObject(value) {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function flattenKeys(value, prefix = "") {
|
|
10
|
+
const keys = [];
|
|
11
|
+
for (const [key, child] of Object.entries(value ?? {})) {
|
|
12
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
13
|
+
if (plainObject(child)) keys.push(...flattenKeys(child, path));
|
|
14
|
+
else keys.push(path);
|
|
15
|
+
}
|
|
16
|
+
return keys.sort();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function visibleStrings(value, prefix = "") {
|
|
20
|
+
const entries = [];
|
|
21
|
+
for (const [key, child] of Object.entries(value ?? {})) {
|
|
22
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
23
|
+
if (plainObject(child)) entries.push(...visibleStrings(child, path));
|
|
24
|
+
else if (typeof child === "string") entries.push([path, child]);
|
|
25
|
+
}
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function canonicalizeLanguageTag(value) {
|
|
30
|
+
const raw = String(value ?? "").trim().replaceAll("_", "-").replace(/\.(.+)$/, "");
|
|
31
|
+
if (!raw || !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(raw)) return "";
|
|
32
|
+
try {
|
|
33
|
+
return Intl.getCanonicalLocales(raw)[0]?.toLowerCase() ?? "";
|
|
34
|
+
} catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateLocaleRegistry(registry) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
if (!plainObject(registry) || registry.schemaVersion !== 1) errors.push("schema_version");
|
|
42
|
+
const fallback = canonicalizeLanguageTag(registry?.fallbackLocale);
|
|
43
|
+
const locales = registry?.locales;
|
|
44
|
+
if (!fallback || !plainObject(locales) || !plainObject(locales[fallback])) errors.push("fallback_locale");
|
|
45
|
+
const baseline = plainObject(locales?.[fallback]) ? flattenKeys(locales[fallback]) : [];
|
|
46
|
+
const budgets = registry?.lengthBudgets ?? {};
|
|
47
|
+
for (const [locale, pack] of Object.entries(locales ?? {})) {
|
|
48
|
+
if (!canonicalizeLanguageTag(locale) || !plainObject(pack)) {
|
|
49
|
+
errors.push(`invalid_locale:${locale}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const keys = flattenKeys(pack);
|
|
53
|
+
if (JSON.stringify(keys) !== JSON.stringify(baseline)) errors.push(`incomplete_locale:${locale}`);
|
|
54
|
+
for (const [key, value] of visibleStrings(pack)) {
|
|
55
|
+
if (!value.trim()) errors.push(`empty_copy:${locale}:${key}`);
|
|
56
|
+
const budget = key.startsWith("gateTitles.") ? budgets.title
|
|
57
|
+
: key.includes("Description") || key.startsWith("primary.actions.") || key.startsWith("primary.afterApproval.") || key === "primary.quality"
|
|
58
|
+
? budgets.description
|
|
59
|
+
: budgets.label;
|
|
60
|
+
if (Number.isInteger(budget) && value.length > budget) errors.push(`length_budget:${locale}:${key}`);
|
|
61
|
+
}
|
|
62
|
+
for (const key of REQUIRED_GATES) {
|
|
63
|
+
if (typeof pack.gateTitles?.[key] !== "string" || !pack.gateTitles[key].trim()) errors.push(`missing_gate_title:${locale}:${key}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (JSON.stringify(registry?.optionOrder) !== JSON.stringify(["approve", "revise", "decline", "cancel"])) errors.push("option_order");
|
|
67
|
+
return { valid: errors.length === 0, errors };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolvePresentationLocale(registry, requestedLocale) {
|
|
71
|
+
const validation = validateLocaleRegistry(registry);
|
|
72
|
+
if (!validation.valid) throw new Error(`Invalid AGDF interaction locale registry: ${validation.errors.join(",")}`);
|
|
73
|
+
const fallback = canonicalizeLanguageTag(registry.fallbackLocale);
|
|
74
|
+
const requested = canonicalizeLanguageTag(requestedLocale);
|
|
75
|
+
if (requested && registry.locales[requested]) return requested;
|
|
76
|
+
const language = requested.split("-")[0];
|
|
77
|
+
if (language && registry.locales[language]) return language;
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function localePack(registry, requestedLocale) {
|
|
82
|
+
return registry.locales[resolvePresentationLocale(registry, requestedLocale)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function gateTitle(registry, requestedLocale, gate) {
|
|
86
|
+
return localePack(registry, requestedLocale).gateTitles[gate] || String(gate || "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stripMarkdown(value) {
|
|
90
|
+
return String(value ?? "")
|
|
91
|
+
.replace(/^#+\s*/, "")
|
|
92
|
+
.replace(/[`*_]/g, "")
|
|
93
|
+
.trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function section(content, heading) {
|
|
97
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
98
|
+
return content.match(new RegExp(`(?:^|\\n)## ${escaped}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`))?.[1]?.trim() ?? "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizedRunTitle(runId) {
|
|
102
|
+
const words = String(runId ?? "")
|
|
103
|
+
.replace(/[._-]+/g, " ")
|
|
104
|
+
.replace(/\s+/g, " ")
|
|
105
|
+
.trim();
|
|
106
|
+
return words ? words.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase()) : "AGDF run";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveHumanRunTitle({ currentArtefactHeading, urHeading, runContent, runId }) {
|
|
110
|
+
const objective = section(String(runContent ?? ""), "Objective").split(/\r?\n/).map(stripMarkdown).find(Boolean) ?? "";
|
|
111
|
+
return [currentArtefactHeading, urHeading, objective].map(stripMarkdown).find(Boolean) || normalizedRunTitle(runId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeArtefactPath(value) {
|
|
115
|
+
const path = String(value ?? "").replace(/^`|`$/g, "").trim();
|
|
116
|
+
if (!path || !/^[A-Za-z0-9._/-]+$/.test(path) || /^([a-z]+:|\/)/i.test(path)) return "";
|
|
117
|
+
const parts = path.replaceAll("\\", "/").split("/");
|
|
118
|
+
if (parts.includes("..")) return "";
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function buildArtefactRefs(artefacts, registry, requestedLocale, { pathExists } = {}) {
|
|
123
|
+
const pack = localePack(registry, requestedLocale);
|
|
124
|
+
return ARTEFACT_ORDER.map((type) => {
|
|
125
|
+
const artefact = artefacts instanceof Map ? artefacts.get(type) : artefacts?.[type];
|
|
126
|
+
const candidatePath = safeArtefactPath(artefact?.path);
|
|
127
|
+
const path = candidatePath && (!pathExists || pathExists(candidatePath, type)) ? candidatePath : "";
|
|
128
|
+
return {
|
|
129
|
+
type,
|
|
130
|
+
exists: Boolean(path),
|
|
131
|
+
status: String(artefact?.status ?? ""),
|
|
132
|
+
path,
|
|
133
|
+
label: type,
|
|
134
|
+
missingLabel: `${type} (${pack.artefacts.notYetCreated})`,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatArtefactRefs(refs) {
|
|
140
|
+
return refs.map((ref) => ref.exists ? `[${ref.label}](${ref.path})` : ref.missingLabel).join(" · ");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function gateOptions(registry, requestedLocale, gate, { includeCancel = false } = {}) {
|
|
144
|
+
const pack = localePack(registry, requestedLocale);
|
|
145
|
+
const options = [
|
|
146
|
+
{ outcome: "approve", label: `Approval: ${gate}`, description: pack.interaction.approveDescription, value: `Approval: ${gate}`, authorizes: true },
|
|
147
|
+
{ outcome: "revise", label: pack.interaction.reviseLabel, description: pack.interaction.reviseDescription, value: "revise", authorizes: false },
|
|
148
|
+
{ outcome: "decline", label: pack.interaction.declineLabel, description: pack.interaction.declineDescription, value: "decline", authorizes: false },
|
|
149
|
+
];
|
|
150
|
+
if (includeCancel) options.push({ outcome: "cancel", label: pack.interaction.cancelLabel, description: pack.interaction.cancelDescription, value: "cancel", authorizes: false });
|
|
151
|
+
return options;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function normalizeInteractionOutcome(value, { expectedApproval, stale = false, timedOut = false, noResponse = false } = {}) {
|
|
155
|
+
if (stale) return "stale";
|
|
156
|
+
if (timedOut) return "timeout";
|
|
157
|
+
if (noResponse || value === undefined || value === null) return "no_response";
|
|
158
|
+
if (typeof value !== "string") return "invalid";
|
|
159
|
+
const normalized = value.trim();
|
|
160
|
+
if (!normalized) return "empty";
|
|
161
|
+
if (normalized === expectedApproval) return "approve";
|
|
162
|
+
if (OUTCOMES.has(normalized) && normalized !== "approve") return normalized;
|
|
163
|
+
return "invalid";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const interactionPresentationConstants = Object.freeze({
|
|
167
|
+
artefactOrder: Object.freeze([...ARTEFACT_ORDER]),
|
|
168
|
+
outcomes: Object.freeze([...OUTCOMES]),
|
|
169
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-agdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Bootstrap AGDF repository-local plugin files, Copilot/OpenCode instructions, skills, control files and machine-readable checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
"test:delivery-path-search-unit": "node ./scripts/delivery-path-search-unit-test.js",
|
|
26
26
|
"test:delivery-path-search-generator": "node ./scripts/delivery-path-search-generator-test.js",
|
|
27
27
|
"test:control-state": "node ./scripts/control-state-test.js",
|
|
28
|
+
"test:interaction-presentation": "node ./scripts/interaction-presentation-test.js",
|
|
28
29
|
"test:verified-change": "node ./scripts/verified-change-test.js",
|
|
29
30
|
"test:runtime-integrity-negative": "node ./scripts/runtime-integrity-negative-test.js",
|
|
30
31
|
"test:release-bootstrap": "node ./scripts/release-bootstrap-smoke-test.js",
|
|
31
32
|
"test:routing": "npm run sync-package-assets && node ./scripts/test-routing.js",
|
|
32
|
-
"smoke-test": "npm run sync-package-assets && npm run test:control-state && npm run test:verified-change && npm run test:runtime-integrity-negative && npm run test:delivery-path-search && npm run test:delivery-path-search-unit && npm run test:delivery-path-search-generator && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
|
|
33
|
+
"smoke-test": "npm run sync-package-assets && npm run test:control-state && npm run test:interaction-presentation && npm run test:verified-change && npm run test:runtime-integrity-negative && npm run test:delivery-path-search && npm run test:delivery-path-search-unit && npm run test:delivery-path-search-generator && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
|
|
33
34
|
"prepack": "npm run sync-package-assets"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|