arkgate 4.8.10 → 4.8.11
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 +33 -2
- package/README.md +5 -4
- package/bin/ark-check-runtime.mjs +6 -0
- package/bin/ark-check.mjs +3 -0
- package/bin/lib/adapter-contract-types.mjs +137 -0
- package/bin/lib/adapter-contract.mjs +4 -180
- package/bin/lib/adapter-finding-refs.mjs +63 -0
- package/bin/lib/agent-projection-formatters.mjs +151 -0
- package/bin/lib/agent-projection-merge.mjs +148 -0
- package/bin/lib/agent-projection-types.mjs +42 -0
- package/bin/lib/agent-projection.mjs +3 -309
- package/bin/lib/project-root.mjs +70 -4
- package/dist/{diagnosticCatalog-biferT4R.d.ts → diagnosticCatalog-DiflIock.d.ts} +22 -39
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +26 -26
- package/dist/index.d.ts +47 -11
- package/dist/index.js +26 -26
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +10 -10
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +10 -10
- package/docs/README.md +3 -3
- package/docs/package-surface.md +2 -1
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/agentProjectionMerge.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/agent-projection-merge.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { agentProjectionContentIdentity, ensureTrailingNewline, normalizeNewlines, safeVersion, } from './agent-projection-formatters.mjs';
|
|
12
|
+
const BEGIN_LINE_RE = /<!--\s*arkgate:agent-projection:begin\b([^>]*)-->/i;
|
|
13
|
+
const END_LINE_RE = /<!--\s*arkgate:agent-projection:end\s*-->/i;
|
|
14
|
+
// Attribute values: alnum + common version/schema tokens only (avoid open char-class ranges).
|
|
15
|
+
const VERSION_ATTR_RE = /\barkgateVersion=([A-Za-z0-9._+-]+)/i;
|
|
16
|
+
const SCHEMA_ATTR_RE = /\bschema=([A-Za-z0-9._+-]+)/i;
|
|
17
|
+
/**
|
|
18
|
+
* Extract the managed projection block from a document (AGENTS.md or equivalent).
|
|
19
|
+
*/
|
|
20
|
+
export function extractAgentProjectionBlock(document) {
|
|
21
|
+
const text = normalizeNewlines(document ?? '');
|
|
22
|
+
const beginMatch = BEGIN_LINE_RE.exec(text);
|
|
23
|
+
if (!beginMatch) {
|
|
24
|
+
return { block: null, body: null, before: text, after: '', beginAttrs: null };
|
|
25
|
+
}
|
|
26
|
+
const beginIndex = beginMatch.index;
|
|
27
|
+
const beginEnd = beginIndex + beginMatch[0].length;
|
|
28
|
+
const rest = text.slice(beginEnd);
|
|
29
|
+
const endMatch = END_LINE_RE.exec(rest);
|
|
30
|
+
if (!endMatch) {
|
|
31
|
+
// Unclosed block: treat as absent so merge can insert a well-formed block.
|
|
32
|
+
return { block: null, body: null, before: text, after: '', beginAttrs: null };
|
|
33
|
+
}
|
|
34
|
+
const endIndexInRest = endMatch.index;
|
|
35
|
+
const endEndInRest = endIndexInRest + endMatch[0].length;
|
|
36
|
+
// Strip a single leading newline after the begin marker; keep body content as-is.
|
|
37
|
+
let body = rest.slice(0, endIndexInRest);
|
|
38
|
+
if (body.startsWith('\n'))
|
|
39
|
+
body = body.slice(1);
|
|
40
|
+
const block = text.slice(beginIndex, beginEnd + endEndInRest);
|
|
41
|
+
const after = rest.slice(endEndInRest);
|
|
42
|
+
return {
|
|
43
|
+
block,
|
|
44
|
+
body,
|
|
45
|
+
before: text.slice(0, beginIndex),
|
|
46
|
+
after,
|
|
47
|
+
beginAttrs: beginMatch[1] ?? '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse stamps from a projection begin marker or full block/document.
|
|
52
|
+
*/
|
|
53
|
+
export function parseAgentProjectionStamp(source) {
|
|
54
|
+
const text = String(source ?? '');
|
|
55
|
+
const begin = BEGIN_LINE_RE.exec(text);
|
|
56
|
+
const attrs = begin?.[1] ?? text;
|
|
57
|
+
const versionMatch = VERSION_ATTR_RE.exec(attrs);
|
|
58
|
+
const schemaMatch = SCHEMA_ATTR_RE.exec(attrs);
|
|
59
|
+
const nonAuthoritative = /\bnonAuthoritative\s*=\s*true\b/i.test(attrs);
|
|
60
|
+
return {
|
|
61
|
+
arkgateVersion: versionMatch?.[1] ?? null,
|
|
62
|
+
schemaVersion: schemaMatch?.[1] ?? null,
|
|
63
|
+
nonAuthoritative,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* True when the document/block stamps the given package version.
|
|
68
|
+
*/
|
|
69
|
+
export function projectionMatchesPackageVersion(source, packageVersion) {
|
|
70
|
+
const stamped = parseAgentProjectionStamp(source).arkgateVersion;
|
|
71
|
+
if (!stamped)
|
|
72
|
+
return false;
|
|
73
|
+
return stamped === safeVersion(packageVersion);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* True when body text carries the non-enforcement label (substring match).
|
|
77
|
+
*/
|
|
78
|
+
export function projectionHasNonEnforcementLabel(bodyOrBlock) {
|
|
79
|
+
return String(bodyOrBlock ?? '').includes('non-authoritative');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Merge a desired projection block into an existing document without rewriting
|
|
83
|
+
* customized content **outside** the managed markers.
|
|
84
|
+
*
|
|
85
|
+
* - Missing document → create `# Ark Enforcement` + block
|
|
86
|
+
* - Existing markers → replace block when content-identity differs; else unchanged
|
|
87
|
+
* - No markers → insert block after the first markdown H1 (or at top)
|
|
88
|
+
*/
|
|
89
|
+
export function mergeAgentProjectionDocument(existing, desiredBlock) {
|
|
90
|
+
const desired = ensureTrailingNewline(normalizeNewlines(desiredBlock));
|
|
91
|
+
const desiredExtract = extractAgentProjectionBlock(desired);
|
|
92
|
+
const desiredBody = desiredExtract.body ??
|
|
93
|
+
desired.replace(BEGIN_LINE_RE, '').replace(END_LINE_RE, '').trim() + '\n';
|
|
94
|
+
const contentIdentity = agentProjectionContentIdentity(desiredBody);
|
|
95
|
+
if (existing == null || !String(existing).trim()) {
|
|
96
|
+
return {
|
|
97
|
+
content: ensureTrailingNewline(`# Ark Enforcement\n\n${desired}`),
|
|
98
|
+
action: 'created',
|
|
99
|
+
previousBlock: null,
|
|
100
|
+
contentIdentity,
|
|
101
|
+
preservedOutsideBlock: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const current = normalizeNewlines(existing);
|
|
105
|
+
const extracted = extractAgentProjectionBlock(current);
|
|
106
|
+
if (extracted.block != null) {
|
|
107
|
+
const currentBody = extracted.body ?? '';
|
|
108
|
+
if (agentProjectionContentIdentity(currentBody) === contentIdentity) {
|
|
109
|
+
return {
|
|
110
|
+
content: ensureTrailingNewline(current),
|
|
111
|
+
action: 'unchanged',
|
|
112
|
+
previousBlock: extracted.block,
|
|
113
|
+
contentIdentity,
|
|
114
|
+
preservedOutsideBlock: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const before = extracted.before.replace(/\s*$/, '\n\n');
|
|
118
|
+
const after = extracted.after.replace(/^\s*/, '\n');
|
|
119
|
+
return {
|
|
120
|
+
content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
|
|
121
|
+
action: 'block-replaced',
|
|
122
|
+
previousBlock: extracted.block,
|
|
123
|
+
contentIdentity,
|
|
124
|
+
preservedOutsideBlock: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Insert after first H1 line when present.
|
|
128
|
+
const h1 = /^(#\s+[^\n]*\n)/m.exec(current);
|
|
129
|
+
if (h1 && h1.index != null) {
|
|
130
|
+
const insertAt = h1.index + h1[1].length;
|
|
131
|
+
const before = current.slice(0, insertAt).replace(/\s*$/, '\n\n');
|
|
132
|
+
const after = current.slice(insertAt).replace(/^\s*/, '\n');
|
|
133
|
+
return {
|
|
134
|
+
content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
|
|
135
|
+
action: 'block-inserted',
|
|
136
|
+
previousBlock: null,
|
|
137
|
+
contentIdentity,
|
|
138
|
+
preservedOutsideBlock: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
content: ensureTrailingNewline(`${desired.trimEnd()}\n\n${current.trimStart()}`),
|
|
143
|
+
action: 'block-inserted',
|
|
144
|
+
previousBlock: null,
|
|
145
|
+
contentIdentity,
|
|
146
|
+
preservedOutsideBlock: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/agentProjectionTypes.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/agent-projection-types.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ARK_AGENT_PROJECTION_SCHEMA_VERSION = '1.0';
|
|
12
|
+
/** Begin marker for the managed projection region inside AGENTS.md (or equivalent). */
|
|
13
|
+
export const AGENT_PROJECTION_BEGIN_MARKER = '<!-- arkgate:agent-projection:begin';
|
|
14
|
+
/** End marker for the managed projection region. */
|
|
15
|
+
export const AGENT_PROJECTION_END_MARKER = '<!-- arkgate:agent-projection:end -->';
|
|
16
|
+
/**
|
|
17
|
+
* Non-enforcement label — must appear in every generated projection body.
|
|
18
|
+
* Agents and humans must not treat the projection as a pass/fail authority.
|
|
19
|
+
*/
|
|
20
|
+
export const AGENT_PROJECTION_NON_ENFORCEMENT_LABEL = 'This projection is **non-authoritative**. Enforcement is `ark-check` / host write hooks / required CI (`--strict-merge`), not AGENTS.md, skills, or this block.';
|
|
21
|
+
/** Surfaces that actually enforce (closed vocabulary for meta + docs). */
|
|
22
|
+
export const AGENT_PROJECTION_ENFORCEMENT_SURFACES = Object.freeze([
|
|
23
|
+
'ark-check',
|
|
24
|
+
'host-write-hooks',
|
|
25
|
+
'ci-strict-merge',
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* High-signal public ruleIds for the compact catalog short list in the projection.
|
|
29
|
+
* Full catalog remains `docs/diagnostics.md` / `DIAGNOSTIC_CATALOG` (ACS02).
|
|
30
|
+
* Titles are supplied by Tooling from the catalog when available.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_AGENT_PROJECTION_RULE_IDS = Object.freeze([
|
|
33
|
+
'LAYER_IMPORT_VIOLATION',
|
|
34
|
+
'LAYER_INTENT_REFERENCE_VIOLATION',
|
|
35
|
+
'CIRCULAR_DEPENDENCY',
|
|
36
|
+
'CAPABILITY_VIOLATION',
|
|
37
|
+
'RAW_EVENT_PUBLISH',
|
|
38
|
+
'ARKRULE_STRUCTURE',
|
|
39
|
+
'ATOMIC_PREFLIGHT_UNAVAILABLE',
|
|
40
|
+
'ANALYSIS_PARSE_INCOMPLETE',
|
|
41
|
+
'ARK_UNKNOWN',
|
|
42
|
+
]);
|
|
@@ -8,312 +8,6 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/agent-projection.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
/** End marker for the managed projection region. */
|
|
15
|
-
export const AGENT_PROJECTION_END_MARKER = '<!-- arkgate:agent-projection:end -->';
|
|
16
|
-
/**
|
|
17
|
-
* Non-enforcement label — must appear in every generated projection body.
|
|
18
|
-
* Agents and humans must not treat the projection as a pass/fail authority.
|
|
19
|
-
*/
|
|
20
|
-
export const AGENT_PROJECTION_NON_ENFORCEMENT_LABEL = 'This projection is **non-authoritative**. Enforcement is `ark-check` / host write hooks / required CI (`--strict-merge`), not AGENTS.md, skills, or this block.';
|
|
21
|
-
/** Surfaces that actually enforce (closed vocabulary for meta + docs). */
|
|
22
|
-
export const AGENT_PROJECTION_ENFORCEMENT_SURFACES = Object.freeze([
|
|
23
|
-
'ark-check',
|
|
24
|
-
'host-write-hooks',
|
|
25
|
-
'ci-strict-merge',
|
|
26
|
-
]);
|
|
27
|
-
/**
|
|
28
|
-
* High-signal public ruleIds for the compact catalog short list in the projection.
|
|
29
|
-
* Full catalog remains `docs/diagnostics.md` / `DIAGNOSTIC_CATALOG` (ACS02).
|
|
30
|
-
* Titles are supplied by Tooling from the catalog when available.
|
|
31
|
-
*/
|
|
32
|
-
export const DEFAULT_AGENT_PROJECTION_RULE_IDS = Object.freeze([
|
|
33
|
-
'LAYER_IMPORT_VIOLATION',
|
|
34
|
-
'LAYER_INTENT_REFERENCE_VIOLATION',
|
|
35
|
-
'CIRCULAR_DEPENDENCY',
|
|
36
|
-
'CAPABILITY_VIOLATION',
|
|
37
|
-
'RAW_EVENT_PUBLISH',
|
|
38
|
-
'ARKRULE_STRUCTURE',
|
|
39
|
-
'ATOMIC_PREFLIGHT_UNAVAILABLE',
|
|
40
|
-
'ANALYSIS_PARSE_INCOMPLETE',
|
|
41
|
-
'ARK_UNKNOWN',
|
|
42
|
-
]);
|
|
43
|
-
const BEGIN_LINE_RE = /<!--\s*arkgate:agent-projection:begin\b([^>]*)-->/i;
|
|
44
|
-
const END_LINE_RE = /<!--\s*arkgate:agent-projection:end\s*-->/i;
|
|
45
|
-
// Attribute values: alnum + common version/schema tokens only (avoid open char-class ranges).
|
|
46
|
-
const VERSION_ATTR_RE = /\barkgateVersion=([A-Za-z0-9._+-]+)/i;
|
|
47
|
-
const SCHEMA_ATTR_RE = /\bschema=([A-Za-z0-9._+-]+)/i;
|
|
48
|
-
/** FNV-1a identity — portable, no Node crypto (same family as stableHash). */
|
|
49
|
-
export function agentProjectionContentIdentity(body) {
|
|
50
|
-
const normalized = String(body ?? '').replace(/\r\n/g, '\n');
|
|
51
|
-
let hash = 0x811c9dc5;
|
|
52
|
-
for (let index = 0; index < normalized.length; index += 1) {
|
|
53
|
-
hash ^= normalized.charCodeAt(index);
|
|
54
|
-
hash = Math.imul(hash, 0x01000193);
|
|
55
|
-
}
|
|
56
|
-
return `fnv1a-${(hash >>> 0).toString(16).padStart(8, '0')}`;
|
|
57
|
-
}
|
|
58
|
-
function normalizeNewlines(text) {
|
|
59
|
-
return String(text ?? '').replace(/\r\n/g, '\n');
|
|
60
|
-
}
|
|
61
|
-
function ensureTrailingNewline(text) {
|
|
62
|
-
const normalized = normalizeNewlines(text);
|
|
63
|
-
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
64
|
-
}
|
|
65
|
-
function safeVersion(version) {
|
|
66
|
-
if (typeof version !== 'string' || version.trim().length === 0)
|
|
67
|
-
return 'unknown';
|
|
68
|
-
// Avoid breaking HTML comments / markdown with control chars.
|
|
69
|
-
return version.trim().replace(/[>\s]/g, '');
|
|
70
|
-
}
|
|
71
|
-
function resolveProfile(profile) {
|
|
72
|
-
return profile === 'compact' ? 'compact' : 'full';
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Build the managed begin marker line (includes version + nonAuthoritative stamp).
|
|
76
|
-
*/
|
|
77
|
-
export function buildAgentProjectionBeginMarker(facts) {
|
|
78
|
-
const version = safeVersion(facts.arkgateVersion);
|
|
79
|
-
const schema = typeof facts.schemaVersion === 'string' && facts.schemaVersion.trim()
|
|
80
|
-
? facts.schemaVersion.trim()
|
|
81
|
-
: ARK_AGENT_PROJECTION_SCHEMA_VERSION;
|
|
82
|
-
return `<!-- arkgate:agent-projection:begin schema=${schema} arkgateVersion=${version} nonAuthoritative=true -->`;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Layer placement rows for the projection (compact markdown table).
|
|
86
|
-
*/
|
|
87
|
-
export function formatAgentProjectionLayers(layers) {
|
|
88
|
-
if (!Array.isArray(layers) || layers.length === 0) {
|
|
89
|
-
return '_No project layers loaded — read `ark.config.json` or run `ark start` / `ark_manifest`._';
|
|
90
|
-
}
|
|
91
|
-
const rows = layers
|
|
92
|
-
.map((layer) => {
|
|
93
|
-
const name = layer.name?.trim() || 'Unknown';
|
|
94
|
-
const patternList = layer.patterns ?? [];
|
|
95
|
-
const prefixList = layer.intentPrefixes ?? [];
|
|
96
|
-
const patterns = patternList.map((pattern) => `\`${pattern}\``).join(', ') || '—';
|
|
97
|
-
const prefixes = prefixList.map((prefix) => `\`${prefix}\``).join(', ') || '—';
|
|
98
|
-
return `| ${name} | ${patterns} | ${prefixes} |`;
|
|
99
|
-
})
|
|
100
|
-
.join('\n');
|
|
101
|
-
return `| Layer | Patterns | Intent prefixes |
|
|
102
|
-
|-------|----------|-----------------|
|
|
103
|
-
${rows}`;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Catalog short-list bullets (ruleId + title). Empty list → pointer only.
|
|
107
|
-
*/
|
|
108
|
-
export function formatAgentProjectionCatalogShortList(entries, docsPath) {
|
|
109
|
-
const path = docsPath.trim() || 'docs/diagnostics.md';
|
|
110
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
111
|
-
return `Full public codes: \`${path}\` (and package \`DIAGNOSTIC_CATALOG\`).`;
|
|
112
|
-
}
|
|
113
|
-
const lines = entries
|
|
114
|
-
.filter((entry) => entry && typeof entry.ruleId === 'string' && entry.ruleId.length > 0)
|
|
115
|
-
.map((entry) => {
|
|
116
|
-
const title = typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : entry.ruleId;
|
|
117
|
-
return `- \`${entry.ruleId}\` — ${title}`;
|
|
118
|
-
});
|
|
119
|
-
return `${lines.join('\n')}
|
|
120
|
-
|
|
121
|
-
Full catalog: \`${path}\` (\`#RULE_ID\` anchors).`;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Projection **body** only (no begin/end markers). Used for content-identity.
|
|
125
|
-
*/
|
|
126
|
-
export function buildAgentProjectionBody(facts) {
|
|
127
|
-
const version = safeVersion(facts.arkgateVersion);
|
|
128
|
-
const profile = resolveProfile(facts.profile);
|
|
129
|
-
const checkCommand = typeof facts.checkCommand === 'string' && facts.checkCommand.trim()
|
|
130
|
-
? facts.checkCommand.trim()
|
|
131
|
-
: 'ark-check --strict-config';
|
|
132
|
-
const docsPath = typeof facts.diagnosticsDocsPath === 'string' && facts.diagnosticsDocsPath.trim()
|
|
133
|
-
? facts.diagnosticsDocsPath.trim()
|
|
134
|
-
: 'docs/diagnostics.md';
|
|
135
|
-
const hostRaw = typeof facts.host === 'string' ? facts.host.trim().toLowerCase() : '';
|
|
136
|
-
const host = hostRaw && hostRaw !== 'unknown' ? hostRaw : null;
|
|
137
|
-
const layers = Array.isArray(facts.layers) ? facts.layers : [];
|
|
138
|
-
const catalog = Array.isArray(facts.catalogShortList) ? facts.catalogShortList : [];
|
|
139
|
-
const lines = [
|
|
140
|
-
'## ArkGate agent contract projection',
|
|
141
|
-
'',
|
|
142
|
-
AGENT_PROJECTION_NON_ENFORCEMENT_LABEL,
|
|
143
|
-
'',
|
|
144
|
-
`- **arkgateVersion:** \`${version}\` (must match the installed package; regenerate with \`ark agents-md --write\` after upgrade)`,
|
|
145
|
-
`- **projectionSchema:** \`${ARK_AGENT_PROJECTION_SCHEMA_VERSION}\``,
|
|
146
|
-
`- **profile:** \`${profile}\`${host ? ` · **host:** \`${host}\`` : ''}`,
|
|
147
|
-
`- **after edits:** \`${checkCommand}\``,
|
|
148
|
-
'',
|
|
149
|
-
];
|
|
150
|
-
if (profile === 'compact') {
|
|
151
|
-
lines.push('### Primary path', '', '1. Run doctor (`ark-check --doctor`) — what is wrong and what to do first. Prefer the project-local CLI; do not wait on MCP “still connecting”.', '2. Name leftover work in plain language; never “done” on green imports alone while leftover design work remains.', '3. Identity handshake is optional when the CLI already resolved the project root. Call `ark_identity` only when using MCP evidence.', '4. Read the rules file with `ark_manifest` (same expectation) or the local `ark.config.json`. `ark://manifest` is compatibility-only / unverified.', '5. Place files inside configured layers; validate; run the check command above on violations — fix the import, do not weaken the rules file.', '6. Single door: illegal imports → fix; leftover design work → map then one small refactor with user OK.', '', '### Layers (summary)', '', formatAgentProjectionLayers(layers), '');
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
lines.push('### Contract layers', '', formatAgentProjectionLayers(layers), '', 'When creating a **new** kind of code that no layer covers, update `ark.config.json` first (`/ark-adopt`), then place the file.', '', '### Diagnostic codes (short list)', '', formatAgentProjectionCatalogShortList(catalog, docsPath), '', '### Session truth', '', '- Machine snapshot: `ark status --json` (or MCP `ark_status`) — identity, activation honesty, last check, residual counts. **Not a score.**', '- Authoritative contract: local `ark.config.json` / CLI, or `ark_manifest` after a matched `ark_identity` handshake. Identity is optional when CLI already resolved the root.', '- Host docs: the same projection schema is merged into `AGENTS.md` and `CLAUDE.md` (`ark agents-md --write`).', '');
|
|
155
|
-
}
|
|
156
|
-
lines.push('### Enforcement surfaces (authoritative)', '', AGENT_PROJECTION_ENFORCEMENT_SURFACES.map((surface) => `- \`${surface}\``).join('\n'), '');
|
|
157
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Full managed block: begin marker + body + end marker.
|
|
161
|
-
*/
|
|
162
|
-
export function buildAgentProjectionBlock(facts) {
|
|
163
|
-
const body = buildAgentProjectionBody(facts);
|
|
164
|
-
const begin = buildAgentProjectionBeginMarker({
|
|
165
|
-
arkgateVersion: facts.arkgateVersion,
|
|
166
|
-
schemaVersion: ARK_AGENT_PROJECTION_SCHEMA_VERSION,
|
|
167
|
-
});
|
|
168
|
-
return `${begin}\n${body}${AGENT_PROJECTION_END_MARKER}\n`;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Machine meta for CLI `--json` / tests (never a gate input).
|
|
172
|
-
*/
|
|
173
|
-
export function buildAgentProjectionMeta(facts) {
|
|
174
|
-
const body = buildAgentProjectionBody(facts);
|
|
175
|
-
const layers = Array.isArray(facts.layers) ? facts.layers : [];
|
|
176
|
-
const catalog = Array.isArray(facts.catalogShortList) ? facts.catalogShortList : [];
|
|
177
|
-
return {
|
|
178
|
-
schemaVersion: ARK_AGENT_PROJECTION_SCHEMA_VERSION,
|
|
179
|
-
arkgateVersion: safeVersion(facts.arkgateVersion),
|
|
180
|
-
nonAuthoritative: true,
|
|
181
|
-
enforcementSurfaces: [...AGENT_PROJECTION_ENFORCEMENT_SURFACES],
|
|
182
|
-
contentIdentity: agentProjectionContentIdentity(body),
|
|
183
|
-
layerCount: layers.length,
|
|
184
|
-
catalogCodeCount: catalog.filter((entry) => entry?.ruleId).length,
|
|
185
|
-
profile: resolveProfile(facts.profile),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Extract the managed projection block from a document (AGENTS.md or equivalent).
|
|
190
|
-
*/
|
|
191
|
-
export function extractAgentProjectionBlock(document) {
|
|
192
|
-
const text = normalizeNewlines(document ?? '');
|
|
193
|
-
const beginMatch = BEGIN_LINE_RE.exec(text);
|
|
194
|
-
if (!beginMatch) {
|
|
195
|
-
return { block: null, body: null, before: text, after: '', beginAttrs: null };
|
|
196
|
-
}
|
|
197
|
-
const beginIndex = beginMatch.index;
|
|
198
|
-
const beginEnd = beginIndex + beginMatch[0].length;
|
|
199
|
-
const rest = text.slice(beginEnd);
|
|
200
|
-
const endMatch = END_LINE_RE.exec(rest);
|
|
201
|
-
if (!endMatch) {
|
|
202
|
-
// Unclosed block: treat as absent so merge can insert a well-formed block.
|
|
203
|
-
return { block: null, body: null, before: text, after: '', beginAttrs: null };
|
|
204
|
-
}
|
|
205
|
-
const endIndexInRest = endMatch.index;
|
|
206
|
-
const endEndInRest = endIndexInRest + endMatch[0].length;
|
|
207
|
-
// Strip a single leading newline after the begin marker; keep body content as-is.
|
|
208
|
-
let body = rest.slice(0, endIndexInRest);
|
|
209
|
-
if (body.startsWith('\n'))
|
|
210
|
-
body = body.slice(1);
|
|
211
|
-
const block = text.slice(beginIndex, beginEnd + endEndInRest);
|
|
212
|
-
const after = rest.slice(endEndInRest);
|
|
213
|
-
return {
|
|
214
|
-
block,
|
|
215
|
-
body,
|
|
216
|
-
before: text.slice(0, beginIndex),
|
|
217
|
-
after,
|
|
218
|
-
beginAttrs: beginMatch[1] ?? '',
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Parse stamps from a projection begin marker or full block/document.
|
|
223
|
-
*/
|
|
224
|
-
export function parseAgentProjectionStamp(source) {
|
|
225
|
-
const text = String(source ?? '');
|
|
226
|
-
const begin = BEGIN_LINE_RE.exec(text);
|
|
227
|
-
const attrs = begin?.[1] ?? text;
|
|
228
|
-
const versionMatch = VERSION_ATTR_RE.exec(attrs);
|
|
229
|
-
const schemaMatch = SCHEMA_ATTR_RE.exec(attrs);
|
|
230
|
-
const nonAuthoritative = /\bnonAuthoritative\s*=\s*true\b/i.test(attrs);
|
|
231
|
-
return {
|
|
232
|
-
arkgateVersion: versionMatch?.[1] ?? null,
|
|
233
|
-
schemaVersion: schemaMatch?.[1] ?? null,
|
|
234
|
-
nonAuthoritative,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* True when the document/block stamps the given package version.
|
|
239
|
-
*/
|
|
240
|
-
export function projectionMatchesPackageVersion(source, packageVersion) {
|
|
241
|
-
const stamped = parseAgentProjectionStamp(source).arkgateVersion;
|
|
242
|
-
if (!stamped)
|
|
243
|
-
return false;
|
|
244
|
-
return stamped === safeVersion(packageVersion);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* True when body text carries the non-enforcement label (substring match).
|
|
248
|
-
*/
|
|
249
|
-
export function projectionHasNonEnforcementLabel(bodyOrBlock) {
|
|
250
|
-
return String(bodyOrBlock ?? '').includes('non-authoritative');
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Merge a desired projection block into an existing document without rewriting
|
|
254
|
-
* customized content **outside** the managed markers.
|
|
255
|
-
*
|
|
256
|
-
* - Missing document → create `# Ark Enforcement` + block
|
|
257
|
-
* - Existing markers → replace block when content-identity differs; else unchanged
|
|
258
|
-
* - No markers → insert block after the first markdown H1 (or at top)
|
|
259
|
-
*/
|
|
260
|
-
export function mergeAgentProjectionDocument(existing, desiredBlock) {
|
|
261
|
-
const desired = ensureTrailingNewline(normalizeNewlines(desiredBlock));
|
|
262
|
-
const desiredExtract = extractAgentProjectionBlock(desired);
|
|
263
|
-
const desiredBody = desiredExtract.body ??
|
|
264
|
-
desired.replace(BEGIN_LINE_RE, '').replace(END_LINE_RE, '').trim() + '\n';
|
|
265
|
-
const contentIdentity = agentProjectionContentIdentity(desiredBody);
|
|
266
|
-
if (existing == null || !String(existing).trim()) {
|
|
267
|
-
return {
|
|
268
|
-
content: ensureTrailingNewline(`# Ark Enforcement\n\n${desired}`),
|
|
269
|
-
action: 'created',
|
|
270
|
-
previousBlock: null,
|
|
271
|
-
contentIdentity,
|
|
272
|
-
preservedOutsideBlock: false,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
const current = normalizeNewlines(existing);
|
|
276
|
-
const extracted = extractAgentProjectionBlock(current);
|
|
277
|
-
if (extracted.block != null) {
|
|
278
|
-
const currentBody = extracted.body ?? '';
|
|
279
|
-
if (agentProjectionContentIdentity(currentBody) === contentIdentity) {
|
|
280
|
-
return {
|
|
281
|
-
content: ensureTrailingNewline(current),
|
|
282
|
-
action: 'unchanged',
|
|
283
|
-
previousBlock: extracted.block,
|
|
284
|
-
contentIdentity,
|
|
285
|
-
preservedOutsideBlock: true,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
const before = extracted.before.replace(/\s*$/, '\n\n');
|
|
289
|
-
const after = extracted.after.replace(/^\s*/, '\n');
|
|
290
|
-
return {
|
|
291
|
-
content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
|
|
292
|
-
action: 'block-replaced',
|
|
293
|
-
previousBlock: extracted.block,
|
|
294
|
-
contentIdentity,
|
|
295
|
-
preservedOutsideBlock: true,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
// Insert after first H1 line when present.
|
|
299
|
-
const h1 = /^(#\s+[^\n]*\n)/m.exec(current);
|
|
300
|
-
if (h1 && h1.index != null) {
|
|
301
|
-
const insertAt = h1.index + h1[1].length;
|
|
302
|
-
const before = current.slice(0, insertAt).replace(/\s*$/, '\n\n');
|
|
303
|
-
const after = current.slice(insertAt).replace(/^\s*/, '\n');
|
|
304
|
-
return {
|
|
305
|
-
content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
|
|
306
|
-
action: 'block-inserted',
|
|
307
|
-
previousBlock: null,
|
|
308
|
-
contentIdentity,
|
|
309
|
-
preservedOutsideBlock: true,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
content: ensureTrailingNewline(`${desired.trimEnd()}\n\n${current.trimStart()}`),
|
|
314
|
-
action: 'block-inserted',
|
|
315
|
-
previousBlock: null,
|
|
316
|
-
contentIdentity,
|
|
317
|
-
preservedOutsideBlock: true,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
11
|
+
export { AGENT_PROJECTION_BEGIN_MARKER, AGENT_PROJECTION_END_MARKER, AGENT_PROJECTION_ENFORCEMENT_SURFACES, AGENT_PROJECTION_NON_ENFORCEMENT_LABEL, ARK_AGENT_PROJECTION_SCHEMA_VERSION, DEFAULT_AGENT_PROJECTION_RULE_IDS, } from './agent-projection-types.mjs';
|
|
12
|
+
export { agentProjectionContentIdentity, buildAgentProjectionBeginMarker, buildAgentProjectionBlock, buildAgentProjectionBody, buildAgentProjectionMeta, formatAgentProjectionCatalogShortList, formatAgentProjectionLayers, } from './agent-projection-formatters.mjs';
|
|
13
|
+
export { extractAgentProjectionBlock, mergeAgentProjectionDocument, parseAgentProjectionStamp, projectionHasNonEnforcementLabel, projectionMatchesPackageVersion, } from './agent-projection-merge.mjs';
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -88,10 +88,64 @@ export function resolveConfigPathWithinRoot(projectRoot, configPathOrName) {
|
|
|
88
88
|
return { ok: true, configPath };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function isFile(absPath) {
|
|
92
|
+
try {
|
|
93
|
+
return Boolean(fs.statSync(absPath, { throwIfNoEntry: false })?.isFile());
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True when --config is a path (nested or absolute), not a basename to walk. */
|
|
100
|
+
export function configNameIsPath(configName) {
|
|
101
|
+
return (
|
|
102
|
+
typeof configName === 'string' &&
|
|
103
|
+
(path.isAbsolute(configName) || /[\\/]/.test(configName))
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isInsideRoot(root, target) {
|
|
108
|
+
const rel = path.relative(root, target);
|
|
109
|
+
return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a nested/absolute --config path without walking parents.
|
|
114
|
+
* `--root examples/app --config examples/app/ark.config.json` must load the nested
|
|
115
|
+
* contract, not latch onto a parent basename walk.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} startDir
|
|
118
|
+
* @param {string} configName
|
|
119
|
+
* @returns {string | null} absolute config file path
|
|
120
|
+
*/
|
|
121
|
+
export function resolveConfigPathCandidate(startDir, configName) {
|
|
122
|
+
if (typeof configName !== 'string' || configName.trim() === '') return null;
|
|
123
|
+
const start = path.resolve(startDir || process.cwd());
|
|
124
|
+
if (path.isAbsolute(configName)) return isFile(configName) ? path.resolve(configName) : null;
|
|
125
|
+
|
|
126
|
+
const fromStart = path.resolve(start, configName);
|
|
127
|
+
if (isFile(fromStart)) return fromStart;
|
|
128
|
+
|
|
129
|
+
const base = path.basename(configName);
|
|
130
|
+
const relDir = path.dirname(configName);
|
|
131
|
+
if (relDir && relDir !== '.') {
|
|
132
|
+
const namedLeaf = path.basename(relDir);
|
|
133
|
+
if (namedLeaf && namedLeaf === path.basename(start)) {
|
|
134
|
+
const stripped = path.join(start, base);
|
|
135
|
+
if (isFile(stripped)) return stripped;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fromCwd = path.resolve(process.cwd(), configName);
|
|
140
|
+
if (fromCwd !== fromStart && isFile(fromCwd)) return fromCwd;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
/**
|
|
92
145
|
* Walk parents from startDir looking for configName (default ark.config.json).
|
|
93
146
|
* Bounds: filesystem root, max depth, git root, workspaces package root.
|
|
94
147
|
* Config found at a bound root is accepted; walking above a bound is refused.
|
|
148
|
+
* Nested relative --config (`examples/app/ark.config.json`) is a file path, never a walk-up name.
|
|
95
149
|
*
|
|
96
150
|
* @param {string} startDir
|
|
97
151
|
* @param {string} [configName='ark.config.json']
|
|
@@ -104,17 +158,29 @@ export function findNearestArkConfig(startDir, configName = 'ark.config.json', o
|
|
|
104
158
|
const boundAtWorkspaces = opts.boundAtWorkspacesRoot !== false;
|
|
105
159
|
|
|
106
160
|
if (typeof configName === 'string' && path.isAbsolute(configName)) {
|
|
107
|
-
if (
|
|
161
|
+
if (isFile(configName)) {
|
|
108
162
|
const root = path.dirname(configName);
|
|
109
163
|
const start = path.resolve(startDir || process.cwd());
|
|
110
|
-
return { root, configPath: configName, walkedUp: path.resolve(root) !== start };
|
|
164
|
+
return { root, configPath: path.resolve(configName), walkedUp: path.resolve(root) !== start };
|
|
111
165
|
}
|
|
112
166
|
return null;
|
|
113
167
|
}
|
|
114
168
|
|
|
169
|
+
const name = configName || 'ark.config.json';
|
|
170
|
+
if (configNameIsPath(name)) {
|
|
171
|
+
const resolved = resolveConfigPathCandidate(startDir, name);
|
|
172
|
+
if (!resolved) return null;
|
|
173
|
+
const start = path.resolve(startDir || process.cwd());
|
|
174
|
+
const inside = isInsideRoot(start, resolved);
|
|
175
|
+
return {
|
|
176
|
+
root: inside ? start : path.dirname(resolved),
|
|
177
|
+
configPath: resolved,
|
|
178
|
+
walkedUp: !inside,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
115
182
|
let dir = path.resolve(startDir || process.cwd());
|
|
116
183
|
const start = dir;
|
|
117
|
-
const name = configName || 'ark.config.json';
|
|
118
184
|
let depth = 0;
|
|
119
185
|
for (;;) {
|
|
120
186
|
const candidate = path.join(dir, name);
|
|
@@ -231,7 +297,7 @@ export function resolveEffectiveProjectRoot(startRoot, opts = {}) {
|
|
|
231
297
|
config: configName,
|
|
232
298
|
configPath: found.configPath,
|
|
233
299
|
configRoot: found.root,
|
|
234
|
-
walkedUp:
|
|
300
|
+
walkedUp: found.walkedUp,
|
|
235
301
|
configFound: true,
|
|
236
302
|
writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
|
|
237
303
|
};
|