aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
100 lines • 4.16 kB
JavaScript
/**
* Managed-marker normalization for hash equivalence.
*
* The deployer injects a managed-marker comment into every deployed
* markdown artifact (see `tools/agents/providers/base.mjs:addManagedMarker`).
* Two forms exist:
*
* 1. YAML-frontmatter inline (current — non-breaking for parsers):
*
* ---
* # aiwg:managed v<VERSION> <SOURCE>
* name: ...
* ...
* ---
*
* 2. HTML-comment-on-line-1 (legacy — files with no frontmatter):
*
* <!-- aiwg:managed v<VERSION> <SOURCE> -->
* # Some heading
*
* Source files in the repo do NOT carry the marker; it's a deploy-time
* artifact only. That means a naive `sha256(source) == sha256(deployed)`
* comparison will always fail by exactly one line, producing the spurious
* drift detected in #1086 and the same false-positive surface in
* `aiwg remove`'s pristine/mutated classifier (per #1048's design).
*
* This module makes the marker invisible to the equivalence relation:
* both deploy-time hash recording and post-deploy comparison hash the
* marker-stripped form. The marker therefore never participates in the
* "is this the same file?" check.
*
* Trade-off: an operator who removes the marker from a deployed file
* but leaves the rest unchanged is NOT flagged as drift (stripped both
* sides → same content → same hash). This is a conscious choice — the
* marker exists for `aiwg remove`'s ownership labelling, not as a
* tamper-evident seal. Real content edits remain detected normally.
*
* @implements #1086
*/
import { readFile } from 'fs/promises';
import { createHash } from 'crypto';
/**
* Match either marker form:
* - HTML comment at line 1: `<!-- aiwg:managed v... ... -->\n`
* - YAML comment inside frontmatter (line 2): `# aiwg:managed v... ...\n`
*
* Multiline mode anchors `^` to start of any line, so the YAML form is
* matched whether it appears as the first or second line of the file.
*
* The pattern intentionally does NOT consume any other line; it strips
* only the single marker line and its trailing newline.
*/
const MANAGED_MARKER_LINE_RE = /^(?:<!-- aiwg:managed [^\n]*-->|# aiwg:managed [^\n]*)\n/m;
/**
* Return the content with the managed-marker line removed if present.
* Idempotent — calling twice on the same input is the same as calling once.
*
* If no marker is present, returns the input unchanged (referentially —
* same string instance).
*/
export function stripManagedMarker(content) {
return content.replace(MANAGED_MARKER_LINE_RE, '');
}
/**
* Read a file from disk, strip any managed-marker line, and return the
* SHA-256 hex digest of the result.
*
* Throws on read errors (caller should catch and treat as missing).
*
* Used in two places:
* - `hashBundleArtifacts()` records this hash for source files at
* deploy time. Source files have no marker, so this is identical
* to the previous unnormalized hash — backwards-compatible with
* existing registry entries.
* - Drift detection (`aiwg doctor`) and pristine classification
* (`aiwg remove`) hash the deployed file using this function. The
* marker line is stripped before hashing so the two sides match.
*/
export async function sha256OfFileNormalized(absPath) {
const buf = await readFile(absPath, 'utf8');
const stripped = stripManagedMarker(buf);
return createHash('sha256').update(stripped, 'utf8').digest('hex');
}
/**
* Read a file and return both raw and managed-marker-normalized hashes.
*
* This is used by drift detectors that must remain compatible with
* registries populated before #1086 normalization shipped. In that narrow
* case the registry may contain the raw deployed hash, while newer deploys
* contain the normalized source hash.
*/
export async function sha256OfFileRawAndNormalized(absPath) {
const buf = await readFile(absPath, 'utf8');
const stripped = stripManagedMarker(buf);
return {
raw: createHash('sha256').update(buf, 'utf8').digest('hex'),
normalized: createHash('sha256').update(stripped, 'utf8').digest('hex'),
};
}
//# sourceMappingURL=managed-marker.js.map