v0.1.0-rc.12
Pre-release
Pre-release
·
120 commits
to main
since this release
Added
profile.employment.skills.add/profile.employment.skills.remove:
per-skill additive wrappers over the full-replaceUpdateEmployment
(#614). Bulk profile uplift across many employments (the maintainer
carries 13 rows, the largest with 100+ skills) previously forced callers
to re-implement the read-merge-write bookkeeping ttctl already performs
internally — one merge script per additive op. Both leaves wrap
update()'sbuildUpdateEmploymentInputmerge path: read the current
row viashow(), compute the merged (dedupe by id, preserve current
order) or filtered skills array, then fire oneUpdateEmployment
mutation per row. Discriminated outcome (updated | noop | preview):
addreturnsnoopwhen every supplied id is already linked (no wire
fire);removereturnsnoopwhen no supplied id matches the row AND
refuses withVALIDATION_ERRORwhen the filtered set would be empty
(Toptal server rejectsskills: [], namingprofile.employment.remove
as the row-level alternative); caller-supplied duplicates dedupe against
each other and against current state. Core:profile.employment.skills
namespace exportingadd(token, employmentId, skillIds, options)and
remove(...). CLI:ttctl profile employment skills {add,remove} <id> --skill-id <id>(repeatable). MCP:ttctl_profile_employment_skills_{add,remove}
using the existingDRY_RUN_EMPLOYMENT_MERGE_PLACEHOLDERfor zero-wire
preview. Wire-shape disposition: Schema/contract rule triggered
(pathpackages/core/src/services/profile/employment/**) but introduces
no new GraphQL ops — both wrappers route through the existing
UpdateEmploymentmutation; Track 1 (existing snapshot) —
UpdateEmploymentsnapshot owned by
46-profile-employment-update-merge.e2e.test.ts. Validated live
(TTCTL_E2E=1) viapackages/e2e/src/79-profile-employment-skills.e2e.test.ts
(add → idempotent re-add → remove → idempotent re-remove →
refusal-on-empty).profile.skills.remove-connection: per-edge unlink of one
ProfileSkillSet → entitylink via theremoveProfileSkillSetConnection
mutation (#463). Sibling toprofile.skills.add-connection(#462 /
rc.11); removes one connection without cascading to the whole skill-set.
Wire input is CAPTURED
(research/captures/web/inputs/RemoveProfileSkillSetConnectionInput.json):
{ skillSetId, connectionId }— two fields, noconnectionType(the
server discriminates the target from the Relay node id's base64 type
prefix; this refutes the prior Pattern-6 inference in
research/notes/10). Same ADR-009profile-capabilityconsent domain
(profileCapabilityConsentIssued: true). Core:
profile.skills.removeConnection(token, fields, consent, options)
returns{ skillSetId, connectionsCount, connectionIds, notice }with
the just-unlinked id absent fromconnectionIds; consent gate
(CONSENT_REQUIREDfires BEFORE dry-run);USER_ERRORmapping for
success: false/errors[];UNKNOWNfor null payload;
AuthRevokedError/Cf403Errorpropagated. CLI:ttctl profile skills remove-connection --skill-set-id <id> --connection-id <id> --consent-profile-capability(no--connection-typeflag — locked at the
unit level byexpect(flags).not.toContain('--connection-type')to
prevent regression to the 3-field wire shape). MCP:
ttctl_profile_skills_remove_connectionwithdestructiveHint: true
andprofileCapabilityConsentIssued: z.literal(true). Wire-shape
disposition: Schema/contract rule triggered (new GraphQL op
removeProfileSkillSetConnectionunder
packages/core/src/services/profile/**, in
TALENT_PROFILE_KNOWN_UNTRUSTED_OPS); Track 1 — snapshot file
packages/e2e/src/wire-snapshots/removeProfileSkillSetConnection.snapshot.json
is intentionally absent at PR-merge time and captured operator-driven
viaTTCTL_E2E_REMOVE_SKILL_CONNECTION=…:… TTCTL_UPDATE_WIRE_SNAPSHOTS=1
post-merge (mirrors sibling #462). Validated live (TTCTL_E2E=1) via
packages/e2e/src/80-profile-skills-remove-connection.e2e.test.ts
(always-on dry-run + consent-missing paths; gated DESTRUCTIVE positive
path requires a populated skill-set with a linked target).
Changed
profile.skills.add-connection: trim wire-extraconnectionTypeand
add a Relay-prefix cross-check (#626). The capture
research/captures/web/inputs/AddProfileSkillSetConnectionInput.json
sends only{ skillSetId, connectionId }(two fields) — the server
discriminates the target from the Relay node id's base64 type segment.
ttctl's previous send shape (three fields includingconnectionType)
was Pattern-6 inferred at #462; this PR aligns the wire to the captured
shape and keeps--connection-typeat the CLI / MCP surface as a
client-side UX guard. New privateinferConnectionTypeFromIdhelper +
RELAY_PREFIX_TO_CONNECTION_TYPEmap cross-check the declared
connectionTypeagainst theconnectionIdRelay prefix; both an
unrecognized prefix and a prefix-vs-declared-type mismatch now throw
VALIDATION_ERRORBEFORE any wire call. Wire-shape disposition:
Schema/contract rule triggered (existing op, wire input shape
modified; touchespackages/core/src/services/profile/**); Track 1
(addProfileSkillSetConnectioninTALENT_PROFILE_KNOWN_UNTRUSTED_OPS;
the response snapshot at
packages/e2e/src/wire-snapshots/addProfileSkillSetConnection.snapshot.json
is unchanged, and the request-side trim is asserted at the unit-test
level via.toEqualon the dry-run preview'svariables.input).
BC: callers passing an unrecognized Relay prefix or a
prefix-vs-declared-type mismatch now surfaceVALIDATION_ERRORat the
service boundary instead of aGRAPHQL_ERRORfrom the live wire —
same end-state (operation refused), better diagnostics. Suite-wide
sweep dropped stale "Pattern-6 / INFERRED wire shape" claims from the
CLI--help, service / tool JSDoc, the78-profile-skills-add-connection.e2e.test.ts
header, and theaddProfileSkillSetConnectionrow rationale in
docs/wire-validation-routing.md.
Fixed
profile.specializations.showexposesoperations.apply.callableas
a string, not a boolean (#637). The synthesized schema declares
Operation.callable: String!(empirical value"ENABLED") and the
codegen already types every other site correctly; only the
specializations service mistyped it asbooleanand projected with
?? false— the wire was always returning a string ("ENABLED"), and
TTCTL_E2E=1 pnpm test:e2e src/66-profile-specializations-show.e2e.test.ts
surfaced the drift viatypeof apply["callable"]returning"string"
not"boolean". Scope:SpecializationApplyOperation.callable
retypedstring; projection defaults to""on a missing wire field;
pretty / table formatters drop the dead.toString()calls; CLI text
formatter renders(unset)for the empty default; MCP tool docs
corrected. BC: the JSON / YAML output for
ttctl profile specializations shownow emits a string value (e.g.
"ENABLED") instead of a boolean —jq/yqconsumers comparing
with==or===need to compare against the string. Same posture
for callers ofprofile.specializations.show()programmatically.profile.education.updateno longer nulls omitted writable fields under
the full-replacement contract, andadd/updatemap ttctl-surface
institutionto wiretitle(the only school-name slot on
EducationInput) (#612). Same posture asUpdateCertification#605
andUpdateBasicInfo#604:UpdateEducationtreatsEducationInputas
a full replacement — pre-fix, callingttctl profile education update <id> --highlight truewould null every other field server-side. Pre-fix,
add()andupdate()also senteducation.institution: <value>to a
wire input that has noinstitutionslot (capture
research/captures/web/inputs/UpdateEducationInput.json) — the live API
rejected withGRAPHQL_ERROR, blocking BOTH adding and updating
education rows from ttctl. Scope:coreupdate()now reads the
current row viashow()then merges through the exported
buildUpdateEducationInput(current, fields)helper (mirror of
buildUpdateCertificationInput);add()builds the wire input via
toEducationWireInput(fields)and defaultsskills: [](the wire
requires non-null, same.blank?posture as the cert sibling).
DRY_RUN_EDUCATION_FIELD_PLACEHOLDERis exported for the MCP layer;
the MCP_updatedry-run preview now surfaces the placeholder for
every unconditional-echo field. BC: removed the--titleflag from
ttctl profile education add/updateand thetitleinput on the
MCPttctl_profile_education_add/_updatetools — the wiretitle
slot is owned byinstitution(school name), and the read-side
Education.titleis server-populated, not user-controlled. Any caller
passing--titlewas previously overwriting the school name; no
preservation path exists because the previous semantics were
data-corrupting.profile.skillsexperienceis years on the wire, not months (#627).
The MCPttctl_profile_skills_update.experiencedocstring claimed
"months"; the CLI--experienceflag,parseExperienceparser,
validation messages, and pretty-print rendering all carried the same
wrong-unit assumption (withparseExperienceACTIVELY multiplyingNy
inputs by 12). The wire capture at
research/captures/web/inputs/UpdateProfileSkillSetExperienceInput.json
annotatesInt (years; 1-20+), and several talents' public profiles
rendered nonsense values (e.g., "60 years" for a skill the caller
intended as 5 years). Scope: corrected every docstring, validation
message, help text, parser, format helper, service jsdoc, test fixture,
and snapshot in the skills sub-domain.parseExperienceno longer
multiplies by 12 —5ynow returns5; theNm(months) shorthand
is rejected outright (was never working semantics). Added defensive
.max(70)Zod bound on bothttctl_profile_skills_add.experienceand
ttctl_profile_skills_update.experienceso corruption attempts
surface at the MCP boundary instead of silently landing on the wire.
BC note: any caller passing--experience 5ypreviously sent60
(= 60 years on the wire — already broken); now sends5correctly. No
preservation path exists for the previous semantics because the
previous semantics were data-corrupting.